Merge branch 'develop' of github.com:Budibase/budibase into develop
This commit is contained in:
commit
650480e3a6
76 changed files with 2226 additions and 785 deletions
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.18",
|
"version": "1.0.19-alpha.0",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/auth",
|
"name": "@budibase/auth",
|
||||||
"version": "1.0.18",
|
"version": "1.0.19-alpha.0",
|
||||||
"description": "Authentication middlewares for budibase builder and apps",
|
"description": "Authentication middlewares for budibase builder and apps",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "1.0.18",
|
"version": "1.0.19-alpha.0",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
|
|
@ -80,4 +80,8 @@
|
||||||
.active svg {
|
.active svg {
|
||||||
color: var(--spectrum-global-color-blue-600);
|
color: var(--spectrum-global-color-blue-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spectrum-ActionButton-label {
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
59
packages/bbui/src/Banner/Banner.svelte
Normal file
59
packages/bbui/src/Banner/Banner.svelte
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
let dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
export let type = "info"
|
||||||
|
export let icon = "Info"
|
||||||
|
export let size = "S"
|
||||||
|
export let extraButtonText
|
||||||
|
export let extraButtonAction
|
||||||
|
|
||||||
|
let show = true
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
show = false
|
||||||
|
dispatch("change")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if show}
|
||||||
|
<div class="spectrum-Toast spectrum-Toast--{type}">
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--size{size} spectrum-Toast-typeIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||||
|
</svg>
|
||||||
|
<div class="spectrum-Toast-body">
|
||||||
|
<div class="spectrum-Toast-content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{#if extraButtonText && extraButtonAction}
|
||||||
|
<button
|
||||||
|
class="spectrum-Button spectrum-Button--sizeM spectrum-Button--overBackground spectrum-Button--quiet"
|
||||||
|
on:click={extraButtonAction}
|
||||||
|
>
|
||||||
|
<span class="spectrum-Button-label">{extraButtonText}</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="spectrum-Toast-buttons">
|
||||||
|
<button
|
||||||
|
class="spectrum-ClearButton spectrum-ClearButton--overBackground spectrum-ClearButton--size{size}"
|
||||||
|
on:click={clear}
|
||||||
|
>
|
||||||
|
<div class="spectrum-ClearButton-fill">
|
||||||
|
<svg
|
||||||
|
class="spectrum-ClearButton-icon spectrum-Icon spectrum-UIIcon-Cross100"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-css-icon-Cross100" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -25,7 +25,9 @@
|
||||||
class="spectrum-Switch-input"
|
class="spectrum-Switch-input"
|
||||||
/>
|
/>
|
||||||
<span class="spectrum-Switch-switch" />
|
<span class="spectrum-Switch-switch" />
|
||||||
|
{#if text}
|
||||||
<label class="spectrum-Switch-label" for={id}>{text}</label>
|
<label class="spectrum-Switch-label" for={id}>{text}</label>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let error = null
|
export let error = null
|
||||||
export let id = null
|
export let id = null
|
||||||
|
export let height = null
|
||||||
|
export let minHeight = null
|
||||||
export const getCaretPosition = () => ({
|
export const getCaretPosition = () => ({
|
||||||
start: textarea.selectionStart,
|
start: textarea.selectionStart,
|
||||||
end: textarea.selectionEnd,
|
end: textarea.selectionEnd,
|
||||||
|
@ -22,6 +24,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
style={(height ? `height: ${height}px;` : "") +
|
||||||
|
(minHeight ? `min-height: ${minHeight}px` : "")}
|
||||||
class="spectrum-Textfield spectrum-Textfield--multiline"
|
class="spectrum-Textfield spectrum-Textfield--multiline"
|
||||||
class:is-invalid={!!error}
|
class:is-invalid={!!error}
|
||||||
class:is-disabled={disabled}
|
class:is-disabled={disabled}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
export let labelPosition = "above"
|
export let labelPosition = "above"
|
||||||
export let error = null
|
export let error = null
|
||||||
export let options = []
|
export let options = []
|
||||||
|
export let direction = "vertical"
|
||||||
export let getOptionLabel = option => extractProperty(option, "label")
|
export let getOptionLabel = option => extractProperty(option, "label")
|
||||||
export let getOptionValue = option => extractProperty(option, "value")
|
export let getOptionValue = option => extractProperty(option, "value")
|
||||||
|
|
||||||
|
@ -31,6 +32,7 @@
|
||||||
{disabled}
|
{disabled}
|
||||||
{value}
|
{value}
|
||||||
{options}
|
{options}
|
||||||
|
{direction}
|
||||||
{getOptionLabel}
|
{getOptionLabel}
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let error = null
|
export let error = null
|
||||||
export let getCaretPosition = null
|
export let getCaretPosition = null
|
||||||
|
export let height = null
|
||||||
|
export let minHeight = null
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
|
@ -25,6 +27,8 @@
|
||||||
{disabled}
|
{disabled}
|
||||||
{value}
|
{value}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
|
{height}
|
||||||
|
{minHeight}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
export let hoverable = false
|
export let hoverable = false
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
export let color
|
||||||
|
|
||||||
$: rotation = getRotation(direction)
|
$: rotation = getRotation(direction)
|
||||||
|
|
||||||
|
@ -25,7 +26,9 @@
|
||||||
focusable="false"
|
focusable="false"
|
||||||
aria-hidden={hidden}
|
aria-hidden={hidden}
|
||||||
aria-label={name}
|
aria-label={name}
|
||||||
style={`transform: rotate(${rotation}deg)`}
|
style={`transform: rotate(${rotation}deg); ${
|
||||||
|
color ? `color: ${color};` : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<use xlink:href="#spectrum-icon-18-{name}" />
|
<use xlink:href="#spectrum-icon-18-{name}" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
<div
|
<div
|
||||||
class="icon"
|
class="icon"
|
||||||
|
class:icon-small={size === "M" || size === "S"}
|
||||||
on:mouseover={() => (showTooltip = true)}
|
on:mouseover={() => (showTooltip = true)}
|
||||||
on:mouseleave={() => (showTooltip = false)}
|
on:mouseleave={() => (showTooltip = false)}
|
||||||
>
|
>
|
||||||
|
@ -44,6 +45,7 @@
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.icon-container {
|
.icon-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -64,4 +66,8 @@
|
||||||
.icon {
|
.icon {
|
||||||
transform: scale(0.75);
|
transform: scale(0.75);
|
||||||
}
|
}
|
||||||
|
.icon-small {
|
||||||
|
margin-top: -2px;
|
||||||
|
margin-bottom: -5px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
export let icon = ""
|
export let icon = ""
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const selected = getContext("tab")
|
let selected = getContext("tab")
|
||||||
let tab
|
let tab
|
||||||
let tabInfo
|
let tabInfo
|
||||||
|
|
||||||
|
@ -16,8 +16,8 @@
|
||||||
// We just need to get this off the main thread to fix this, by using
|
// We just need to get this off the main thread to fix this, by using
|
||||||
// a 0ms timeout.
|
// a 0ms timeout.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
tabInfo = tab.getBoundingClientRect()
|
tabInfo = tab?.getBoundingClientRect()
|
||||||
if ($selected.title === title) {
|
if (tabInfo && $selected.title === title) {
|
||||||
$selected.info = tabInfo
|
$selected.info = tabInfo
|
||||||
}
|
}
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
|
@ -6,9 +6,13 @@
|
||||||
export let selected
|
export let selected
|
||||||
export let vertical = false
|
export let vertical = false
|
||||||
export let noPadding = false
|
export let noPadding = false
|
||||||
|
// added as a separate option as noPadding is used for vertical padding
|
||||||
|
export let noHorizPadding = false
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let emphasized = false
|
export let emphasized = false
|
||||||
|
|
||||||
|
let thisSelected = undefined
|
||||||
|
|
||||||
let _id = id()
|
let _id = id()
|
||||||
const tab = writable({ title: selected, id: _id, emphasized })
|
const tab = writable({ title: selected, id: _id, emphasized })
|
||||||
setContext("tab", tab)
|
setContext("tab", tab)
|
||||||
|
@ -18,9 +22,19 @@
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if ($tab.title !== selected) {
|
if (thisSelected !== selected) {
|
||||||
|
thisSelected = selected
|
||||||
|
dispatch("select", thisSelected)
|
||||||
|
} else if ($tab.title !== thisSelected) {
|
||||||
|
thisSelected = $tab.title
|
||||||
selected = $tab.title
|
selected = $tab.title
|
||||||
dispatch("select", selected)
|
dispatch("select", thisSelected)
|
||||||
|
}
|
||||||
|
if ($tab.title !== thisSelected) {
|
||||||
|
tab.update(state => {
|
||||||
|
state.title = thisSelected
|
||||||
|
return state
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,6 +73,7 @@
|
||||||
<div
|
<div
|
||||||
bind:this={container}
|
bind:this={container}
|
||||||
class:quiet
|
class:quiet
|
||||||
|
class:noHorizPadding
|
||||||
class="selected-border spectrum-Tabs {quiet &&
|
class="selected-border spectrum-Tabs {quiet &&
|
||||||
'spectrum-Tabs--quiet'} spectrum-Tabs--{vertical
|
'spectrum-Tabs--quiet'} spectrum-Tabs--{vertical
|
||||||
? 'vertical'
|
? 'vertical'
|
||||||
|
@ -99,6 +114,9 @@
|
||||||
.spectrum-Tabs--horizontal .spectrum-Tabs-selectionIndicator {
|
.spectrum-Tabs--horizontal .spectrum-Tabs-selectionIndicator {
|
||||||
bottom: 0 !important;
|
bottom: 0 !important;
|
||||||
}
|
}
|
||||||
|
.noHorizPadding {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
.noPadding {
|
.noPadding {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,7 @@ export { default as Badge } from "./Badge/Badge.svelte"
|
||||||
export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
|
export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
|
||||||
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
|
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
|
||||||
export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte"
|
export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte"
|
||||||
|
export { default as Banner } from "./Banner/Banner.svelte"
|
||||||
|
|
||||||
// Typography
|
// Typography
|
||||||
export { default as Body } from "./Typography/Body.svelte"
|
export { default as Body } from "./Typography/Body.svelte"
|
||||||
|
|
227
packages/builder/assets/bb-spaceship.svg
Executable file
227
packages/builder/assets/bb-spaceship.svg
Executable file
|
@ -0,0 +1,227 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 424.9 376.9" style="enable-background:new 0 0 424.9 376.9;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:url(#SVGID_1_);}
|
||||||
|
.st1{fill:url(#SVGID_00000173146453699406745440000014498258185172885933_);}
|
||||||
|
.st2{fill:url(#SVGID_00000072276482643233320020000011975551142177928352_);}
|
||||||
|
.st3{fill:url(#SVGID_00000000209806784423878430000006497884456548529061_);}
|
||||||
|
.st4{fill:url(#SVGID_00000145771761158215620900000018209346423627093124_);}
|
||||||
|
.st5{fill:#FFFFFF;}
|
||||||
|
.st6{fill:url(#SVGID_00000134247842479715961920000014109114775597637809_);}
|
||||||
|
.st7{fill:url(#SVGID_00000119810811088729404010000001339012453506459048_);}
|
||||||
|
.st8{fill:url(#SVGID_00000056391310230795810690000008029868739465234354_);}
|
||||||
|
.st9{fill:#010202;}
|
||||||
|
</style>
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="51.0692" y1="421.4218" x2="245.8004" y2="138.7009">
|
||||||
|
<stop offset="0" style="stop-color:#90D1E1"/>
|
||||||
|
<stop offset="0.9921" style="stop-color:#F195BE"/>
|
||||||
|
</linearGradient>
|
||||||
|
<circle class="st0" cx="211.1" cy="189.1" r="179.2"/>
|
||||||
|
<linearGradient id="SVGID_00000181791594591671767050000008664012113219957652_" gradientUnits="userSpaceOnUse" x1="240.147" y1="117.7065" x2="241.3703" y2="117.7065">
|
||||||
|
<stop offset="6.070287e-03" style="stop-color:#EAB593"/>
|
||||||
|
<stop offset="0.9975" style="stop-color:#F4C278"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_00000181791594591671767050000008664012113219957652_);" d="M240.1,118
|
||||||
|
C241.1,117.6,242.4,117,240.1,118L240.1,118z"/>
|
||||||
|
<linearGradient id="SVGID_00000058550945621228767580000005935124031691961279_" gradientUnits="userSpaceOnUse" x1="94.8494" y1="142.6618" x2="97.0305" y2="142.6618">
|
||||||
|
<stop offset="6.070287e-03" style="stop-color:#EAB593"/>
|
||||||
|
<stop offset="0.9975" style="stop-color:#F4C278"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_00000058550945621228767580000005935124031691961279_);" d="M97,142.9c-3.2-0.6-2.2-0.3-1.3-0.2
|
||||||
|
c0.4,0,0.7,0.1,1.1,0.2C96.9,142.8,97,142.8,97,142.9z"/>
|
||||||
|
<linearGradient id="SVGID_00000137132525261985581510000000877506361518317499_" gradientUnits="userSpaceOnUse" x1="178.9468" y1="243.9432" x2="180.17" y2="243.9432">
|
||||||
|
<stop offset="6.070287e-03" style="stop-color:#EAB593"/>
|
||||||
|
<stop offset="0.9975" style="stop-color:#F4C278"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_00000137132525261985581510000000877506361518317499_);" d="M178.9,244.2
|
||||||
|
C179.9,243.8,181.2,243.3,178.9,244.2L178.9,244.2z"/>
|
||||||
|
<linearGradient id="SVGID_00000147899999161542128250000012606861005779996338_" gradientUnits="userSpaceOnUse" x1="147.0169" y1="218.2912" x2="147.8155" y2="218.2912">
|
||||||
|
<stop offset="6.070287e-03" style="stop-color:#EAB593"/>
|
||||||
|
<stop offset="0.9975" style="stop-color:#F4C278"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_00000147899999161542128250000012606861005779996338_);" d="M147.8,218.4c-1.2-0.2-0.8-0.1-0.5-0.1
|
||||||
|
C147.5,218.3,147.6,218.3,147.8,218.4C147.8,218.4,147.8,218.4,147.8,218.4z"/>
|
||||||
|
<path class="st5" d="M136.2,205.2l0.6,2.3l-6.8,1.6l0.8,3.3l-1.9,0.5l-1.3-5.6L136.2,205.2z"/>
|
||||||
|
<path class="st5" d="M134.6,216.1c2.7-1.1,4.9-0.1,5.8,2.1c0.9,2.3,0,4.5-2.7,5.6c-2.7,1.1-5,0.2-5.9-2.1
|
||||||
|
C130.9,219.4,131.9,217.2,134.6,216.1z M136.8,221.5c1.5-0.6,2.1-1.6,1.7-2.6c-0.4-1-1.5-1.3-3-0.7c-1.5,0.6-2.2,1.6-1.8,2.6
|
||||||
|
C134.1,221.9,135.3,222.1,136.8,221.5z"/>
|
||||||
|
<radialGradient id="SVGID_00000148647616164358038390000000096934996311696560_" cx="211.1145" cy="189.2326" r="154.4542" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="3.599497e-04" style="stop-color:#FFFFFF"/>
|
||||||
|
<stop offset="0.9083" style="stop-color:#FFFFFF;stop-opacity:9.169579e-02"/>
|
||||||
|
<stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<circle style="fill:url(#SVGID_00000148647616164358038390000000096934996311696560_);" cx="211.1" cy="189.2" r="154.5"/>
|
||||||
|
<linearGradient id="SVGID_00000181795849749029799540000017664282943098263974_" gradientUnits="userSpaceOnUse" x1="201.3407" y1="294.9478" x2="220.3445" y2="87.1506">
|
||||||
|
<stop offset="0" style="stop-color:#5760A9"/>
|
||||||
|
<stop offset="0.5157" style="stop-color:#ADB3D9"/>
|
||||||
|
<stop offset="0.6156" style="stop-color:#BEC4E2"/>
|
||||||
|
<stop offset="0.7199" style="stop-color:#BBC1DF"/>
|
||||||
|
<stop offset="0.8218" style="stop-color:#B0B7D7"/>
|
||||||
|
<stop offset="0.9223" style="stop-color:#9FA6CA"/>
|
||||||
|
<stop offset="1" style="stop-color:#8D95BD"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_00000181795849749029799540000017664282943098263974_);" d="M397.7,131.5L314,95.7L85.6,142.5l-2.7-4.6
|
||||||
|
l-4.4,0.8l-2-4.5c0,0-0.8-2.1,2.2-1.8c3,0.3,4.7,0.2,10.6-3.7c5.9-3.9,2.3-2.2,2.8-5.5c0.5-3.3-5.2-6.1-11.5-10.7
|
||||||
|
c-6.3-4.6-12.4-5.9-14.1-2.7c-4.9,11,1.7,17.2,1.7,17.2l3.9,3.3l-0.6,2.1l2.7,7.4l-4,0.7l-1,5.4l-46.2,9.5l1.3,19.3
|
||||||
|
c-0.3-0.6,0.8,1.9-0.8,2c-3.1,0.1-5.7,0.5-8.1,1.6c-5.4,2.4-9.5,7-11.2,12.6C2.4,196.4,5,203.4,10,207c3.3,2.4,5.2,4.1,8.9,3.4
|
||||||
|
c0.8,0.2,1.2,0,2,0c1,0,3-0.1,3.8-0.5c1,0.1,1.3,1,1.4-0.3c0-0.1,1.6,0.1,1.6,0c0.1-0.1-0.3,1.8-0.1,1.7l-0.5,3.8
|
||||||
|
c-5.1,0.8-10.1,2.8-13.7,6.6c-4,4.3-5.4,9.6-4.9,15.3c0.4,5.2,1.3,9.2,6.7,11.3c2.9,1.1,6.6,2.3,9.5,1.5c1.6-0.4,4.5-0.2,6.1-0.6
|
||||||
|
l1.2,9l44.4,25.4l276.3-52.7l67.8-67.9L397.7,131.5z M26.2,202.7c0-0.2,0-0.4,0-0.6L26.2,202.7C26.2,202.7,26.2,202.7,26.2,202.7z
|
||||||
|
M28,229.2l0.1,0.9c-0.2-0.2-0.3-0.3-0.5-0.4C27.7,229.5,27.9,229.4,28,229.2z M27.2,233.9c0,0-0.1,0-0.1,0c0.4-0.4,0.7-0.8,1.1-1.1
|
||||||
|
c0.1-0.1,0.1-0.1,0.1-0.2l0.1,1.1C28,233.6,27.6,233.7,27.2,233.9z M28.8,244.6c0.1-0.1,0.1-0.2,0.2-0.3c0-0.1,0.1-0.3,0.1-0.4
|
||||||
|
l0,0.7C29,244.5,28.9,244.6,28.8,244.6z"/>
|
||||||
|
<linearGradient id="SVGID_00000152227878615532122350000012236979203658928007_" gradientUnits="userSpaceOnUse" x1="318.3721" y1="135.4384" x2="340.2567" y2="95.2667" gradientTransform="matrix(0.921 -3.872472e-02 0 1 31.6061 15.4856)">
|
||||||
|
<stop offset="0" style="stop-color:#B263A4"/>
|
||||||
|
<stop offset="2.387382e-02" style="stop-color:#B465A4"/>
|
||||||
|
<stop offset="0.7022" style="stop-color:#E5A177"/>
|
||||||
|
<stop offset="0.9988" style="stop-color:#FAB84D"/>
|
||||||
|
</linearGradient>
|
||||||
|
<polygon style="fill:url(#SVGID_00000152227878615532122350000012236979203658928007_);" points="312.1,97 315.4,129.8 367,120.3
|
||||||
|
"/>
|
||||||
|
<g>
|
||||||
|
<path class="st9" d="M422.2,162.2c-0.3-1.1-1.1-1.9-1.7-2.8c-0.8-1-1.6-2-2.3-3.1c-2.6-3.4-4.4-6.6-7-10c-1.6-2.1-3-4.2-4.6-6.4
|
||||||
|
c-1.3-1.9-4.9-6.9-7.8-9.6c-1.6-1.4-11-5.7-19.1-9.2C352.7,54.5,287.3,7.3,211.1,7.3c-87.4,0-160.5,62-177.8,144.4c0,0-0.1,0-0.1,0
|
||||||
|
c-1.2,0.3-2.4,0.5-3.6,0.8c-0.9,0.2-1.9,0.4-2.8,0.6c-0.7,0.1-1.4,0.3-2,0.4c-0.4,0.1-0.8,0.2-1.2,0.2c-0.1,0-0.2,0-0.3,0.1
|
||||||
|
l-1.7,0.2l0.2,1.8l1.5,18.7c-0.7,0.1-1.5,0.3-2.2,0.4c-0.9,0.2-1.8,0.4-2.7,0.5c-4.8,0.8-9,3.5-11.9,7.6c-2.9,4.1-4.2,9-3.5,13.9
|
||||||
|
c0.6,4.9,3.1,9.2,6.9,12.1c3.2,2.5,7.1,3.7,11.1,3.5c0.7,0,1.5-0.1,2.2-0.2l1.7-0.3c0.5-0.1,0.9-0.2,1.4-0.3l0.2,2.4
|
||||||
|
c-0.6,0.1-1.1,0.2-1.7,0.3c-0.9,0.2-1.8,0.4-2.7,0.5c-4.8,0.8-9,3.5-11.9,7.6c-2.9,4.1-4.2,9-3.5,13.9c1.2,9.4,9,16,18,15.6
|
||||||
|
c0.7,0,1.4-0.1,2.2-0.2l1.7-0.3c0.3-0.1,0.6-0.1,0.9-0.2l0.5,6.4l0.2,1.1l0.8,0.4l17.6,10.1c29.7,59.9,91.5,101.3,162.9,101.3
|
||||||
|
c98.8,0,179.5-79.3,181.6-177.7c1.3-1.3,2.7-2.6,4-4c2.7-2.6,5.4-5.3,8.1-7.9c2.3-2.3,4.7-4.6,7-6.9c2.1-2.1,4.2-4.2,6.3-6.2
|
||||||
|
c1-1,2-1.9,3-2.9C421.9,164.3,422.5,163.3,422.2,162.2z M211.1,12.3c72.3,0,134.6,43.7,161.9,106.1c-7-3-14.1-5.9-21.1-8.9
|
||||||
|
c-8.4-3.6-16.9-7.1-25.3-10.7c-2.5-1.1-5-2.1-7.5-3.2c-1.3-0.5-2.5-1.1-3.8-1.6c-1.1-0.5-2.4-1.2-3.6-1c-0.2,0-0.3,0.1-0.5,0.1
|
||||||
|
c-0.5,0.1-1.1,0.2-1.6,0.3c-0.8,0.2-1.6,0.3-2.4,0.5c-1.1,0.2-2.2,0.5-3.2,0.7c-1.3,0.3-2.7,0.6-4,0.8c-1.6,0.3-3.1,0.7-4.7,1
|
||||||
|
c-1.8,0.4-3.6,0.7-5.3,1.1c-2,0.4-4,0.8-6,1.3c-2.2,0.5-4.4,0.9-6.5,1.4c-2.4,0.5-4.7,1-7.1,1.5c-2.5,0.5-5,1.1-7.5,1.6
|
||||||
|
c-2.7,0.6-5.3,1.1-8,1.7c-2.8,0.6-5.6,1.2-8.4,1.8c-2.9,0.6-5.8,1.2-8.7,1.8c-3,0.6-6,1.3-9.1,1.9c-3.1,0.7-6.2,1.3-9.3,2
|
||||||
|
c-3.2,0.7-6.4,1.3-9.6,2c-3.2,0.7-6.5,1.4-9.7,2.1c-3.3,0.7-6.6,1.4-9.9,2.1c-3.3,0.7-6.7,1.4-10,2.1c-3.3,0.7-6.7,1.4-10,2.1
|
||||||
|
c-3.3,0.7-6.7,1.4-10,2.1c-3.3,0.7-6.7,1.4-10,2.1c-3.3,0.7-6.6,1.4-9.9,2.1c-3.3,0.7-6.5,1.4-9.8,2.1c-3.2,0.7-6.4,1.4-9.7,2
|
||||||
|
c-3.1,0.7-6.3,1.3-9.4,2c-3.1,0.6-6.1,1.3-9.2,1.9c-3,0.6-5.9,1.3-8.9,1.9c-2.4,0.5-4.8,1-7.2,1.5c-0.7-1.2-1.5-2.3-2.5-3.2
|
||||||
|
c-0.2-0.2-0.5-0.2-0.8-0.2c-0.3-0.3-0.7-0.3-1.1-0.2c-0.9,0.2-1.7,0.4-2.6,0.5c-0.7-1.4-1.3-2.9-1.8-4.4c6.3,1,12.7-1.9,16.6-7.4
|
||||||
|
c0.3-0.4,0.2-0.8-0.1-1c0-1.9-1.7-4.2-3.6-6.1c0.6-1.1,1.3-2.1,1.8-3.2c0.7-0.1,1.3-0.5,1.6-1.1c0.6-1.2-0.1-2.7-1.4-3.1
|
||||||
|
c-1.3-0.4-3.1,1.1-2.3,2.5c0,0.3,0.1,0.5,0.2,0.8c-0.6,0.9-1.1,1.7-1.6,2.6c-1.4-1.2-2.7-2.2-3.4-2.8c-3.1-2.2-13.4-9.7-18-6.5
|
||||||
|
c-0.3,0-0.6,0.1-0.7,0.4c-0.1,0.2-0.2,0.4-0.3,0.6c-0.1,0.1-0.1,0.2-0.2,0.4c-3.8,7.4-2.1,16.7,5.2,21.5c0.1,0,0.2,0.1,0.2,0.1
|
||||||
|
c-0.2,0.3-0.4,0.7-0.5,1.1c-0.2,0.2-0.2,0.5-0.1,0.8c0,0.1,0,0.1,0.1,0.2c0,0.6,0.3,1.2,0.6,1.7c0.5,1.5,1.1,3,1.6,4.4
|
||||||
|
c-0.9,0.2-1.8,0.4-2.7,0.7c-0.4,0-0.8,0.2-1,0.6c-0.5,1.4-1.1,2.7-1.4,4.2c-2.2,0.5-4.4,0.9-6.5,1.4c-2.3,0.5-4.5,1-6.8,1.4
|
||||||
|
c-2.1,0.4-4.2,0.9-6.2,1.3c-1.9,0.4-3.8,0.8-5.7,1.2c-1.3,0.3-2.6,0.5-3.9,0.8C56.3,71.6,126.9,12.3,211.1,12.3z M312.1,97
|
||||||
|
L312.1,97l10.6,4.4c-0.1,0.1-0.2,0.3-0.3,0.4c-0.1,0.2-0.3,0.4-0.5,0.6c0,0-0.2,0.2-0.3,0.3c-0.1,0.1-0.3,0.3-0.3,0.4
|
||||||
|
c-0.2,0.2-0.3,0.4-0.5,0.6c-0.3,0.4-0.7,0.8-1.1,1.1c-0.7,0.7-1.4,1.4-2.2,2c0,0-0.4,0.3-0.4,0.3c-0.1,0.1-0.3,0.2-0.4,0.3
|
||||||
|
c-0.4,0.3-0.9,0.6-1.3,0.9c-0.6,0.4-1.3,0.8-2,1.1L312.1,97z M327.3,115.9c3.5-1.8,8.4-4.4,11.4-7.8l6.6,2.8c0,0.1,0,0.2,0,0.3
|
||||||
|
c0,0.3,0,0,0,0.4c-0.1,0.3-0.1,0.7-0.2,1c-0.3,1.2-0.6,1.8-1.3,2.9c-1.2,2-3.1,3.9-4.9,5.2c-3.3,2.3-6.8,4.2-10.3,6.3l-12.7,2.6
|
||||||
|
l-0.4-3.8C317.4,121.1,323.1,118.1,327.3,115.9z M359.2,116.8l6.4,2.7l-7.2,1.5C358.8,119.6,359.1,118.2,359.2,116.8z M70.8,141.6
|
||||||
|
c3.9-0.5,7.7-1.3,11.5-2.2c0.1,0,0.1-0.1,0.2-0.1c0.3,0.7,0.6,1.3,1,1.9c-2.4,0.5-4.7,1-7.1,1.5c-2.2,0.5-4.3,0.9-6.5,1.4
|
||||||
|
C70.2,143.2,70.5,142.4,70.8,141.6z M66.3,112.9c0.7,1.2,1.7,2.4,2.8,3.5c-1.3,2-1.6,4.7-1.3,7.1c0.1,0.5,0.2,1.2,0.4,1.9
|
||||||
|
C65.6,121.8,65.1,117.2,66.3,112.9z M84,115.9c0.6,0.5,1.9,1.7,3.3,3c-0.3,0.5-0.5,0.9-0.8,1.4c-0.8,1.3-1.7,2.7-2.2,4.1
|
||||||
|
c-1.1-0.5-2.2-1-3.3-1.6c-0.2-0.5-0.7-0.8-1.1-0.7c-1.6-1-3.2-2-4.6-3.1c0,0-0.1-0.1-0.1-0.1c0.1-0.9,0-1.8,0.1-2.7
|
||||||
|
c0.3-1.6,1.2-2.8,2-4.1c0.1-0.2,0.1-0.4,0.1-0.6C79.6,112.8,81.9,114.4,84,115.9z M76.3,131c0-0.1-0.1-0.1-0.1-0.1
|
||||||
|
c0.9-0.5,1.6-1.8,2.2-2.6c0.7-1.1,1.4-2.3,2-3.5c2,1.1,4.1,2.2,6.1,2.7c-0.9,0.9-1.8,1.6-2.9,2.2c-1.3,0.7-2.7,0.9-3.9,1.7
|
||||||
|
C78.6,131.4,77.4,131.3,76.3,131z M88.6,127.9c0.2,0,0.3,0,0.5,0c-0.5,0.4-1.1,0.8-1.6,1.1C87.9,128.7,88.3,128.3,88.6,127.9z
|
||||||
|
M87.8,125.5c-0.6-0.1-1.3-0.3-1.9-0.5c0.9-1.1,1.6-2.5,2.3-3.7c0.2-0.3,0.3-0.5,0.5-0.8C91.3,123.5,92.8,126.5,87.8,125.5z
|
||||||
|
M76,111.3c-1.3,1.1-2,3-2.3,4.6c-0.1,0.5-0.2,1.1-0.2,1.8c-3.2-3-8.8-9.1-3-8.3c2,0.3,3.9,0.9,5.8,1.8
|
||||||
|
C76.2,111.2,76.1,111.2,76,111.3z M69.7,123.8c-0.4-2.2,0.1-4.1,0.7-6.2c1.6,1.4,3.1,2.5,3.8,3.1c0.9,0.7,2.6,1.9,4.5,3.1
|
||||||
|
c-0.5,1.2-1,2.3-1.7,3.4c-0.5,0.9-1.5,1.9-1.8,2.9c-0.6-0.8-1.7-0.9-2.6-0.6c-0.3-0.2-0.6-0.3-0.8-0.5c-0.3-0.2-0.5-0.4-0.7-0.6
|
||||||
|
C71.3,126.9,70,125.3,69.7,123.8z M74.3,132.4c0.1,0.2,0.1,0.4,0,0.6c-0.2,0.5-0.9,0.5-1.3,0.2c-0.5-0.3-0.5-1-0.2-1.5
|
||||||
|
C73.3,132,73.8,132.2,74.3,132.4z M73.7,135.7c0.8,0,1.5-0.3,2.1-0.8c0.4,1,0.8,2,1.3,3c-0.8,0.2-1.6,0.4-2.4,0.6
|
||||||
|
C74.4,137.5,74,136.6,73.7,135.7z M91.7,113.1C91.7,113.1,91.7,113.1,91.7,113.1c0.1,0.1,0.2,0.2,0.1,0.3c0,0,0,0-0.1,0
|
||||||
|
c-0.1,0-0.1-0.1-0.2-0.1C91.5,113.2,91.6,113.1,91.7,113.1z M12.3,206.2c0.4-0.1,0.7-0.3,1.1-0.4c0.5-0.2,1-0.6,1.5-0.8
|
||||||
|
c0.4-0.2,0.1-0.7-0.3-0.7c-0.6,0.1-1.1,0.1-1.7,0.2c-0.5,0.1-1.1,0.2-1.6,0.4c-0.1,0-0.3,0.1-0.4,0.1c-1.2-1.2-2.2-2.5-2.9-4
|
||||||
|
c0.3-0.1,0.5-0.2,0.8-0.3c0.5-0.2,1.1-0.4,1.6-0.7c0.5-0.2,1-0.6,1.5-0.8c0.4-0.2,0.1-0.7-0.3-0.7c-0.6,0.1-1.1,0.1-1.7,0.2
|
||||||
|
c-0.5,0.1-1.1,0.2-1.6,0.4c-0.3,0.1-0.6,0.2-0.9,0.3c-0.3-1-0.6-2-0.7-3c-0.1-0.7-0.1-1.4-0.1-2.2c0.4-0.2,0.9-0.3,1.3-0.5
|
||||||
|
c0.5-0.2,1.1-0.4,1.6-0.7c0.5-0.2,1-0.6,1.5-0.8c0.4-0.2,0.1-0.7-0.3-0.7c-0.6,0.1-1.1,0.1-1.7,0.2c-0.5,0.1-1.1,0.2-1.6,0.4
|
||||||
|
c-0.2,0.1-0.4,0.1-0.7,0.2c0.2-1.5,0.6-3.1,1.3-4.5c0.2,0,0.3,0,0.5,0c0.3-0.1,0.5-0.1,0.8-0.2c0.5-0.1,1-0.3,1.4-0.4
|
||||||
|
c0.5-0.1,1-0.3,1.4-0.4c0.5-0.2,1-0.4,1.5-0.6c0.4-0.1,0.4-0.8-0.1-0.8c-0.3,0-0.5,0-0.8,0c-0.3,0-0.5,0-0.8,0
|
||||||
|
c-0.5,0.1-1.1,0.1-1.6,0.3c-0.4,0.1-0.9,0.2-1.3,0.3c0.2-0.2,0.3-0.5,0.5-0.7c2.1-2.9,5-4.9,8.2-5.7c-0.8,1.2-1.5,2.4-2.1,3.7
|
||||||
|
c-1.4,3.2-2.1,6.7-2,10.1c0.1,3.3,1.1,6.6,2.7,9.5c0.8,1.6,1.9,3,3,4.4c0.3,0.4,0.7,0.9,1.1,1.4C17.4,208.8,14.6,207.9,12.3,206.2z
|
||||||
|
M24.3,208.3l-0.7,0.1c-0.6-0.9-1.6-1.6-2.2-2.4c-1-1.2-1.9-2.5-2.7-3.9c-1.5-2.7-2.4-5.7-2.6-8.7c-0.1-3.2,0.5-6.3,1.8-9.2
|
||||||
|
c0.7-1.5,1.5-2.9,2.4-4.3c0.2-0.3,0.5-0.7,0.7-1c0.2,0,0.4-0.1,0.6-0.1c0.7-0.1,1.3-0.2,1.9-0.4l2.3,29.6
|
||||||
|
C25.3,208.1,24.8,208.2,24.3,208.3z M15.9,245.7c0.5-0.2,1-0.4,1.4-0.6c0.5-0.2,1-0.6,1.5-0.8c0.4-0.2,0.1-0.7-0.3-0.7
|
||||||
|
c-0.6,0.1-1.1,0.1-1.7,0.2c-0.5,0.1-1.1,0.2-1.6,0.4c-0.3,0.1-0.6,0.2-0.9,0.3c-1.2-1.2-2.2-2.6-3-4.2c0.4-0.2,0.9-0.3,1.3-0.5
|
||||||
|
c0.5-0.2,1.1-0.4,1.6-0.7c0.5-0.2,1-0.6,1.5-0.8c0.4-0.2,0.1-0.7-0.3-0.7c-0.6,0.1-1.1,0.1-1.7,0.2c-0.5,0.1-1.1,0.2-1.6,0.4
|
||||||
|
c-0.5,0.1-0.9,0.3-1.4,0.4c-0.3-0.9-0.5-1.8-0.6-2.8c-0.1-1.1-0.2-2.1-0.1-3.2c0.1,0,0.2-0.1,0.3-0.1c0.5-0.2,1.1-0.3,1.6-0.5
|
||||||
|
c0.5-0.2,1.1-0.4,1.6-0.6c0.5-0.2,0.9-0.6,1.4-0.8c0.3-0.2,0.2-0.7-0.2-0.7c-0.5,0.1-1.1,0-1.6,0.1c-0.6,0.1-1.2,0.2-1.7,0.3
|
||||||
|
c-0.4,0.1-0.8,0.2-1.1,0.3c0.4-1.9,1.1-3.6,2.2-5.3c0.1,0,0.1,0,0.2,0c0.3-0.1,0.5-0.1,0.8-0.2c0.5-0.1,1-0.3,1.4-0.4
|
||||||
|
c0.5-0.1,1-0.3,1.4-0.4c0.5-0.2,1-0.4,1.5-0.6c0.4-0.1,0.4-0.8-0.1-0.8c-0.3,0-0.5,0-0.8,0c-0.3,0-0.5,0-0.8,0
|
||||||
|
c-0.5,0.1-1.1,0.1-1.6,0.3c-0.1,0-0.3,0.1-0.4,0.1c2.2-2.4,5.1-4,8.3-4.6c0,0,0,0,0.1,0c-0.4,0.5-0.7,1.1-1,1.6
|
||||||
|
c-0.8,1.4-1.5,2.9-2.1,4.5c-1.1,3.2-1.4,6.7-0.9,10c0.4,3.2,1.5,6.2,3.2,8.9c0.8,1.3,1.8,2.6,2.9,3.8c0.2,0.2,0.4,0.4,0.6,0.6
|
||||||
|
C21.7,248.6,18.5,247.6,15.9,245.7z M28.4,247.7c-0.7-0.6-1.4-1.1-2-1.7c-1-1-1.8-2.1-2.5-3.2c-1.6-2.5-2.6-5.3-3-8.2
|
||||||
|
c-0.4-3-0.1-6,0.8-8.9c0.4-1.5,1-2.8,1.7-4.2c0.6-1,1.5-2.1,1.8-3.3c0,0,0,0,0,0c0.5-0.1,0.9-0.2,1.4-0.3l2.3,29.6
|
||||||
|
C28.7,247.7,28.6,247.7,28.4,247.7z M25.7,157.2l282.7-59.5c0.5,4,0.9,7.9,1.4,11.9c0.7,6.2,1.4,12.4,2.1,18.6c0.1,1,0.3,6,2,5.6
|
||||||
|
c0.5-0.1,1.1-0.2,1.6-0.3c1.7-0.4,3.5-0.7,5.2-1.1c5.3-1.1,10.7-2.2,16-3.3c6-1.2,12-2.4,18-3.7c4.6-0.9,9.1-1.9,13.7-2.8
|
||||||
|
c1.2-0.2,2.4-0.5,3.6-0.7l23.3,10.4L325,198.2L33.4,255.5L25.7,157.2z M325.2,202.1l23.2,27.2L76.3,281.1l-39.2-22.5L325.2,202.1z
|
||||||
|
M211.1,365.8c-66.6,0-124.7-37.1-154.8-91.6L75,284.8l0.7,0.2h0.6c0.7-0.1,1.3-0.3,2-0.4c1.9-0.4,3.8-0.7,5.7-1.1
|
||||||
|
c3-0.6,6-1.2,9.1-1.7c4-0.8,8-1.5,12-2.3c4.8-0.9,9.6-1.8,14.5-2.8c5.5-1.1,11-2.1,16.5-3.2c6.1-1.2,12.1-2.3,18.2-3.5
|
||||||
|
c6.5-1.2,13-2.5,19.4-3.7c6.8-1.3,13.5-2.6,20.3-3.9c6.9-1.3,13.8-2.6,20.7-3.9c6.9-1.3,13.8-2.6,20.7-3.9
|
||||||
|
c6.7-1.3,13.5-2.6,20.2-3.9c6.5-1.2,12.9-2.5,19.4-3.7c6-1.2,12.1-2.3,18.1-3.5c5.5-1,11-2.1,16.5-3.1c4.8-0.9,9.6-1.8,14.4-2.7
|
||||||
|
c4-0.8,7.9-1.5,11.9-2.3c3-0.6,6-1.1,9-1.7c1.9-0.4,3.8-0.7,5.6-1.1c1.2-0.2,2.3-0.3,3.2-1.2c1.6-1.6,3.2-3.2,4.8-4.8
|
||||||
|
c1.9-1.9,3.8-3.8,5.7-5.7c2.4-2.4,4.8-4.7,7.2-7.1c2.7-2.7,5.4-5.3,8.1-8c2.6-2.6,5.3-5.2,7.9-7.8
|
||||||
|
C382.7,291.4,305.5,365.8,211.1,365.8z M352.1,227.7l-23.5-27.6l68.9-64.5l20.6,27.1L352.1,227.7z"/>
|
||||||
|
<path class="st9" d="M180.7,145.3l6.5,65.1l27.8-4.7l-5.8-65.5L180.7,145.3z M188.9,208.2l-6.1-61.3l24.7-4.4l5.5,61.6L188.9,208.2
|
||||||
|
z"/>
|
||||||
|
<path class="st9" d="M150.8,168.3l4.4,48.5l20.4-4.2l-4.6-48.2L150.8,168.3z M156.9,214.5l-4.1-44.7l16.4-3.1l4.1,44.4L156.9,214.5
|
||||||
|
z"/>
|
||||||
|
<path class="st9" d="M120.2,199.8l2.5,24l28.7-6.4l-1.9-24.7L120.2,199.8z M124.5,221.4l-2.1-20.2l25.4-6.1l1.6,20.8L124.5,221.4z"
|
||||||
|
/>
|
||||||
|
<path class="st9" d="M40.5,215.3l2.9,27.8l22.4-3.3l-2.5-28.5L40.5,215.3z M45.2,240.9l-2.5-24l19-3.3l2.2,24.6L45.2,240.9z"/>
|
||||||
|
<path class="st9" d="M308.3,222.2l-10.7-7.4l-25.9,4.3l10.7,7.4L308.3,222.2z M297.3,216.3l7.3,5l-21.8,3.7l-7.3-5L297.3,216.3z"/>
|
||||||
|
<path class="st9" d="M295.2,236.7l25.9-4.3l-10.7-7.4l-25.9,4.3L295.2,236.7z M317.4,231.5l-21.8,3.7l-7.3-5l21.8-3.7L317.4,231.5z
|
||||||
|
"/>
|
||||||
|
<path class="st9" d="M88.7,275.9l25.9-4.3l-10.7-7.4l-25.9,4.3L88.7,275.9z M110.8,270.8L89,274.4l-7.3-5l21.8-3.7L110.8,270.8z"/>
|
||||||
|
<path class="st9" d="M91.3,254.3l-25.9,4.3l10.7,7.4l25.9-4.3L91.3,254.3z M69.1,259.4l21.8-3.7l7.3,5l-21.8,3.7L69.1,259.4z"/>
|
||||||
|
<path class="st9" d="M245.4,175l-2.8-36c-0.2-2-1.9-3.5-3.9-3.3c-1,0.1-1.9,0.5-2.5,1.3c-0.6,0.7-0.9,1.7-0.9,2.6l2.9,36.7
|
||||||
|
l-1.9,0.3l-2.8-35.8c-0.1-1-0.5-1.9-1.3-2.5c-0.7-0.6-1.7-0.9-2.6-0.9c-1,0.1-1.8,0.5-2.5,1.3c-0.6,0.7-0.9,1.7-0.9,2.6l2.9,36.5
|
||||||
|
l-1.9,0.3l-2.7-35.1c-0.2-2-1.9-3.5-3.9-3.3c-1,0.1-1.9,0.5-2.5,1.3c-0.6,0.7-0.9,1.7-0.9,2.6l2.8,35.7l-1.5,0.3l2.5,24.2l28.1-4.3
|
||||||
|
l-1.9-24.8L245.4,175z M237.2,139.4c0-0.5,0.1-0.9,0.4-1.2c0.3-0.3,0.7-0.6,1.2-0.6c0,0,0.1,0,0.1,0c0.9,0,1.6,0.7,1.7,1.6
|
||||||
|
l2.8,36.2l-3.4,0.6L237.2,139.4z M228.2,141.1c0-0.5,0.1-0.9,0.4-1.2c0.3-0.3,0.7-0.6,1.2-0.6h0c0,0,0.1,0,0.1,0
|
||||||
|
c0.4,0,0.8,0.1,1.1,0.4c0.3,0.3,0.6,0.7,0.6,1.2l2.8,36l-3.4,0.6L228.2,141.1z M219.1,143.4c0-0.5,0.1-0.9,0.4-1.2
|
||||||
|
c0.3-0.3,0.7-0.6,1.2-0.6c0,0,0.1,0,0.1,0c0.9,0,1.6,0.7,1.7,1.6l2.8,35.3l-3.4,0.6L219.1,143.4z M222.6,201.5l-2.1-20.4l24.8-4.2
|
||||||
|
l1.6,20.9L222.6,201.5z"/>
|
||||||
|
<path class="st9" d="M99.7,179c0.2-2.1-2.5-3-4.1-2.1c-1.1,0.7-1.8,2.1-1.5,3.3c-1.6-0.8-3.5,0-5.3,0.3c-2.6,0.4-5.2,0.9-7.8,1.5
|
||||||
|
c0.1-2.5-0.1-5-0.3-7.4c2.6-0.2,5.3-0.6,7.9-1.1c0.2,0,0.3-0.1,0.4-0.2c1.1,1.3,3.5,1.5,4.5,0c0.4-0.6,0.6-1.6,0.4-2.5
|
||||||
|
c0,0,0,0,0-0.1c0.2-2.1-2.5-3-4.1-2.1c-1,0.6-1.6,1.9-1.6,3c-0.1,0-0.2,0-0.2,0c-2.5,0.4-5,0.8-7.5,1.4c0-0.2,0-0.4-0.1-0.6
|
||||||
|
c-0.2-2.4-0.3-5-1.2-7.2c2.7-0.3,5.3-0.6,8-1c1.9-0.3,4.5-0.1,5.8-1.8c0.1,0.4,0.2,0.7,0.4,1c1.1,1.4,3.6,1.7,4.6,0.2
|
||||||
|
c0.4-0.6,0.6-1.6,0.4-2.5c0,0,0,0,0-0.1c0.2-2.1-2.5-3-4.1-2.1c-1.1,0.6-1.7,2-1.5,3.2c-1.6-1.2-3.8-0.2-5.7,0.1
|
||||||
|
c-2.9,0.5-5.7,1-8.5,1.6c-0.7,0.2-0.6,1.2,0,1.3c-0.2,0.7-0.3,1.4-0.3,2.1c-1.8-0.4-3.7,0.3-5.6,0.7c-3.4,0.6-6.9,1.2-10.3,1.9
|
||||||
|
c-0.7,0.1-0.6,1.2,0,1.3c-0.6,4.3,0.1,8.7,0.4,12.9c0.3,4.4,0.6,8.7,1.4,13c-1.7-0.4-3.6-0.2-5.4,0.2c0.1-1.4-0.1-2.7-0.3-4.1
|
||||||
|
c-0.1-1-0.1-2.1-0.4-3.1c0.5-0.1,1-0.2,1.5-0.2c0.4-0.1,0.7-0.5,0.7-0.9c-0.2-2.5-0.4-5.1-0.6-7.6c0-0.5-0.4-1-0.9-0.9
|
||||||
|
c-5.9,0.7-10.8-2.7-11.6-8.7c0-0.1,0-0.1,0-0.1c0,0,0,0,0,0c0-0.1-0.1-0.2-0.1-0.3c0-0.1-0.1-0.1-0.1-0.2c0,0,0,0-0.1-0.1
|
||||||
|
c-0.2-0.2-0.5-0.3-0.8-0.2c-2.5,0.4-5.1,0.8-7.6,1.2c-0.4,0.1-0.7,0.5-0.7,0.9c0.5,6.4,1.1,12.7,1.6,19.1c0.1,0.7,0.6,1,1.2,0.9
|
||||||
|
c0.4-0.1,0.8-0.1,1.1-0.2c-0.1,0.9,0,1.8,0.1,2.7c0.1,1.8,0.2,3.7,0.8,5.4c0,0.4,0.2,0.9,0.6,0.8c3.7-0.5,7.4-1,11.1-1.5
|
||||||
|
c3.2-0.5,7.3-0.5,10.1-2.2c0.1,0.5,0.9,0.4,0.9-0.1c0.1-4.9-0.4-9.7-0.8-14.6c-0.3-4-0.4-8.1-1.6-11.9c3-0.3,6-0.7,9-1.2
|
||||||
|
c2-0.3,4.5-0.3,6.4-1.2c0.1,1.7,0.3,3.4,0.4,5c0.2,2.9,0.4,5.7,1,8.5c-0.2,0.4,0,1.1,0.6,1.1c2.8-0.3,5.5-0.7,8.3-1.1
|
||||||
|
c1.8-0.3,4.2-0.1,5.5-1.5c0.1,0.2,0.1,0.3,0.2,0.4c1.1,1.4,3.6,1.7,4.6,0.2C99.7,180.9,99.9,179.9,99.7,179
|
||||||
|
C99.7,179,99.7,179,99.7,179z M90.9,170.5c0.7-0.5,1.2,0.1,1.5,0.7c0,0.3,0,0.5-0.2,0.8c-0.3,0.6-1.2,0.6-1.6,0.1
|
||||||
|
C90.1,171.7,90.4,170.8,90.9,170.5z M95.5,160.8c0.7-0.5,1.2,0.1,1.5,0.7c0,0.3,0,0.5-0.2,0.8c-0.3,0.6-1.2,0.6-1.6,0.1
|
||||||
|
C94.8,162,95,161.2,95.5,160.8z M40.1,174.1c0-0.1,0-0.1,0-0.2c1.8,0.1,4-0.5,5.9-1c1.3,6,6.1,9.8,12.3,9.5c0,1.9-0.1,4.2,0.5,5.8
|
||||||
|
c-0.1,0-0.1,0-0.2,0c-1-0.1-2.6,0.4-3.6,0.6c-4.5,0.7-9,1.5-13.4,2.2c-0.4-4.5-0.7-9-1.1-13.5C40.3,176.6,40.5,175.1,40.1,174.1z
|
||||||
|
M49.6,191.6c-0.1,0.9,0,2,0.1,2.9c0.1,1.6,0.1,3.1,0.5,4.6c-2,0.4-4.1,0.7-6.1,1.1c0.1-1.6-0.1-3.1-0.2-4.7
|
||||||
|
c-0.1-0.9-0.1-2-0.4-2.9C45.5,192.3,47.6,192,49.6,191.6z M51.7,198.9c0.1-1.4-0.1-2.9-0.3-4.3c-0.1-1-0.1-2.1-0.4-3.1
|
||||||
|
c2.1-0.3,4.1-0.7,6.2-1c-0.1,0.9,0,2,0.1,2.9c0.1,1.5,0.1,3,0.5,4.4c-0.7,0.1-1.3,0.3-2,0.4C54.4,198.4,53,198.6,51.7,198.9z
|
||||||
|
M97.9,180.2c-0.3,0.6-1.2,0.6-1.6,0.1c-0.4-0.5-0.2-1.3,0.3-1.7c0.7-0.5,1.2,0.1,1.5,0.7C98.1,179.6,98.1,179.9,97.9,180.2z"/>
|
||||||
|
<path class="st9" d="M293.1,170.4c-5.6,0.8-11.1,1.6-16.7,2.4c0.2-6.9-0.6-14.5-2.3-21.1c0.5-0.2,0.9-0.4,1.2-0.9
|
||||||
|
c0.4-0.6,0.6-1.6,0.4-2.5c0,0,0,0,0-0.1c0.2-2.1-2.5-3-4.1-2.1c-1.5,0.9-2.1,3.1-1,4.5c0.6,0.8,1.7,1.3,2.8,1.2
|
||||||
|
c-0.6,6.8-0.1,14.5,1.3,21.2c-2.4,0.4-4.7,0.8-7.1,1.2c0.2-4-0.3-8.1-0.7-12.1c-0.3-2.8-0.4-5.5-1.3-8.1c0.6-0.1,1.2-0.4,1.5-0.9
|
||||||
|
c0.4-0.6,0.6-1.6,0.4-2.5c0,0,0,0,0-0.1c0.2-2.1-2.5-3-4.1-2.1c-1.5,0.9-2.1,3.1-1,4.5c0.5,0.7,1.3,1.1,2.1,1.2
|
||||||
|
c-0.5,3.2-0.1,6.6,0.2,9.8c0.3,3.6,0.5,7.2,1.2,10.7c-2.2,0.4-4.5,0.8-6.7,1.3c-0.1-7-0.4-15-2.3-21.8c0.5-0.2,0.9-0.4,1.2-0.9
|
||||||
|
c0.4-0.6,0.6-1.6,0.4-2.5c0,0,0,0,0-0.1c0.2-2.1-2.5-3-4.1-2.1c-1.5,0.9-2.1,3.1-1,4.5c0.5,0.7,1.5,1.2,2.4,1.2
|
||||||
|
c-0.7,7.3,0.3,15.7,1.5,22.7c0.1,0.5,0.5,0.7,0.9,0.7c0.1,0.1,0.3,0.2,0.6,0.2c11.5-1.4,23-3.3,34.4-5.4c-0.4,3.7,0.1,7.7,0.5,11.3
|
||||||
|
c0,0.1,0,0.2,0.1,0.2c-2.7,0-5.4,0.5-8,1c-1.9,0.3-4.1,0.4-5.8,1.4c-0.9-0.8-2.4-1-3.4-0.4c-1.5,0.9-2.1,3.1-1,4.5
|
||||||
|
c1.1,1.4,3.6,1.7,4.6,0.2c0.4-0.6,0.6-1.6,0.4-2.5c0,0,0,0,0-0.1c0-0.2,0-0.3,0-0.5c1.5,0.1,3-0.2,4.5-0.5c3-0.5,6.2-0.8,9.1-1.8
|
||||||
|
c0.3-0.1,0.5-0.4,0.5-0.7c0.4,0,0.7-0.3,0.7-0.8c-0.2-3.8-0.3-8.1-1.5-11.8C294.6,171.2,294,170.2,293.1,170.4z M272.4,149.6
|
||||||
|
c-0.4-0.5-0.2-1.3,0.3-1.7c0.7-0.5,1.2,0.1,1.5,0.7c0,0.3,0,0.5-0.2,0.8C273.7,150.1,272.8,150.1,272.4,149.6z M264.6,150.2
|
||||||
|
c0.7-0.5,1.2,0.1,1.5,0.7c0,0.3,0,0.5-0.2,0.8c-0.3,0.6-1.2,0.6-1.6,0.1C263.8,151.4,264.1,150.6,264.6,150.2z M255.5,151.9
|
||||||
|
c-0.4-0.5-0.2-1.3,0.3-1.7c0.7-0.5,1.2,0.1,1.5,0.7c0,0.3,0,0.5-0.2,0.8C256.8,152.4,255.9,152.4,255.5,151.9z M279.2,189.1
|
||||||
|
c-0.3,0.6-1.2,0.6-1.6,0.1c-0.4-0.5-0.2-1.3,0.3-1.7c0.7-0.5,1.2,0.1,1.5,0.7C279.4,188.5,279.4,188.8,279.2,189.1z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 21 KiB |
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.0.18",
|
"version": "1.0.19-alpha.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -65,10 +65,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.18",
|
"@budibase/bbui": "^1.0.19-alpha.0",
|
||||||
"@budibase/client": "^1.0.18",
|
"@budibase/client": "^1.0.19-alpha.0",
|
||||||
"@budibase/colorpicker": "1.1.2",
|
"@budibase/colorpicker": "1.1.2",
|
||||||
"@budibase/string-templates": "^1.0.18",
|
"@budibase/string-templates": "^1.0.19-alpha.0",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
|
44
packages/builder/src/builderStore/datasource.js
Normal file
44
packages/builder/src/builderStore/datasource.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { datasources, tables } from "../stores/backend"
|
||||||
|
import { IntegrationNames } from "../constants/backend"
|
||||||
|
import analytics, { Events } from "../analytics"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import cloneDeep from "lodash/cloneDeepWith"
|
||||||
|
|
||||||
|
function prepareData(config) {
|
||||||
|
let datasource = {}
|
||||||
|
let existingTypeCount = get(datasources).list.filter(
|
||||||
|
ds => ds.source === config.type
|
||||||
|
).length
|
||||||
|
|
||||||
|
let baseName = IntegrationNames[config.type]
|
||||||
|
let name =
|
||||||
|
existingTypeCount === 0 ? baseName : `${baseName}-${existingTypeCount + 1}`
|
||||||
|
|
||||||
|
datasource.type = "datasource"
|
||||||
|
datasource.source = config.type
|
||||||
|
datasource.config = config.config
|
||||||
|
datasource.name = name
|
||||||
|
datasource.plus = config.plus
|
||||||
|
|
||||||
|
return datasource
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveDatasource(config) {
|
||||||
|
const datasource = prepareData(config)
|
||||||
|
// Create datasource
|
||||||
|
const resp = await datasources.save(datasource, datasource.plus)
|
||||||
|
|
||||||
|
// update the tables incase data source plus
|
||||||
|
await tables.fetch()
|
||||||
|
await datasources.select(resp._id)
|
||||||
|
analytics.captureEvent(Events.DATASOURCE.CREATED, {
|
||||||
|
name: resp.name,
|
||||||
|
source: resp.source,
|
||||||
|
})
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRestDatasource(integration) {
|
||||||
|
const config = cloneDeep(integration)
|
||||||
|
return saveDatasource(config)
|
||||||
|
}
|
|
@ -39,7 +39,7 @@
|
||||||
$: editRowComponent = isUsersTable ? CreateEditUser : CreateEditRow
|
$: editRowComponent = isUsersTable ? CreateEditUser : CreateEditRow
|
||||||
$: {
|
$: {
|
||||||
UNSORTABLE_TYPES.forEach(type => {
|
UNSORTABLE_TYPES.forEach(type => {
|
||||||
Object.values(schema).forEach(col => {
|
Object.values(schema || {}).forEach(col => {
|
||||||
if (col.type === type) {
|
if (col.type === type) {
|
||||||
col.sortable = false
|
col.sortable = false
|
||||||
}
|
}
|
||||||
|
@ -113,16 +113,16 @@
|
||||||
|
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
<div>
|
<div>
|
||||||
<div class="table-title">
|
|
||||||
{#if title}
|
{#if title}
|
||||||
|
<div class="table-title">
|
||||||
<Heading size="S">{title}</Heading>
|
<Heading size="S">{title}</Heading>
|
||||||
{/if}
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div transition:fade|local>
|
<div transition:fade|local>
|
||||||
<Spinner size="10" />
|
<Spinner size="10" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="popovers">
|
<div class="popovers">
|
||||||
<slot />
|
<slot />
|
||||||
{#if !isUsersTable && selectedRows.length > 0}
|
{#if !isUsersTable && selectedRows.length > 0}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
|
import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
|
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
|
||||||
|
import { customQueryIconText, customQueryIconColor } from "helpers/data/utils"
|
||||||
import ICONS from "./icons"
|
import ICONS from "./icons"
|
||||||
|
|
||||||
let openDataSources = []
|
let openDataSources = []
|
||||||
|
@ -129,6 +130,8 @@
|
||||||
<NavItem
|
<NavItem
|
||||||
indentLevel={1}
|
indentLevel={1}
|
||||||
icon="SQLQuery"
|
icon="SQLQuery"
|
||||||
|
iconText={customQueryIconText(datasource, query)}
|
||||||
|
iconColor={customQueryIconColor(datasource, query)}
|
||||||
text={query.name}
|
text={query.name}
|
||||||
opened={$queries.selected === query._id}
|
opened={$queries.selected === query._id}
|
||||||
selected={$queries.selected === query._id}
|
selected={$queries.selected === query._id}
|
||||||
|
|
|
@ -9,19 +9,53 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
|
import { IntegrationTypes } from "constants/backend"
|
||||||
|
|
||||||
export let integration
|
export let datasource
|
||||||
export let schema
|
export let schema
|
||||||
|
export let creating
|
||||||
|
|
||||||
|
function filter([key, value]) {
|
||||||
|
if (!value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !(
|
||||||
|
(datasource.source === IntegrationTypes.REST &&
|
||||||
|
key === "defaultHeaders") ||
|
||||||
|
value.deprecated
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$: config = datasource?.config
|
||||||
|
$: configKeys = Object.entries(schema || {})
|
||||||
|
.filter(el => filter(el))
|
||||||
|
.map(([key]) => key)
|
||||||
|
|
||||||
let addButton
|
let addButton
|
||||||
|
|
||||||
|
function getDisplayName(key) {
|
||||||
|
let name
|
||||||
|
if (schema[key]?.display) {
|
||||||
|
name = schema[key].display
|
||||||
|
} else {
|
||||||
|
name = key
|
||||||
|
}
|
||||||
|
return capitalise(name)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
<Layout gap="S">
|
<Layout noPadding gap="S">
|
||||||
{#each Object.keys(schema) as configKey}
|
{#if !creating}
|
||||||
|
<div class="form-row">
|
||||||
|
<Label>Name</Label>
|
||||||
|
<Input on:change bind:value={datasource.name} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#each configKeys as configKey}
|
||||||
{#if schema[configKey].type === "object"}
|
{#if schema[configKey].type === "object"}
|
||||||
<div class="form-row ssl">
|
<div class="form-row ssl">
|
||||||
<Label>{capitalise(configKey)}</Label>
|
<Label>{getDisplayName(configKey)}</Label>
|
||||||
<Button secondary thin outline on:click={addButton.addEntry()}
|
<Button secondary thin outline on:click={addButton.addEntry()}
|
||||||
>Add</Button
|
>Add</Button
|
||||||
>
|
>
|
||||||
|
@ -29,30 +63,31 @@
|
||||||
<KeyValueBuilder
|
<KeyValueBuilder
|
||||||
bind:this={addButton}
|
bind:this={addButton}
|
||||||
defaults={schema[configKey].default}
|
defaults={schema[configKey].default}
|
||||||
bind:object={integration[configKey]}
|
bind:object={config[configKey]}
|
||||||
|
on:change
|
||||||
noAddButton={true}
|
noAddButton={true}
|
||||||
/>
|
/>
|
||||||
{:else if schema[configKey].type === "boolean"}
|
{:else if schema[configKey].type === "boolean"}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<Label>{capitalise(configKey)}</Label>
|
<Label>{getDisplayName(configKey)}</Label>
|
||||||
<Toggle text="" bind:value={integration[configKey]} />
|
<Toggle text="" bind:value={config[configKey]} />
|
||||||
</div>
|
</div>
|
||||||
{:else if schema[configKey].type === "longForm"}
|
{:else if schema[configKey].type === "longForm"}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<Label>{capitalise(configKey)}</Label>
|
<Label>{getDisplayName(configKey)}</Label>
|
||||||
<TextArea
|
<TextArea
|
||||||
type={schema[configKey].type}
|
type={schema[configKey].type}
|
||||||
on:change
|
on:change
|
||||||
bind:value={integration[configKey]}
|
bind:value={config[configKey]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<Label>{capitalise(configKey)}</Label>
|
<Label>{getDisplayName(configKey)}</Label>
|
||||||
<Input
|
<Input
|
||||||
type={schema[configKey].type}
|
type={schema[configKey].type}
|
||||||
on:change
|
on:change
|
||||||
bind:value={integration[configKey]}
|
bind:value={config[configKey]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,221 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Divider,
|
||||||
|
InlineAlert,
|
||||||
|
Button,
|
||||||
|
notifications,
|
||||||
|
Modal,
|
||||||
|
Table,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { datasources, integrations, tables } from "stores/backend"
|
||||||
|
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
|
||||||
|
import CreateExternalTableModal from "./CreateExternalTableModal.svelte"
|
||||||
|
import ArrayRenderer from "components/common/ArrayRenderer.svelte"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
|
export let datasource
|
||||||
|
export let save
|
||||||
|
|
||||||
|
let tableSchema = {
|
||||||
|
name: {},
|
||||||
|
primary: { displayName: "Primary Key" },
|
||||||
|
}
|
||||||
|
let relationshipSchema = {
|
||||||
|
tables: {},
|
||||||
|
columns: {},
|
||||||
|
}
|
||||||
|
let relationshipModal
|
||||||
|
let createExternalTableModal
|
||||||
|
let selectedFromRelationship, selectedToRelationship
|
||||||
|
let confirmDialog
|
||||||
|
|
||||||
|
$: integration = datasource && $integrations[datasource.source]
|
||||||
|
$: plusTables = datasource?.plus
|
||||||
|
? Object.values(datasource?.entities || {})
|
||||||
|
: []
|
||||||
|
$: relationships = getRelationships(plusTables)
|
||||||
|
$: schemaError = $datasources.schemaError
|
||||||
|
$: relationshipInfo = relationshipTableData(relationships)
|
||||||
|
|
||||||
|
function getRelationships(tables) {
|
||||||
|
if (!tables || !Array.isArray(tables)) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
let pairs = {}
|
||||||
|
for (let table of tables) {
|
||||||
|
for (let column of Object.values(table.schema)) {
|
||||||
|
if (column.type !== "link") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// these relationships have an id to pair them to each other
|
||||||
|
// one has a main for the from side
|
||||||
|
const key = column.main ? "from" : "to"
|
||||||
|
pairs[column._id] = {
|
||||||
|
...pairs[column._id],
|
||||||
|
[key]: column,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pairs
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRelationshipDisplayString(fromCol, toCol) {
|
||||||
|
function getTableName(tableId) {
|
||||||
|
if (!tableId || typeof tableId !== "string") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return plusTables.find(table => table._id === tableId)?.name || "Unknown"
|
||||||
|
}
|
||||||
|
if (!toCol || !fromCol) {
|
||||||
|
return "Cannot build name"
|
||||||
|
}
|
||||||
|
const fromTableName = getTableName(toCol.tableId)
|
||||||
|
const toTableName = getTableName(fromCol.tableId)
|
||||||
|
const throughTableName = getTableName(fromCol.through)
|
||||||
|
|
||||||
|
let displayString
|
||||||
|
if (throughTableName) {
|
||||||
|
displayString = `${fromTableName} through ${throughTableName} → ${toTableName}`
|
||||||
|
} else {
|
||||||
|
displayString = `${fromTableName} → ${toTableName}`
|
||||||
|
}
|
||||||
|
return displayString
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateDatasourceSchema() {
|
||||||
|
try {
|
||||||
|
await datasources.updateSchema(datasource)
|
||||||
|
notifications.success(`Datasource ${name} tables updated successfully.`)
|
||||||
|
await tables.fetch()
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Error updating datasource schema: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickTable(table) {
|
||||||
|
tables.select(table)
|
||||||
|
$goto(`../../table/${table._id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRelationshipModal(fromRelationship, toRelationship) {
|
||||||
|
selectedFromRelationship = fromRelationship || {}
|
||||||
|
selectedToRelationship = toRelationship || {}
|
||||||
|
relationshipModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewTable() {
|
||||||
|
createExternalTableModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function relationshipTableData(relations) {
|
||||||
|
return Object.values(relations).map(relationship => ({
|
||||||
|
tables: buildRelationshipDisplayString(
|
||||||
|
relationship.from,
|
||||||
|
relationship.to
|
||||||
|
),
|
||||||
|
columns: `${relationship.from?.name} to ${relationship.to?.name}`,
|
||||||
|
from: relationship.from,
|
||||||
|
to: relationship.to,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={relationshipModal}>
|
||||||
|
<CreateEditRelationship
|
||||||
|
{datasource}
|
||||||
|
{save}
|
||||||
|
close={relationshipModal.hide}
|
||||||
|
{plusTables}
|
||||||
|
fromRelationship={selectedFromRelationship}
|
||||||
|
toRelationship={selectedToRelationship}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal bind:this={createExternalTableModal}>
|
||||||
|
<CreateExternalTableModal {datasource} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={confirmDialog}
|
||||||
|
okText="Fetch tables"
|
||||||
|
onOk={updateDatasourceSchema}
|
||||||
|
onCancel={() => confirmDialog.hide()}
|
||||||
|
warning={false}
|
||||||
|
title="Confirm table fetch"
|
||||||
|
>
|
||||||
|
<Body>
|
||||||
|
If you have fetched tables from this database before, this action may
|
||||||
|
overwrite any changes you made after your initial fetch.
|
||||||
|
</Body>
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<Divider size="S" />
|
||||||
|
<div class="query-header">
|
||||||
|
<Heading size="S">Tables</Heading>
|
||||||
|
<div class="table-buttons">
|
||||||
|
<Button secondary on:click={() => confirmDialog.show()}>
|
||||||
|
Fetch tables
|
||||||
|
</Button>
|
||||||
|
<Button cta icon="Add" on:click={createNewTable}>New table</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Body>
|
||||||
|
This datasource can determine tables automatically. Budibase can fetch your
|
||||||
|
tables directly from the database and you can use them without having to write
|
||||||
|
any queries at all.
|
||||||
|
</Body>
|
||||||
|
{#if schemaError}
|
||||||
|
<InlineAlert
|
||||||
|
type="error"
|
||||||
|
header="Error fetching tables"
|
||||||
|
message={schemaError}
|
||||||
|
onConfirm={datasources.removeSchemaError}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<Table
|
||||||
|
on:click={({ detail }) => onClickTable(detail)}
|
||||||
|
schema={tableSchema}
|
||||||
|
data={Object.values(plusTables)}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
customRenderers={[{ column: "primary", component: ArrayRenderer }]}
|
||||||
|
/>
|
||||||
|
{#if plusTables?.length !== 0}
|
||||||
|
<Divider size="S" />
|
||||||
|
<div class="query-header">
|
||||||
|
<Heading size="S">Relationships</Heading>
|
||||||
|
<Button primary on:click={openRelationshipModal}>
|
||||||
|
Define relationship
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Body>
|
||||||
|
Tell budibase how your tables are related to get even more smart features.
|
||||||
|
</Body>
|
||||||
|
{/if}
|
||||||
|
<Table
|
||||||
|
on:click={({ detail }) => openRelationshipModal(detail.from, detail.to)}
|
||||||
|
schema={relationshipSchema}
|
||||||
|
data={relationshipInfo}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.query-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 0 var(--spacing-s) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,45 @@
|
||||||
|
<script>
|
||||||
|
import { Divider, Heading, ActionButton, Badge, Body } from "@budibase/bbui"
|
||||||
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
|
|
||||||
|
export let datasource
|
||||||
|
|
||||||
|
let addHeader
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Divider size="S" />
|
||||||
|
<div class="query-header">
|
||||||
|
<div class="badge">
|
||||||
|
<Heading size="S">Headers</Heading>
|
||||||
|
<Badge quiet grey>Optional</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Body size="S">
|
||||||
|
Headers enable you to provide additional information about the request, such
|
||||||
|
as format.
|
||||||
|
</Body>
|
||||||
|
<KeyValueBuilder
|
||||||
|
bind:this={addHeader}
|
||||||
|
bind:object={datasource.config.defaultHeaders}
|
||||||
|
on:change
|
||||||
|
noAddButton
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<ActionButton icon="Add" on:click={() => addHeader.addEntry()}>
|
||||||
|
Add header
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.query-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -3,9 +3,11 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import ICONS from "../icons"
|
import ICONS from "../icons"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import { IntegrationNames } from "constants"
|
import { IntegrationNames, IntegrationTypes } from "constants/backend"
|
||||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||||
|
import { createRestDatasource } from "builderStore/datasource"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
|
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
|
||||||
|
|
||||||
export let modal
|
export let modal
|
||||||
|
@ -19,8 +21,6 @@
|
||||||
|
|
||||||
checkShowImport()
|
checkShowImport()
|
||||||
|
|
||||||
const INTERNAL = "BUDIBASE"
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
fetchIntegrations()
|
fetchIntegrations()
|
||||||
})
|
})
|
||||||
|
@ -50,10 +50,14 @@
|
||||||
importModal.show()
|
importModal.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseNextModal() {
|
async function chooseNextModal() {
|
||||||
if (integration.type === INTERNAL) {
|
if (integration.type === IntegrationTypes.INTERNAL) {
|
||||||
externalDatasourceModal.hide()
|
externalDatasourceModal.hide()
|
||||||
internalTableModal.show()
|
internalTableModal.show()
|
||||||
|
} else if (integration.type === IntegrationTypes.REST) {
|
||||||
|
// skip modal for rest, create straight away
|
||||||
|
const resp = await createRestDatasource(integration)
|
||||||
|
$goto(`./datasource/${resp._id}`)
|
||||||
} else {
|
} else {
|
||||||
externalDatasourceModal.show()
|
externalDatasourceModal.show()
|
||||||
}
|
}
|
||||||
|
@ -63,7 +67,7 @@
|
||||||
const response = await api.get("/api/integrations")
|
const response = await api.get("/api/integrations")
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
integrations = {
|
integrations = {
|
||||||
[INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
[IntegrationTypes.INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
||||||
...json,
|
...json,
|
||||||
}
|
}
|
||||||
return json
|
return json
|
||||||
|
@ -108,8 +112,8 @@
|
||||||
to your app using Budibase's built-in database.
|
to your app using Budibase's built-in database.
|
||||||
</Body>
|
</Body>
|
||||||
<div
|
<div
|
||||||
class:selected={integration.type === INTERNAL}
|
class:selected={integration.type === IntegrationTypes.INTERNAL}
|
||||||
on:click={() => selectIntegration(INTERNAL)}
|
on:click={() => selectIntegration(IntegrationTypes.INTERNAL)}
|
||||||
class="item hoverable"
|
class="item hoverable"
|
||||||
>
|
>
|
||||||
<div class="item-body">
|
<div class="item-body">
|
||||||
|
@ -124,7 +128,7 @@
|
||||||
<Detail size="S">Connect to data source</Detail>
|
<Detail size="S">Connect to data source</Detail>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-list">
|
<div class="item-list">
|
||||||
{#each Object.entries(integrations).filter(([key]) => key !== INTERNAL) as [integrationType, schema]}
|
{#each Object.entries(integrations).filter(([key]) => key !== IntegrationTypes.INTERNAL) as [integrationType, schema]}
|
||||||
<div
|
<div
|
||||||
class:selected={integration.type === integrationType}
|
class:selected={integration.type === integrationType}
|
||||||
on:click={() => selectIntegration(integrationType)}
|
on:click={() => selectIntegration(integrationType)}
|
||||||
|
|
|
@ -1,66 +1,33 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { ModalContent, notifications, Body, Layout } from "@budibase/bbui"
|
import { ModalContent, notifications, Body, Layout } from "@budibase/bbui"
|
||||||
import analytics, { Events } from "analytics"
|
|
||||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||||
import { datasources, tables } from "stores/backend"
|
import { IntegrationNames } from "constants/backend"
|
||||||
import { IntegrationNames } from "constants"
|
|
||||||
import cloneDeep from "lodash/cloneDeepWith"
|
import cloneDeep from "lodash/cloneDeepWith"
|
||||||
|
import { saveDatasource as save } from "builderStore/datasource"
|
||||||
|
|
||||||
export let integration
|
export let integration
|
||||||
export let modal
|
export let modal
|
||||||
|
|
||||||
// kill the reference so the input isn't saved
|
// kill the reference so the input isn't saved
|
||||||
let config = cloneDeep(integration)
|
let datasource = cloneDeep(integration)
|
||||||
|
|
||||||
function prepareData() {
|
|
||||||
let datasource = {}
|
|
||||||
let existingTypeCount = $datasources.list.filter(
|
|
||||||
ds => ds.source == config.type
|
|
||||||
).length
|
|
||||||
|
|
||||||
let baseName = IntegrationNames[config.type]
|
|
||||||
let name =
|
|
||||||
existingTypeCount === 0
|
|
||||||
? baseName
|
|
||||||
: `${baseName}-${existingTypeCount + 1}`
|
|
||||||
|
|
||||||
datasource.type = "datasource"
|
|
||||||
datasource.source = config.type
|
|
||||||
datasource.config = config.config
|
|
||||||
datasource.name = name
|
|
||||||
datasource.plus = config.plus
|
|
||||||
|
|
||||||
return datasource
|
|
||||||
}
|
|
||||||
async function saveDatasource() {
|
async function saveDatasource() {
|
||||||
const datasource = prepareData()
|
|
||||||
try {
|
try {
|
||||||
// Create datasource
|
const resp = await save(datasource)
|
||||||
const resp = await datasources.save(datasource, datasource.plus)
|
|
||||||
|
|
||||||
// update the tables incase data source plus
|
|
||||||
await tables.fetch()
|
|
||||||
await datasources.select(resp._id)
|
|
||||||
$goto(`./datasource/${resp._id}`)
|
$goto(`./datasource/${resp._id}`)
|
||||||
notifications.success(`Datasource updated successfully.`)
|
notifications.success(`Datasource updated successfully.`)
|
||||||
analytics.captureEvent(Events.DATASOURCE.CREATED, {
|
|
||||||
name: resp.name,
|
|
||||||
source: resp.source,
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(`Error saving datasource: ${err}`)
|
notifications.error(`Error saving datasource: ${err}`)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={`Connect to ${IntegrationNames[config.type]}`}
|
title={`Connect to ${IntegrationNames[datasource.type]}`}
|
||||||
onConfirm={() => saveDatasource()}
|
onConfirm={() => saveDatasource()}
|
||||||
onCancel={() => modal.show()}
|
onCancel={() => modal.show()}
|
||||||
confirmText={config.plus
|
confirmText={datasource.plus
|
||||||
? "Fetch tables from database"
|
? "Fetch tables from database"
|
||||||
: "Save and continue to query"}
|
: "Save and continue to query"}
|
||||||
cancelText="Back"
|
cancelText="Back"
|
||||||
|
@ -72,7 +39,11 @@
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<IntegrationConfigForm schema={config.schema} integration={config.config} />
|
<IntegrationConfigForm
|
||||||
|
schema={datasource.schema}
|
||||||
|
bind:datasource
|
||||||
|
creating={true}
|
||||||
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { datasources, tables } from "stores/backend"
|
import { datasources, queries, tables } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
|
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
@ -12,12 +12,18 @@
|
||||||
let updateDatasourceDialog
|
let updateDatasourceDialog
|
||||||
|
|
||||||
async function deleteDatasource() {
|
async function deleteDatasource() {
|
||||||
const wasSelectedSource = $datasources.selected
|
let wasSelectedSource = $datasources.selected
|
||||||
|
if (!wasSelectedSource && $queries.selected) {
|
||||||
|
const queryId = $queries.selected
|
||||||
|
wasSelectedSource = $datasources.list.find(ds =>
|
||||||
|
queryId.includes(ds._id)
|
||||||
|
)?._id
|
||||||
|
}
|
||||||
const wasSelectedTable = $tables.selected
|
const wasSelectedTable = $tables.selected
|
||||||
await datasources.delete(datasource)
|
await datasources.delete(datasource)
|
||||||
notifications.success("Datasource deleted")
|
notifications.success("Datasource deleted")
|
||||||
// navigate to first index page if the source you are deleting is selected
|
// navigate to first index page if the source you are deleting is selected
|
||||||
const entities = Object.values(datasource.entities)
|
const entities = Object.values(datasource?.entities || {})
|
||||||
if (
|
if (
|
||||||
wasSelectedSource === datasource._id ||
|
wasSelectedSource === datasource._id ||
|
||||||
(entities &&
|
(entities &&
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui"
|
import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { queries } from "stores/backend"
|
import { datasources, queries } from "stores/backend"
|
||||||
|
|
||||||
export let query
|
export let query
|
||||||
|
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
|
|
||||||
async function deleteQuery() {
|
async function deleteQuery() {
|
||||||
|
const wasSelectedQuery = $queries.selected
|
||||||
|
const selectedDatasource = $datasources.selected
|
||||||
await queries.delete(query)
|
await queries.delete(query)
|
||||||
|
if (wasSelectedQuery === query._id) {
|
||||||
|
$goto(`./datasource/${selectedDatasource}`)
|
||||||
|
}
|
||||||
notifications.success("Query deleted")
|
notifications.success("Query deleted")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
export let value
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{Array.isArray(value) ? value.join(", ") : value}
|
|
@ -15,6 +15,9 @@
|
||||||
name: "handlebars",
|
name: "handlebars",
|
||||||
base: "text/html",
|
base: "text/html",
|
||||||
},
|
},
|
||||||
|
Text: {
|
||||||
|
name: "text/html",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,30 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon, Body } from "@budibase/bbui"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a target="_blank" href="https://github.com/Budibase/budibase/discussions">
|
<a target="_blank" href="https://github.com/Budibase/budibase/discussions">
|
||||||
<Icon hoverable name="Help" size="XXL" />
|
<div class="inner hoverable">
|
||||||
|
<div class="hidden hoverable">
|
||||||
|
<Body size="S">Need help? Go to our forums</Body>
|
||||||
|
</div>
|
||||||
|
<Icon name="Help" size="XXL" />
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.inner :global(*) {
|
||||||
|
pointer-events: all;
|
||||||
|
transition: color var(--spectrum-global-animation-duration-100, 130ms);
|
||||||
|
}
|
||||||
|
.inner:hover :global(*) {
|
||||||
|
color: var(--spectrum-alias-icon-color-selected-hover);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -14,4 +32,10 @@
|
||||||
right: var(--spacing-m);
|
right: var(--spacing-m);
|
||||||
border-radius: 55%;
|
border-radius: 55%;
|
||||||
}
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.inner:hover .hidden {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
export let selected = false
|
export let selected = false
|
||||||
export let opened = false
|
export let opened = false
|
||||||
export let draggable = false
|
export let draggable = false
|
||||||
|
export let iconText
|
||||||
|
export let iconColor
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -42,9 +44,13 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<slot name="icon" />
|
<slot name="icon" />
|
||||||
{#if icon}
|
{#if iconText}
|
||||||
|
<div class="iconText" style={iconColor ? `color: ${iconColor};` : ""}>
|
||||||
|
{iconText}
|
||||||
|
</div>
|
||||||
|
{:else if icon}
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<Icon size="S" name={icon} />
|
<Icon color={iconColor} size="S" name={icon} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text">{text}</div>
|
<div class="text">{text}</div>
|
||||||
|
@ -123,4 +129,9 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.iconText {
|
||||||
|
margin-top: 1px;
|
||||||
|
font-size: var(--spectrum-global-dimension-font-size-50);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
<script>
|
||||||
|
import { Heading, Icon, Input, Label, Body } from "@budibase/bbui"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
let dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
export let defaultValue = ""
|
||||||
|
export let value
|
||||||
|
export let type = "label"
|
||||||
|
export let size = "M"
|
||||||
|
|
||||||
|
let editing = false
|
||||||
|
|
||||||
|
function setEditing(state) {
|
||||||
|
editing = state
|
||||||
|
if (editing) {
|
||||||
|
dispatch("change")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="parent">
|
||||||
|
{#if !editing}
|
||||||
|
{#if type === "heading"}
|
||||||
|
<Heading {size}>{value || defaultValue}</Heading>
|
||||||
|
{:else if type === "body"}
|
||||||
|
<Body {size}>{value || defaultValue}</Body>
|
||||||
|
{:else}
|
||||||
|
<Label {size}>{value || defaultValue}</Label>
|
||||||
|
{/if}
|
||||||
|
<div class="hide">
|
||||||
|
<Icon name="Edit" hoverable size="S" on:click={() => setEditing(true)} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="input">
|
||||||
|
<Input placeholder={defaultValue} bind:value on:change />
|
||||||
|
</div>
|
||||||
|
<Icon
|
||||||
|
name="SaveFloppy"
|
||||||
|
hoverable
|
||||||
|
size="S"
|
||||||
|
on:click={() => setEditing(false)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.parent {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.hide {
|
||||||
|
display: none;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.parent:hover .hide {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -17,7 +17,7 @@
|
||||||
queries as queriesStore,
|
queries as queriesStore,
|
||||||
} from "stores/backend"
|
} from "stores/backend"
|
||||||
import { datasources, integrations } from "stores/backend"
|
import { datasources, integrations } from "stores/backend"
|
||||||
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
|
||||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
|
|
||||||
|
@ -148,15 +148,15 @@
|
||||||
/>
|
/>
|
||||||
{#if value?.type === "query"}
|
{#if value?.type === "query"}
|
||||||
<i class="ri-settings-5-line" on:click={openQueryParamsDrawer} />
|
<i class="ri-settings-5-line" on:click={openQueryParamsDrawer} />
|
||||||
<Drawer title={"Query Parameters"} bind:this={drawer}>
|
<Drawer title={"Query Bindings"} bind:this={drawer}>
|
||||||
<Button slot="buttons" cta on:click={saveQueryParams}>Save</Button>
|
<Button slot="buttons" cta on:click={saveQueryParams}>Save</Button>
|
||||||
<DrawerContent slot="body">
|
<DrawerContent slot="body">
|
||||||
<Layout noPadding>
|
<Layout noPadding gap="XS">
|
||||||
{#if getQueryParams(value).length > 0}
|
{#if getQueryParams(value).length > 0}
|
||||||
<ParameterBuilder
|
<BindingBuilder
|
||||||
bind:customParams={tmpQueryParams}
|
bind:customParams={tmpQueryParams}
|
||||||
parameters={getQueryParams(value)}
|
bindings={getQueryParams(value)}
|
||||||
{bindings}
|
bind:bindableOptions={bindings}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<IntegrationQueryEditor
|
<IntegrationQueryEditor
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Layout, Input, Checkbox } from "@budibase/bbui"
|
import { Select, Layout, Input, Checkbox } from "@budibase/bbui"
|
||||||
import { datasources, integrations, queries } from "stores/backend"
|
import { datasources, integrations, queries } from "stores/backend"
|
||||||
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
|
||||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
|
@ -53,10 +53,10 @@
|
||||||
|
|
||||||
{#if query?.parameters?.length > 0}
|
{#if query?.parameters?.length > 0}
|
||||||
<div>
|
<div>
|
||||||
<ParameterBuilder
|
<BindingBuilder
|
||||||
bind:customParams={parameters.queryParams}
|
bind:customParams={parameters.queryParams}
|
||||||
parameters={query.parameters}
|
bindings={query.parameters}
|
||||||
{bindings}
|
bind:bindableOptions={bindings}
|
||||||
/>
|
/>
|
||||||
<IntegrationQueryEditor
|
<IntegrationQueryEditor
|
||||||
height={200}
|
height={200}
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
<script>
|
||||||
|
import { Label, Select } from "@budibase/bbui"
|
||||||
|
import { permissions, roles } from "stores/backend"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { Roles } from "constants/backend"
|
||||||
|
|
||||||
|
export let query
|
||||||
|
export let saveId
|
||||||
|
export let label
|
||||||
|
|
||||||
|
$: updateRole(roleId, saveId)
|
||||||
|
|
||||||
|
let roleId, loaded
|
||||||
|
|
||||||
|
async function updateRole(role, id) {
|
||||||
|
roleId = role
|
||||||
|
const queryId = query?._id || id
|
||||||
|
if (roleId && queryId) {
|
||||||
|
for (let level of ["read", "write"]) {
|
||||||
|
await permissions.save({
|
||||||
|
level,
|
||||||
|
role,
|
||||||
|
resource: queryId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (!query || !query._id) {
|
||||||
|
roleId = Roles.BASIC
|
||||||
|
loaded = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
roleId = (await permissions.forResource(query._id))["read"]
|
||||||
|
} catch (err) {
|
||||||
|
roleId = Roles.BASIC
|
||||||
|
}
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if loaded}
|
||||||
|
{#if label}
|
||||||
|
<Label>{label}</Label>
|
||||||
|
{/if}
|
||||||
|
<Select
|
||||||
|
value={roleId}
|
||||||
|
on:change={e => updateRole(e.detail)}
|
||||||
|
options={$roles}
|
||||||
|
getOptionLabel={x => x.name}
|
||||||
|
getOptionValue={x => x._id}
|
||||||
|
autoWidth
|
||||||
|
/>
|
||||||
|
{/if}
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script>
|
||||||
|
import { TextArea } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let data
|
||||||
|
export let height
|
||||||
|
export let minHeight = "120"
|
||||||
|
|
||||||
|
$: string = JSON.stringify(data || {}, null, 2)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<TextArea disabled value={string} {height} {minHeight} />
|
|
@ -1,41 +1,118 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, Button, Input } from "@budibase/bbui"
|
import {
|
||||||
|
Icon,
|
||||||
|
ActionButton,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Toggle,
|
||||||
|
Select,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { lowercase } from "helpers"
|
||||||
|
|
||||||
|
let dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let defaults
|
export let defaults
|
||||||
export let object = defaults || {}
|
export let object = defaults || {}
|
||||||
|
export let activity = {}
|
||||||
export let readOnly
|
export let readOnly
|
||||||
export let noAddButton
|
export let noAddButton
|
||||||
|
export let name
|
||||||
|
export let headings = false
|
||||||
|
export let options
|
||||||
|
export let toggle
|
||||||
|
export let keyPlaceholder = "Key"
|
||||||
|
export let valuePlaceholder = "Value"
|
||||||
|
export let tooltip
|
||||||
|
|
||||||
let fields = Object.entries(object).map(([name, value]) => ({ name, value }))
|
let fields = Object.entries(object).map(([name, value]) => ({ name, value }))
|
||||||
|
let fieldActivity = buildFieldActivity(activity)
|
||||||
|
|
||||||
$: object = fields.reduce(
|
$: object = fields.reduce(
|
||||||
(acc, next) => ({ ...acc, [next.name]: next.value }),
|
(acc, next) => ({ ...acc, [next.name]: next.value }),
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function buildFieldActivity(obj) {
|
||||||
|
if (!obj || typeof obj !== "object") {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const array = Array(fields.length)
|
||||||
|
for (let [key, value] of Object.entries(obj)) {
|
||||||
|
const field = fields.find(el => el.name === key)
|
||||||
|
const idx = fields.indexOf(field)
|
||||||
|
array[idx] = idx !== -1 ? value : true
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
export function addEntry() {
|
export function addEntry() {
|
||||||
fields = [...fields, {}]
|
fields = [...fields, { name: "", value: "" }]
|
||||||
|
fieldActivity = [...fieldActivity, true]
|
||||||
|
changed()
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteEntry(idx) {
|
function deleteEntry(idx) {
|
||||||
fields.splice(idx, 1)
|
fields.splice(idx, 1)
|
||||||
|
fieldActivity.splice(idx, 1)
|
||||||
|
changed()
|
||||||
|
}
|
||||||
|
|
||||||
|
function changed() {
|
||||||
fields = fields
|
fields = fields
|
||||||
|
const newActivity = {}
|
||||||
|
for (let idx = 0; idx < fields.length; idx++) {
|
||||||
|
const fieldName = fields[idx].name
|
||||||
|
if (fieldName) {
|
||||||
|
newActivity[fieldName] = fieldActivity[idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activity = newActivity
|
||||||
|
dispatch("change", fields)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Builds Objects with Key Value Pairs. Useful for building things like Request Headers. -->
|
<!-- Builds Objects with Key Value Pairs. Useful for building things like Request Headers. -->
|
||||||
<div class="container" class:readOnly>
|
{#if Object.keys(object || {}).length > 0}
|
||||||
|
{#if headings}
|
||||||
|
<div class="container" class:container-active={toggle}>
|
||||||
|
<Label {tooltip}>{keyPlaceholder}</Label>
|
||||||
|
<Label>{valuePlaceholder}</Label>
|
||||||
|
{#if toggle}
|
||||||
|
<Label>Active</Label>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="container" class:container-active={toggle} class:readOnly>
|
||||||
{#each fields as field, idx}
|
{#each fields as field, idx}
|
||||||
<Input placeholder="Key" bind:value={field.name} />
|
<Input
|
||||||
<Input placeholder="Value" bind:value={field.value} />
|
placeholder={keyPlaceholder}
|
||||||
|
bind:value={field.name}
|
||||||
|
on:change={changed}
|
||||||
|
/>
|
||||||
|
{#if options}
|
||||||
|
<Select bind:value={field.value} on:change={changed} {options} />
|
||||||
|
{:else}
|
||||||
|
<Input
|
||||||
|
placeholder={valuePlaceholder}
|
||||||
|
bind:value={field.value}
|
||||||
|
on:change={changed}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if toggle}
|
||||||
|
<Toggle bind:value={fieldActivity[idx]} on:change={changed} />
|
||||||
|
{/if}
|
||||||
{#if !readOnly}
|
{#if !readOnly}
|
||||||
<Icon hoverable name="Close" on:click={() => deleteEntry(idx)} />
|
<Icon hoverable name="Close" on:click={() => deleteEntry(idx)} />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
{#if !readOnly && !noAddButton}
|
{#if !readOnly && !noAddButton}
|
||||||
<div>
|
<div>
|
||||||
<Button secondary thin outline on:click={addEntry}>Add</Button>
|
<ActionButton icon="Add" secondary thin outline on:click={addEntry}
|
||||||
|
>Add{name ? ` ${lowercase(name)}` : ""}</ActionButton
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -47,4 +124,10 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: var(--spacing-m);
|
margin-bottom: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
.container-active {
|
||||||
|
grid-template-columns: 1fr 1fr 50px 20px;
|
||||||
|
}
|
||||||
|
.readOnly {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, Body, Button, Input, Heading, Layout } from "@budibase/bbui"
|
import { Body, Button, Heading, Icon, Input, Layout } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
|
@ -7,83 +7,82 @@
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
|
||||||
export let bindable = true
|
export let bindable = true
|
||||||
export let parameters = []
|
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
export let bindableOptions = []
|
||||||
export let customParams = {}
|
export let customParams = {}
|
||||||
|
|
||||||
function newQueryParameter() {
|
function newQueryBinding() {
|
||||||
parameters = [...parameters, {}]
|
bindings = [...bindings, {}]
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteQueryParameter(idx) {
|
function deleteQueryBinding(idx) {
|
||||||
parameters.splice(idx, 1)
|
bindings.splice(idx, 1)
|
||||||
parameters = parameters
|
bindings = bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is necessary due to the way readable and writable bindings are stored.
|
// This is necessary due to the way readable and writable bindings are stored.
|
||||||
// The readable binding in the UI gets converted to a UUID value that the client understands
|
// The readable binding in the UI gets converted to a UUID value that the client understands
|
||||||
// for parsing, then converted back so we can display it the readable form in the UI
|
// for parsing, then converted back so we can display it the readable form in the UI
|
||||||
function onBindingChange(param, valueToParse) {
|
function onBindingChange(param, valueToParse) {
|
||||||
const parsedBindingValue = readableToRuntimeBinding(bindings, valueToParse)
|
customParams[param] = readableToRuntimeBinding(
|
||||||
customParams[param] = parsedBindingValue
|
bindableOptions,
|
||||||
|
valueToParse
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout paddingX="none" gap="S">
|
<Layout noPadding={bindable} gap="S">
|
||||||
<div class="controls">
|
<div class="controls" class:height={!bindable}>
|
||||||
<Heading size="XS">Parameters</Heading>
|
<Heading size="XS">Bindings</Heading>
|
||||||
{#if !bindable}
|
{#if !bindable}
|
||||||
<Button secondary on:click={newQueryParameter}>Add Param</Button>
|
<Button secondary on:click={newQueryBinding}>Add Binding</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
{#if !bindable}
|
{#if !bindable}
|
||||||
Parameters come in two parts: the parameter name, and a default/fallback
|
Bindings come in two parts: the binding name, and a default/fallback
|
||||||
value.
|
value. These bindings can be used as Handlebars expressions throughout the
|
||||||
|
query.
|
||||||
{:else}
|
{:else}
|
||||||
Enter a value for each parameter. The default values will be used for any
|
Enter a value for each binding. The default values will be used for any
|
||||||
values left blank.
|
values left blank.
|
||||||
{/if}
|
{/if}
|
||||||
</Body>
|
</Body>
|
||||||
<div class="parameters" class:bindable>
|
<div class="bindings" class:bindable>
|
||||||
{#each parameters as parameter, idx}
|
{#each bindings as binding, idx}
|
||||||
<Input
|
<Input
|
||||||
placeholder="Parameter Name"
|
placeholder="Binding Name"
|
||||||
thin
|
thin
|
||||||
disabled={bindable}
|
disabled={bindable}
|
||||||
bind:value={parameter.name}
|
bind:value={binding.name}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Default"
|
placeholder="Default"
|
||||||
thin
|
thin
|
||||||
disabled={bindable}
|
disabled={bindable}
|
||||||
bind:value={parameter.default}
|
bind:value={binding.default}
|
||||||
/>
|
/>
|
||||||
{#if bindable}
|
{#if bindable}
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
title={`Query parameter "${parameter.name}"`}
|
title={`Query binding "${binding.name}"`}
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
thin
|
thin
|
||||||
on:change={evt => onBindingChange(parameter.name, evt.detail)}
|
on:change={evt => onBindingChange(binding.name, evt.detail)}
|
||||||
value={runtimeToReadableBinding(
|
value={runtimeToReadableBinding(
|
||||||
bindings,
|
bindableOptions,
|
||||||
customParams?.[parameter.name]
|
customParams?.[binding.name]
|
||||||
)}
|
)}
|
||||||
{bindings}
|
{bindableOptions}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Icon
|
<Icon hoverable name="Close" on:click={() => deleteQueryBinding(idx)} />
|
||||||
hoverable
|
|
||||||
name="Close"
|
|
||||||
on:click={() => deleteQueryParameter(idx)}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.parameters.bindable {
|
.bindings.bindable {
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,13 +90,16 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameters {
|
.bindings {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 5%;
|
grid-template-columns: 1fr 1fr 5%;
|
||||||
grid-gap: 10px;
|
grid-gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.height {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
|
@ -2,29 +2,42 @@
|
||||||
import { Label, Layout, Input } from "@budibase/bbui"
|
import { Label, Layout, Input } from "@budibase/bbui"
|
||||||
import Editor from "./QueryEditor.svelte"
|
import Editor from "./QueryEditor.svelte"
|
||||||
import KeyValueBuilder from "./KeyValueBuilder.svelte"
|
import KeyValueBuilder from "./KeyValueBuilder.svelte"
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
|
||||||
export let fields = {}
|
export let fields = {}
|
||||||
export let schema
|
export let schema
|
||||||
export let editable
|
export let editable
|
||||||
|
|
||||||
$: schemaKeys = Object.keys(schema.fields)
|
$: schemaKeys = Object.keys(schema?.fields || {})
|
||||||
|
|
||||||
function updateCustomFields({ detail }) {
|
function updateCustomFields({ detail }) {
|
||||||
fields.customData = detail.value
|
fields.customData = detail.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDisplayName(field) {
|
||||||
|
let name
|
||||||
|
if (schema.fields[field]?.display) {
|
||||||
|
name = schema.fields[field]?.display
|
||||||
|
} else {
|
||||||
|
name = field
|
||||||
|
}
|
||||||
|
return capitalise(name)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form on:submit|preventDefault>
|
<form on:submit|preventDefault>
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
{#each schemaKeys as field}
|
{#each schemaKeys as field}
|
||||||
{#if schema.fields[field]?.type === "object"}
|
{#if schema.fields[field]?.type === "object"}
|
||||||
<div>
|
<Label small>{getDisplayName(field)}</Label>
|
||||||
<Label small>{field}</Label>
|
<KeyValueBuilder
|
||||||
<KeyValueBuilder readOnly={!editable} bind:object={fields[field]} />
|
name={getDisplayName(field)}
|
||||||
</div>
|
readOnly={!editable}
|
||||||
|
bind:object={fields[field]}
|
||||||
|
/>
|
||||||
{:else if schema.fields[field]?.type === "json"}
|
{:else if schema.fields[field]?.type === "json"}
|
||||||
<div>
|
<div>
|
||||||
<Label extraSmall grey>{field}</Label>
|
<Label extraSmall grey>{getDisplayName(field)}</Label>
|
||||||
<Editor
|
<Editor
|
||||||
mode="json"
|
mode="json"
|
||||||
on:change={({ detail }) => (fields[field] = detail.value)}
|
on:change={({ detail }) => (fields[field] = detail.value)}
|
||||||
|
@ -34,9 +47,9 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="horizontal">
|
<div class="horizontal">
|
||||||
<Label small>{field}</Label>
|
<Label small>{getDisplayName(field)}</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter {field}"
|
placeholder="Enter {getDisplayName(field)}"
|
||||||
outline
|
outline
|
||||||
disabled={!editable}
|
disabled={!editable}
|
||||||
type={schema.fields[field]?.type}
|
type={schema.fields[field]?.type}
|
||||||
|
|
|
@ -14,37 +14,27 @@
|
||||||
Tab,
|
Tab,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { notifications, Divider } from "@budibase/bbui"
|
import { notifications, Divider } from "@budibase/bbui"
|
||||||
import api from "builderStore/api"
|
|
||||||
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
|
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
|
||||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||||
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
|
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
|
||||||
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
|
||||||
import {
|
import { datasources, integrations, queries } from "stores/backend"
|
||||||
datasources,
|
|
||||||
integrations,
|
|
||||||
queries,
|
|
||||||
roles,
|
|
||||||
permissions,
|
|
||||||
} from "stores/backend"
|
|
||||||
import { capitalise } from "../../helpers"
|
import { capitalise } from "../../helpers"
|
||||||
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
||||||
import { Roles } from "constants/backend"
|
import JSONPreview from "./JSONPreview.svelte"
|
||||||
import { onMount } from "svelte"
|
import { SchemaTypeOptions } from "constants/backend"
|
||||||
|
import KeyValueBuilder from "./KeyValueBuilder.svelte"
|
||||||
|
import { fieldsToSchema, schemaToFields } from "helpers/data/utils"
|
||||||
|
import AccessLevelSelect from "./AccessLevelSelect.svelte"
|
||||||
|
|
||||||
export let query
|
export let query
|
||||||
|
|
||||||
let fields = query.schema ? schemaToFields(query.schema) : []
|
let fields = query?.schema ? schemaToFields(query.schema) : []
|
||||||
let parameters
|
let parameters
|
||||||
let data = []
|
let data = []
|
||||||
let roleId
|
let saveId
|
||||||
const transformerDocs =
|
const transformerDocs =
|
||||||
"https://docs.budibase.com/building-apps/data/transformers"
|
"https://docs.budibase.com/building-apps/data/transformers"
|
||||||
const typeOptions = [
|
|
||||||
{ label: "Text", value: "string" },
|
|
||||||
{ label: "Number", value: "number" },
|
|
||||||
{ label: "Boolean", value: "boolean" },
|
|
||||||
{ label: "Datetime", value: "datetime" },
|
|
||||||
]
|
|
||||||
|
|
||||||
$: datasource = $datasources.list.find(ds => ds._id === query.datasourceId)
|
$: datasource = $datasources.list.find(ds => ds._id === query.datasourceId)
|
||||||
$: query.schema = fieldsToSchema(fields)
|
$: query.schema = fieldsToSchema(fields)
|
||||||
|
@ -60,84 +50,37 @@
|
||||||
query.transformer = "return data"
|
query.transformer = "return data"
|
||||||
}
|
}
|
||||||
|
|
||||||
function newField() {
|
|
||||||
fields = [...fields, {}]
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteField(idx) {
|
|
||||||
fields.splice(idx, 1)
|
|
||||||
fields = fields
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetDependentFields() {
|
function resetDependentFields() {
|
||||||
if (query.fields.extra) {
|
if (query.fields.extra) {
|
||||||
query.fields.extra = {}
|
query.fields.extra = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateRole(role, id = null) {
|
|
||||||
roleId = role
|
|
||||||
if (query?._id || id) {
|
|
||||||
for (let level of ["read", "write"]) {
|
|
||||||
await permissions.save({
|
|
||||||
level,
|
|
||||||
role,
|
|
||||||
resource: query?._id || id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateExtraQuery(extraQueryFields) {
|
function populateExtraQuery(extraQueryFields) {
|
||||||
query.fields.extra = extraQueryFields
|
query.fields.extra = extraQueryFields
|
||||||
}
|
}
|
||||||
|
|
||||||
async function previewQuery() {
|
async function previewQuery() {
|
||||||
try {
|
try {
|
||||||
const response = await api.post(`/api/queries/preview`, {
|
const response = await queries.preview(query)
|
||||||
fields: query.fields,
|
if (response.rows.length === 0) {
|
||||||
queryVerb: query.queryVerb,
|
|
||||||
transformer: query.transformer,
|
|
||||||
parameters: query.parameters.reduce(
|
|
||||||
(acc, next) => ({
|
|
||||||
...acc,
|
|
||||||
[next.name]: next.default,
|
|
||||||
}),
|
|
||||||
{}
|
|
||||||
),
|
|
||||||
datasourceId: datasource._id,
|
|
||||||
})
|
|
||||||
const json = await response.json()
|
|
||||||
|
|
||||||
if (response.status !== 200) throw new Error(json.message)
|
|
||||||
|
|
||||||
data = json.rows || []
|
|
||||||
|
|
||||||
if (data.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
|
||||||
|
fields = response.schema
|
||||||
notifications.success("Query executed successfully.")
|
notifications.success("Query executed successfully.")
|
||||||
|
|
||||||
// Assume all the fields are strings and create a basic schema from the
|
|
||||||
// unique fields returned by the server
|
|
||||||
fields = json.schemaFields.map(field => ({
|
|
||||||
name: field,
|
|
||||||
type: "string",
|
|
||||||
}))
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(`Query Error: ${err.message}`)
|
notifications.error(err)
|
||||||
console.error(err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveQuery() {
|
async function saveQuery() {
|
||||||
try {
|
try {
|
||||||
const { _id } = await queries.save(query.datasourceId, query)
|
const { _id } = await queries.save(query.datasourceId, query)
|
||||||
await updateRole(roleId, _id)
|
saveId = _id
|
||||||
notifications.success(`Query saved successfully.`)
|
notifications.success(`Query saved successfully.`)
|
||||||
$goto(`../${_id}`)
|
$goto(`../${_id}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -145,38 +88,6 @@
|
||||||
notifications.error(`Error creating query. ${err.message}`)
|
notifications.error(`Error creating query. ${err.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function schemaToFields(schema) {
|
|
||||||
return Object.keys(schema).map(key => ({
|
|
||||||
name: key,
|
|
||||||
type: query.schema[key].type,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
function fieldsToSchema(fieldsToConvert) {
|
|
||||||
return fieldsToConvert.reduce(
|
|
||||||
(acc, next) => ({
|
|
||||||
...acc,
|
|
||||||
[next.name]: {
|
|
||||||
name: next.name,
|
|
||||||
type: next.type,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (!query || !query._id) {
|
|
||||||
roleId = Roles.BASIC
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
roleId = (await permissions.forResource(query._id))["read"]
|
|
||||||
} catch (err) {
|
|
||||||
roleId = Roles.BASIC
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout gap="S" noPadding>
|
<Layout gap="S" noPadding>
|
||||||
|
@ -200,14 +111,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-field">
|
<div class="config-field">
|
||||||
<Label>Access level</Label>
|
<AccessLevelSelect {saveId} {query} label="Access Level" />
|
||||||
<Select
|
|
||||||
value={roleId}
|
|
||||||
on:change={e => updateRole(e.detail)}
|
|
||||||
options={$roles}
|
|
||||||
getOptionLabel={x => x.name}
|
|
||||||
getOptionValue={x => x._id}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{#if integrationInfo?.extra && query.queryVerb}
|
{#if integrationInfo?.extra && query.queryVerb}
|
||||||
<ExtraQueryConfig
|
<ExtraQueryConfig
|
||||||
|
@ -216,7 +120,7 @@
|
||||||
config={integrationInfo.extra}
|
config={integrationInfo.extra}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<ParameterBuilder bind:parameters={query.parameters} bindable={false} />
|
<BindingBuilder bind:bindings={query.parameters} bindable={false} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if shouldShowQueryConfig}
|
{#if shouldShowQueryConfig}
|
||||||
|
@ -271,29 +175,15 @@
|
||||||
{#if data}
|
{#if data}
|
||||||
<Tabs selected="JSON">
|
<Tabs selected="JSON">
|
||||||
<Tab title="JSON">
|
<Tab title="JSON">
|
||||||
<pre
|
<JSONPreview data={data[0]} minHeight="120" />
|
||||||
class="preview">
|
|
||||||
<!-- prettier-ignore -->
|
|
||||||
{#if !data[0]}
|
|
||||||
Please run your query to fetch some data.
|
|
||||||
{:else}
|
|
||||||
{JSON.stringify(data[0], undefined, 2)}
|
|
||||||
{/if}
|
|
||||||
</pre>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Schema">
|
<Tab title="Schema">
|
||||||
<Layout gap="S">
|
<KeyValueBuilder
|
||||||
{#each fields as field, idx}
|
bind:object={fields}
|
||||||
<div class="field">
|
name="field"
|
||||||
<Input placeholder="Field Name" bind:value={field.name} />
|
headings
|
||||||
<Select bind:value={field.type} options={typeOptions} />
|
options={SchemaTypeOptions}
|
||||||
<Icon name="bleClose" on:click={() => deleteField(idx)} />
|
/>
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
<div>
|
|
||||||
<Button secondary on:click={newField}>Add Field</Button>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Preview">
|
<Tab title="Preview">
|
||||||
<ExternalDataSourceTable {query} {data} />
|
<ExternalDataSourceTable {query} {data} />
|
||||||
|
@ -322,29 +212,11 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 5%;
|
|
||||||
gap: var(--spacing-l);
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewer {
|
.viewer {
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
width: 640px;
|
width: 640px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview {
|
|
||||||
height: 100%;
|
|
||||||
min-height: 120px;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
background-color: var(--grey-2);
|
|
||||||
padding: var(--spacing-m);
|
|
||||||
border-radius: 8px;
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewer-controls {
|
.viewer-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -154,3 +154,58 @@ export const ALLOWABLE_NUMBER_TYPES = ALLOWABLE_NUMBER_OPTIONS.map(
|
||||||
export const SWITCHABLE_TYPES = ALLOWABLE_NUMBER_TYPES.concat(
|
export const SWITCHABLE_TYPES = ALLOWABLE_NUMBER_TYPES.concat(
|
||||||
ALLOWABLE_STRING_TYPES
|
ALLOWABLE_STRING_TYPES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const IntegrationTypes = {
|
||||||
|
POSTGRES: "POSTGRES",
|
||||||
|
MONGODB: "MONGODB",
|
||||||
|
COUCHDB: "COUCHDB",
|
||||||
|
S3: "S3",
|
||||||
|
MYSQL: "MYSQL",
|
||||||
|
REST: "REST",
|
||||||
|
DYNAMODB: "DYNAMODB",
|
||||||
|
ELASTICSEARCH: "ELASTICSEARCH",
|
||||||
|
SQL_SERVER: "SQL_SERVER",
|
||||||
|
AIRTABLE: "AIRTABLE",
|
||||||
|
ARANGODB: "ARANGODB",
|
||||||
|
ORACLE: "ORACLE",
|
||||||
|
INTERNAL: "INTERNAL",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IntegrationNames = {
|
||||||
|
[IntegrationTypes.POSTGRES]: "PostgreSQL",
|
||||||
|
[IntegrationTypes.MONGODB]: "MongoDB",
|
||||||
|
[IntegrationTypes.COUCHDB]: "CouchDB",
|
||||||
|
[IntegrationTypes.S3]: "S3",
|
||||||
|
[IntegrationTypes.MYSQL]: "MySQL",
|
||||||
|
[IntegrationTypes.REST]: "REST",
|
||||||
|
[IntegrationTypes.DYNAMODB]: "DynamoDB",
|
||||||
|
[IntegrationTypes.ELASTICSEARCH]: "ElasticSearch",
|
||||||
|
[IntegrationTypes.SQL_SERVER]: "SQL Server",
|
||||||
|
[IntegrationTypes.AIRTABLE]: "Airtable",
|
||||||
|
[IntegrationTypes.ARANGODB]: "ArangoDB",
|
||||||
|
[IntegrationTypes.ORACLE]: "Oracle",
|
||||||
|
[IntegrationTypes.INTERNAL]: "Internal",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SchemaTypeOptions = [
|
||||||
|
{ label: "Text", value: "string" },
|
||||||
|
{ label: "Number", value: "number" },
|
||||||
|
{ label: "Boolean", value: "boolean" },
|
||||||
|
{ label: "Datetime", value: "datetime" },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const RawRestBodyTypes = {
|
||||||
|
NONE: "none",
|
||||||
|
FORM: "form",
|
||||||
|
ENCODED: "encoded",
|
||||||
|
JSON: "json",
|
||||||
|
TEXT: "text",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RestBodyTypes = [
|
||||||
|
{ name: "none", value: "none" },
|
||||||
|
{ name: "form-data", value: "form" },
|
||||||
|
{ name: "x-www-form-encoded", value: "encoded" },
|
||||||
|
{ name: "raw (JSON)", value: "json" },
|
||||||
|
{ name: "raw (Text)", value: "text" },
|
||||||
|
]
|
||||||
|
|
|
@ -15,21 +15,6 @@ export const AppStatus = {
|
||||||
DEPLOYED: "published",
|
DEPLOYED: "published",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IntegrationNames = {
|
|
||||||
POSTGRES: "PostgreSQL",
|
|
||||||
MONGODB: "MongoDB",
|
|
||||||
COUCHDB: "CouchDB",
|
|
||||||
S3: "S3",
|
|
||||||
MYSQL: "MySQL",
|
|
||||||
REST: "REST",
|
|
||||||
DYNAMODB: "DynamoDB",
|
|
||||||
ELASTICSEARCH: "ElasticSearch",
|
|
||||||
SQL_SERVER: "SQL Server",
|
|
||||||
AIRTABLE: "Airtable",
|
|
||||||
ARANGODB: "ArangoDB",
|
|
||||||
ORACLE: "Oracle",
|
|
||||||
}
|
|
||||||
|
|
||||||
// fields on the user table that cannot be edited
|
// fields on the user table that cannot be edited
|
||||||
export const UNEDITABLE_USER_FIELDS = [
|
export const UNEDITABLE_USER_FIELDS = [
|
||||||
"email",
|
"email",
|
||||||
|
|
121
packages/builder/src/helpers/data/utils.js
Normal file
121
packages/builder/src/helpers/data/utils.js
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
import { IntegrationTypes } from "constants/backend"
|
||||||
|
|
||||||
|
export function schemaToFields(schema) {
|
||||||
|
const response = {}
|
||||||
|
if (schema && typeof schema === "object") {
|
||||||
|
for (let [field, value] of Object.entries(schema)) {
|
||||||
|
response[field] = value?.type || "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fieldsToSchema(fields) {
|
||||||
|
const response = {}
|
||||||
|
if (fields && typeof fields === "object") {
|
||||||
|
for (let [name, type] of Object.entries(fields)) {
|
||||||
|
response[name] = { name, type }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
export function breakQueryString(qs) {
|
||||||
|
if (!qs) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
if (qs.includes("?")) {
|
||||||
|
qs = qs.split("?")[1]
|
||||||
|
}
|
||||||
|
const params = qs.split("&")
|
||||||
|
let paramObj = {}
|
||||||
|
for (let param of params) {
|
||||||
|
const [key, value] = param.split("=")
|
||||||
|
paramObj[key] = value
|
||||||
|
}
|
||||||
|
return paramObj
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildQueryString(obj) {
|
||||||
|
let str = ""
|
||||||
|
if (obj) {
|
||||||
|
for (let [key, value] of Object.entries(obj)) {
|
||||||
|
if (!key || key === "") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (str !== "") {
|
||||||
|
str += "&"
|
||||||
|
}
|
||||||
|
str += `${key}=${value || ""}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
export function keyValueToQueryParameters(obj) {
|
||||||
|
let array = []
|
||||||
|
if (obj && typeof obj === "object") {
|
||||||
|
for (let [key, value] of Object.entries(obj)) {
|
||||||
|
array.push({ name: key, default: value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryParametersToKeyValue(array) {
|
||||||
|
let obj = {}
|
||||||
|
if (Array.isArray(array)) {
|
||||||
|
for (let param of array) {
|
||||||
|
obj[param.name] = param.default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
export function customQueryIconText(datasource, query) {
|
||||||
|
if (datasource.source !== IntegrationTypes.REST) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch (query.queryVerb) {
|
||||||
|
case "create":
|
||||||
|
return "POST"
|
||||||
|
case "update":
|
||||||
|
return "PUT"
|
||||||
|
case "read":
|
||||||
|
return "GET"
|
||||||
|
case "delete":
|
||||||
|
return "DELETE"
|
||||||
|
case "patch":
|
||||||
|
return "PATCH"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function customQueryIconColor(datasource, query) {
|
||||||
|
if (datasource.source !== IntegrationTypes.REST) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch (query.queryVerb) {
|
||||||
|
case "create":
|
||||||
|
return "#dcc339"
|
||||||
|
case "update":
|
||||||
|
return "#5197ec"
|
||||||
|
case "read":
|
||||||
|
return "#53a761"
|
||||||
|
case "delete":
|
||||||
|
return "#ea7d82"
|
||||||
|
case "patch":
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flipHeaderState(headersActivity) {
|
||||||
|
if (!headersActivity) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
const enabled = {}
|
||||||
|
Object.entries(headersActivity).forEach(([key, value]) => {
|
||||||
|
enabled[key] = !value
|
||||||
|
})
|
||||||
|
return enabled
|
||||||
|
}
|
|
@ -19,6 +19,8 @@ export const pipe = (arg, funcs) => flow(funcs)(arg)
|
||||||
|
|
||||||
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
|
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
|
||||||
|
|
||||||
|
export const lowercase = s => s.substring(0, 1).toLowerCase() + s.substring(1)
|
||||||
|
|
||||||
export const get_name = s => (!s ? "" : last(s.split("/")))
|
export const get_name = s => (!s ? "" : last(s.split("/")))
|
||||||
|
|
||||||
export const get_capitalised_name = name => pipe(name, [get_name, capitalise])
|
export const get_capitalised_name = name => pipe(name, [get_name, capitalise])
|
||||||
|
|
|
@ -6,4 +6,5 @@ export {
|
||||||
capitalise,
|
capitalise,
|
||||||
get_name,
|
get_name,
|
||||||
get_capitalised_name,
|
get_capitalised_name,
|
||||||
|
lowercase,
|
||||||
} from "./helpers"
|
} from "./helpers"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { store, automationStore } from "builderStore"
|
import { store, automationStore } from "builderStore"
|
||||||
import { roles } from "stores/backend"
|
import { roles, flags } from "stores/backend"
|
||||||
import { Icon, ActionGroup, Tabs, Tab, notifications } from "@budibase/bbui"
|
import { Icon, ActionGroup, Tabs, Tab, notifications } from "@budibase/bbui"
|
||||||
import DeployModal from "components/deploy/DeployModal.svelte"
|
import DeployModal from "components/deploy/DeployModal.svelte"
|
||||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||||
|
@ -49,6 +49,7 @@
|
||||||
}
|
}
|
||||||
await automationStore.actions.fetch()
|
await automationStore.actions.fetch()
|
||||||
await roles.fetch()
|
await roles.fetch()
|
||||||
|
await flags.fetch()
|
||||||
return pkg
|
return pkg
|
||||||
} else {
|
} else {
|
||||||
throw new Error(pkg)
|
throw new Error(pkg)
|
||||||
|
|
|
@ -1,13 +1,23 @@
|
||||||
<script>
|
<script>
|
||||||
import { params } from "@roxi/routify"
|
import { params } from "@roxi/routify"
|
||||||
import { queries } from "stores/backend"
|
import { queries, datasources } from "stores/backend"
|
||||||
|
import { IntegrationTypes } from "constants/backend"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
|
let datasourceId
|
||||||
if ($params.query) {
|
if ($params.query) {
|
||||||
const query = $queries.list.find(q => q._id === $params.query)
|
const query = $queries.list.find(q => q._id === $params.query)
|
||||||
if (query) {
|
if (query) {
|
||||||
queries.select(query)
|
queries.select(query)
|
||||||
|
datasourceId = query.datasourceId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const datasource = $datasources.list.find(
|
||||||
|
ds => ds._id === $datasources.selected || ds._id === datasourceId
|
||||||
|
)
|
||||||
|
if (datasource?.source === IntegrationTypes.REST) {
|
||||||
|
$goto(`../rest/${$params.query}`)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { params } from "@roxi/routify"
|
import { params, goto } from "@roxi/routify"
|
||||||
import { database, queries } from "stores/backend"
|
import { database, datasources, queries } from "stores/backend"
|
||||||
import QueryInterface from "components/integration/QueryViewer.svelte"
|
import QueryInterface from "components/integration/QueryViewer.svelte"
|
||||||
|
import { IntegrationTypes } from "constants/backend"
|
||||||
|
|
||||||
|
let selectedQuery, datasource
|
||||||
$: selectedQuery = $queries.list.find(
|
$: selectedQuery = $queries.list.find(
|
||||||
query => query._id === $queries.selected
|
query => query._id === $queries.selected
|
||||||
) || {
|
) || {
|
||||||
|
@ -11,6 +13,14 @@
|
||||||
fields: {},
|
fields: {},
|
||||||
queryVerb: "read",
|
queryVerb: "read",
|
||||||
}
|
}
|
||||||
|
$: datasource = $datasources.list.find(
|
||||||
|
ds => ds._id === $params.selectedDatasource
|
||||||
|
)
|
||||||
|
$: {
|
||||||
|
if (datasource?.source === IntegrationTypes.REST) {
|
||||||
|
$goto(`../rest/${$params.query}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
<script>
|
||||||
|
import { Body } from "@budibase/bbui"
|
||||||
|
import { RawRestBodyTypes } from "constants/backend"
|
||||||
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
|
import CodeMirrorEditor, {
|
||||||
|
EditorModes,
|
||||||
|
} from "components/common/CodeMirrorEditor.svelte"
|
||||||
|
|
||||||
|
const objectTypes = [RawRestBodyTypes.FORM, RawRestBodyTypes.ENCODED]
|
||||||
|
const textTypes = [RawRestBodyTypes.JSON, RawRestBodyTypes.TEXT]
|
||||||
|
|
||||||
|
export let query
|
||||||
|
export let bodyType
|
||||||
|
|
||||||
|
$: checkRequestBody(bodyType)
|
||||||
|
|
||||||
|
function checkRequestBody(type) {
|
||||||
|
if (!bodyType || !query) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const currentType = typeof query?.fields.requestBody
|
||||||
|
if (objectTypes.includes(type) && currentType !== "object") {
|
||||||
|
query.fields.requestBody = {}
|
||||||
|
} else if (textTypes.includes(type) && currentType !== "string") {
|
||||||
|
query.fields.requestBody = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="margin">
|
||||||
|
{#if bodyType === RawRestBodyTypes.NONE}
|
||||||
|
<div class="none">
|
||||||
|
<Body size="S" weight="800">THE REQUEST DOES NOT HAVE A BODY</Body>
|
||||||
|
</div>
|
||||||
|
{:else if objectTypes.includes(bodyType)}
|
||||||
|
<KeyValueBuilder
|
||||||
|
bind:object={query.fields.requestBody}
|
||||||
|
name="param"
|
||||||
|
headings
|
||||||
|
/>
|
||||||
|
{:else if textTypes.includes(bodyType)}
|
||||||
|
<CodeMirrorEditor
|
||||||
|
height={200}
|
||||||
|
mode={bodyType === RawRestBodyTypes.JSON
|
||||||
|
? EditorModes.JSON
|
||||||
|
: EditorModes.Text}
|
||||||
|
value={query.fields.requestBody}
|
||||||
|
resize="vertical"
|
||||||
|
on:change={e => (query.fields.requestBody = e.detail)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.margin {
|
||||||
|
margin-top: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.none {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<script>
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
export let value
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{capitalise(value)}
|
|
@ -6,75 +6,48 @@
|
||||||
Body,
|
Body,
|
||||||
Divider,
|
Divider,
|
||||||
Layout,
|
Layout,
|
||||||
|
notifications,
|
||||||
|
Table,
|
||||||
Modal,
|
Modal,
|
||||||
InlineAlert,
|
|
||||||
ActionButton,
|
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { datasources, integrations, queries, tables } from "stores/backend"
|
import { datasources, integrations, queries, tables } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
|
||||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||||
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
|
import RestExtraConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/RestExtraConfigForm.svelte"
|
||||||
import CreateExternalTableModal from "./modals/CreateExternalTableModal.svelte"
|
import PlusConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/PlusConfigForm.svelte"
|
||||||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||||
import { capitalise } from "helpers"
|
import VerbRenderer from "./_components/VerbRenderer.svelte"
|
||||||
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
import { IntegrationTypes } from "constants/backend"
|
||||||
|
import { isEqual } from "lodash"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
|
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
||||||
let importQueriesModal
|
let importQueriesModal
|
||||||
let relationshipModal
|
|
||||||
let createExternalTableModal
|
let baseDatasource, changed
|
||||||
let selectedFromRelationship, selectedToRelationship
|
const querySchema = {
|
||||||
|
name: {},
|
||||||
|
queryVerb: { displayName: "Method" },
|
||||||
|
}
|
||||||
|
|
||||||
$: datasource = $datasources.list.find(ds => ds._id === $datasources.selected)
|
$: datasource = $datasources.list.find(ds => ds._id === $datasources.selected)
|
||||||
$: integration = datasource && $integrations[datasource.source]
|
$: integration = datasource && $integrations[datasource.source]
|
||||||
$: plusTables = datasource?.plus
|
$: {
|
||||||
? Object.values(datasource.entities || {})
|
if (
|
||||||
: []
|
datasource &&
|
||||||
$: relationships = getRelationships(plusTables)
|
(!baseDatasource || baseDatasource.source !== datasource.source)
|
||||||
$: schemaError = $datasources.schemaError
|
) {
|
||||||
|
baseDatasource = cloneDeep(datasource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$: queryList = $queries.list.filter(
|
||||||
|
query => query.datasourceId === datasource?._id
|
||||||
|
)
|
||||||
|
$: hasChanged(baseDatasource, datasource)
|
||||||
|
|
||||||
function getRelationships(tables) {
|
function hasChanged(base, ds) {
|
||||||
if (!tables || !Array.isArray(tables)) {
|
if (base && ds) {
|
||||||
return {}
|
changed = !isEqual(base, ds)
|
||||||
}
|
}
|
||||||
let pairs = {}
|
|
||||||
for (let table of tables) {
|
|
||||||
for (let column of Object.values(table.schema)) {
|
|
||||||
if (column.type !== "link") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// these relationships have an id to pair them to each other
|
|
||||||
// one has a main for the from side
|
|
||||||
const key = column.main ? "from" : "to"
|
|
||||||
pairs[column._id] = {
|
|
||||||
...pairs[column._id],
|
|
||||||
[key]: column,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pairs
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildRelationshipDisplayString(fromCol, toCol) {
|
|
||||||
function getTableName(tableId) {
|
|
||||||
if (!tableId || typeof tableId !== "string") {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return plusTables.find(table => table._id === tableId)?.name || "Unknown"
|
|
||||||
}
|
|
||||||
if (!toCol || !fromCol) {
|
|
||||||
return "Cannot build name"
|
|
||||||
}
|
|
||||||
const fromTableName = getTableName(toCol.tableId)
|
|
||||||
const toTableName = getTableName(fromCol.tableId)
|
|
||||||
const throughTableName = getTableName(fromCol.through)
|
|
||||||
|
|
||||||
let displayString
|
|
||||||
if (throughTableName) {
|
|
||||||
displayString = `${fromTableName} through ${throughTableName} → ${toTableName}`
|
|
||||||
} else {
|
|
||||||
displayString = `${fromTableName} → ${toTableName}`
|
|
||||||
}
|
|
||||||
return displayString
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveDatasource() {
|
async function saveDatasource() {
|
||||||
|
@ -86,53 +59,18 @@
|
||||||
}
|
}
|
||||||
await datasources.fetch()
|
await datasources.fetch()
|
||||||
notifications.success(`Datasource ${name} updated successfully.`)
|
notifications.success(`Datasource ${name} updated successfully.`)
|
||||||
|
baseDatasource = cloneDeep(datasource)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(`Error saving datasource: ${err}`)
|
notifications.error(`Error saving datasource: ${err}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateDatasourceSchema() {
|
|
||||||
try {
|
|
||||||
await datasources.updateSchema(datasource)
|
|
||||||
notifications.success(`Datasource ${name} tables updated successfully.`)
|
|
||||||
await tables.fetch()
|
|
||||||
} catch (err) {
|
|
||||||
notifications.error(`Error updating datasource schema: ${err}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onClickQuery(query) {
|
function onClickQuery(query) {
|
||||||
queries.select(query)
|
queries.select(query)
|
||||||
$goto(`./${query._id}`)
|
$goto(`./${query._id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClickTable(table) {
|
|
||||||
tables.select(table)
|
|
||||||
$goto(`../../table/${table._id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function openRelationshipModal(fromRelationship, toRelationship) {
|
|
||||||
selectedFromRelationship = fromRelationship || {}
|
|
||||||
selectedToRelationship = toRelationship || {}
|
|
||||||
relationshipModal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
function createNewTable() {
|
|
||||||
createExternalTableModal.show()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={relationshipModal}>
|
|
||||||
<CreateEditRelationship
|
|
||||||
{datasource}
|
|
||||||
save={saveDatasource}
|
|
||||||
close={relationshipModal.hide}
|
|
||||||
{plusTables}
|
|
||||||
fromRelationship={selectedFromRelationship}
|
|
||||||
toRelationship={selectedToRelationship}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal bind:this={importQueriesModal}>
|
<Modal bind:this={importQueriesModal}>
|
||||||
{#if datasource.source === "REST"}
|
{#if datasource.source === "REST"}
|
||||||
<ImportRestQueriesModal
|
<ImportRestQueriesModal
|
||||||
|
@ -142,10 +80,6 @@
|
||||||
{/if}
|
{/if}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={createExternalTableModal}>
|
|
||||||
<CreateExternalTableModal {datasource} />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{#if datasource && integration}
|
{#if datasource && integration}
|
||||||
<section>
|
<section>
|
||||||
<Layout>
|
<Layout>
|
||||||
|
@ -156,125 +90,58 @@
|
||||||
height="26"
|
height="26"
|
||||||
width="26"
|
width="26"
|
||||||
/>
|
/>
|
||||||
<Heading size="M">{datasource.name}</Heading>
|
<Heading size="M">{baseDatasource.name}</Heading>
|
||||||
</header>
|
</header>
|
||||||
<Body size="M">{integration.description}</Body>
|
<Body size="M">{integration.description}</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider />
|
<Divider size="S" />
|
||||||
<div class="container">
|
|
||||||
<div class="config-header">
|
<div class="config-header">
|
||||||
<Heading size="S">Configuration</Heading>
|
<Heading size="S">Configuration</Heading>
|
||||||
<Button secondary on:click={saveDatasource}>Save</Button>
|
<Button disabled={!changed} cta on:click={saveDatasource}>Save</Button>
|
||||||
</div>
|
</div>
|
||||||
<Body size="S">
|
|
||||||
Connect your database to Budibase using the config below.
|
|
||||||
</Body>
|
|
||||||
<IntegrationConfigForm
|
<IntegrationConfigForm
|
||||||
|
on:change={hasChanged}
|
||||||
schema={integration.datasource}
|
schema={integration.datasource}
|
||||||
integration={datasource.config}
|
bind:datasource
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
{#if datasource.plus}
|
{#if datasource.plus}
|
||||||
<Divider />
|
<PlusConfigForm bind:datasource save={saveDatasource} />
|
||||||
<div class="query-header">
|
|
||||||
<Heading size="S">Tables</Heading>
|
|
||||||
<div class="table-buttons">
|
|
||||||
<div>
|
|
||||||
<ActionButton
|
|
||||||
size="S"
|
|
||||||
quiet
|
|
||||||
icon="DataRefresh"
|
|
||||||
on:click={updateDatasourceSchema}
|
|
||||||
>
|
|
||||||
Fetch tables from database
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Body>
|
|
||||||
This datasource can determine tables automatically. Budibase can fetch
|
|
||||||
your tables directly from the database and you can use them without
|
|
||||||
having to write any queries at all.
|
|
||||||
</Body>
|
|
||||||
{#if schemaError}
|
|
||||||
<InlineAlert
|
|
||||||
type="error"
|
|
||||||
header="Error fetching tables"
|
|
||||||
message={schemaError}
|
|
||||||
onConfirm={datasources.removeSchemaError}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
<div class="query-list">
|
<Divider size="S" />
|
||||||
{#each plusTables as table}
|
|
||||||
<div class="query-list-item" on:click={() => onClickTable(table)}>
|
|
||||||
<p class="query-name">{table.name}</p>
|
|
||||||
<p>Primary Key: {table.primary}</p>
|
|
||||||
<p>→</p>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
<div class="add-table">
|
|
||||||
<Button cta on:click={createNewTable}>Create new table</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if plusTables?.length !== 0}
|
|
||||||
<Divider />
|
|
||||||
<div class="query-header">
|
|
||||||
<Heading size="S">Relationships</Heading>
|
|
||||||
<ActionButton
|
|
||||||
icon="DataCorrelated"
|
|
||||||
primary
|
|
||||||
size="S"
|
|
||||||
quiet
|
|
||||||
on:click={openRelationshipModal}
|
|
||||||
>
|
|
||||||
Define existing relationship
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
<Body>
|
|
||||||
Tell budibase how your tables are related to get even more smart
|
|
||||||
features.
|
|
||||||
</Body>
|
|
||||||
{/if}
|
|
||||||
<div class="query-list">
|
|
||||||
{#each Object.values(relationships) as relationship}
|
|
||||||
<div
|
|
||||||
class="query-list-item"
|
|
||||||
on:click={() =>
|
|
||||||
openRelationshipModal(relationship.from, relationship.to)}
|
|
||||||
>
|
|
||||||
<p class="query-name">
|
|
||||||
{buildRelationshipDisplayString(
|
|
||||||
relationship.from,
|
|
||||||
relationship.to
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p>{relationship.from?.name} to {relationship.to?.name}</p>
|
|
||||||
<p>→</p>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<Divider />
|
|
||||||
<div class="query-header">
|
<div class="query-header">
|
||||||
<Heading size="S">Queries</Heading>
|
<Heading size="S">Queries</Heading>
|
||||||
<div class="query-buttons">
|
<div class="query-buttons">
|
||||||
{#if datasource.source === "REST"}
|
{#if datasource?.source === IntegrationTypes.REST}
|
||||||
<Button secondary on:click={() => importQueriesModal.show()}
|
<Button secondary on:click={() => importQueriesModal.show()}
|
||||||
>Import</Button
|
>Import</Button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
<Button secondary on:click={() => $goto("./new")}>Add Query</Button>
|
<Button cta icon="Add" on:click={() => $goto("./new")}
|
||||||
|
>Add query
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Body size="S">
|
||||||
|
To build an app using a datasource, you must first query the data. A
|
||||||
|
query is a request for data or information from a datasource, for
|
||||||
|
example a database table.
|
||||||
|
</Body>
|
||||||
|
{#if queryList && queryList.length > 0}
|
||||||
<div class="query-list">
|
<div class="query-list">
|
||||||
{#each $queries.list.filter(query => query.datasourceId === datasource._id) as query}
|
<Table
|
||||||
<div class="query-list-item" on:click={() => onClickQuery(query)}>
|
on:click={({ detail }) => onClickQuery(detail)}
|
||||||
<p class="query-name">{query.name}</p>
|
schema={querySchema}
|
||||||
<p>{capitalise(query.queryVerb)}</p>
|
data={queryList}
|
||||||
<p>→</p>
|
allowEditColumns={false}
|
||||||
</div>
|
allowEditRows={false}
|
||||||
{/each}
|
allowSelectRows={false}
|
||||||
|
customRenderers={[{ column: "queryVerb", component: VerbRenderer }]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if datasource?.source === IntegrationTypes.REST}
|
||||||
|
<RestExtraConfigForm bind:datasource on:change={hasChanged} />
|
||||||
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -297,18 +164,11 @@
|
||||||
margin: 0 0 var(--spacing-xs) 0;
|
margin: 0 0 var(--spacing-xs) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: var(--border-radius-m);
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.query-header {
|
.query-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 0 0 var(--spacing-s) 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-buttons {
|
.query-buttons {
|
||||||
|
@ -321,48 +181,4 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-list-item {
|
|
||||||
border-radius: var(--border-radius-m);
|
|
||||||
background: var(--background);
|
|
||||||
border: var(--border-dark);
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 2fr 0.75fr 20px;
|
|
||||||
align-items: center;
|
|
||||||
padding-left: var(--spacing-m);
|
|
||||||
padding-right: var(--spacing-m);
|
|
||||||
gap: var(--layout-xs);
|
|
||||||
transition: 200ms background ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.query-list-item:hover {
|
|
||||||
background: var(--grey-1);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
color: var(--grey-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.query-name {
|
|
||||||
color: var(--ink);
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-buttons {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-buttons div {
|
|
||||||
grid-column-end: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-table {
|
|
||||||
margin-top: var(--spacing-m);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script>
|
||||||
|
import { params } from "@roxi/routify"
|
||||||
|
import { queries } from "stores/backend"
|
||||||
|
|
||||||
|
if ($params.query) {
|
||||||
|
const query = $queries.list.find(q => q._id === $params.query)
|
||||||
|
if (query) {
|
||||||
|
queries.select(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
|
@ -0,0 +1,403 @@
|
||||||
|
<script>
|
||||||
|
import { params } from "@roxi/routify"
|
||||||
|
import { datasources, integrations, queries, flags } from "stores/backend"
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
Banner,
|
||||||
|
Divider,
|
||||||
|
Button,
|
||||||
|
Heading,
|
||||||
|
RadioGroup,
|
||||||
|
Label,
|
||||||
|
Body,
|
||||||
|
TextArea,
|
||||||
|
Table,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
|
import EditableLabel from "components/common/inputs/EditableLabel.svelte"
|
||||||
|
import CodeMirrorEditor, {
|
||||||
|
EditorModes,
|
||||||
|
} from "components/common/CodeMirrorEditor.svelte"
|
||||||
|
import RestBodyInput from "../../_components/RestBodyInput.svelte"
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import {
|
||||||
|
fieldsToSchema,
|
||||||
|
schemaToFields,
|
||||||
|
breakQueryString,
|
||||||
|
buildQueryString,
|
||||||
|
keyValueToQueryParameters,
|
||||||
|
queryParametersToKeyValue,
|
||||||
|
flipHeaderState,
|
||||||
|
} from "helpers/data/utils"
|
||||||
|
import {
|
||||||
|
RestBodyTypes as bodyTypes,
|
||||||
|
SchemaTypeOptions,
|
||||||
|
} from "constants/backend"
|
||||||
|
import JSONPreview from "components/integration/JSONPreview.svelte"
|
||||||
|
import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte"
|
||||||
|
import Placeholder from "assets/bb-spaceship.svg"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
|
let query, datasource
|
||||||
|
let breakQs = {},
|
||||||
|
bindings = {}
|
||||||
|
let url = ""
|
||||||
|
let saveId
|
||||||
|
let response, schema, enabledHeaders
|
||||||
|
let datasourceType, integrationInfo, queryConfig, responseSuccess
|
||||||
|
|
||||||
|
$: datasourceType = datasource?.source
|
||||||
|
$: integrationInfo = $integrations[datasourceType]
|
||||||
|
$: queryConfig = integrationInfo?.query
|
||||||
|
$: url = buildUrl(url, breakQs)
|
||||||
|
$: checkQueryName(url)
|
||||||
|
$: responseSuccess =
|
||||||
|
response?.info?.code >= 200 && response?.info?.code <= 206
|
||||||
|
|
||||||
|
function getSelectedQuery() {
|
||||||
|
return cloneDeep(
|
||||||
|
$queries.list.find(q => q._id === $queries.selected) || {
|
||||||
|
datasourceId: $params.selectedDatasource,
|
||||||
|
parameters: [],
|
||||||
|
fields: {},
|
||||||
|
queryVerb: "read",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkQueryName(inputUrl = null) {
|
||||||
|
if (query && (!query.name || query.flags.urlName)) {
|
||||||
|
query.flags.urlName = true
|
||||||
|
query.name = url || inputUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUrl(base, qsObj) {
|
||||||
|
if (!base) {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
const qs = buildQueryString(qsObj)
|
||||||
|
let newUrl = base
|
||||||
|
if (base.includes("?")) {
|
||||||
|
newUrl = base.split("?")[0]
|
||||||
|
}
|
||||||
|
return qs.length > 0 ? `${newUrl}?${qs}` : newUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
function learnMoreBanner() {
|
||||||
|
window.open("https://docs.budibase.com/building-apps/data/transformers")
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQuery() {
|
||||||
|
const newQuery = { ...query }
|
||||||
|
const queryString = buildQueryString(breakQs)
|
||||||
|
newQuery.fields.path = url.split("?")[0]
|
||||||
|
newQuery.fields.queryString = queryString
|
||||||
|
newQuery.fields.disabledHeaders = flipHeaderState(enabledHeaders)
|
||||||
|
newQuery.schema = fieldsToSchema(schema)
|
||||||
|
newQuery.parameters = keyValueToQueryParameters(bindings)
|
||||||
|
return newQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveQuery() {
|
||||||
|
const toSave = buildQuery()
|
||||||
|
try {
|
||||||
|
const { _id } = await queries.save(toSave.datasourceId, toSave)
|
||||||
|
saveId = _id
|
||||||
|
query = getSelectedQuery()
|
||||||
|
notifications.success(`Request saved successfully.`)
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Error creating query. ${err.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runQuery() {
|
||||||
|
try {
|
||||||
|
response = await queries.preview(buildQuery(query))
|
||||||
|
if (response.rows.length === 0) {
|
||||||
|
notifications.info("Request did not return any data.")
|
||||||
|
} else {
|
||||||
|
response.info = response.info || { code: 200 }
|
||||||
|
schema = response.schema
|
||||||
|
notifications.success("Request sent successfully.")
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
query = getSelectedQuery()
|
||||||
|
datasource = $datasources.list.find(ds => ds._id === query?.datasourceId)
|
||||||
|
const datasourceUrl = datasource?.config.url
|
||||||
|
const qs = query?.fields.queryString
|
||||||
|
breakQs = breakQueryString(qs)
|
||||||
|
if (datasourceUrl && !query.fields.path?.startsWith(datasourceUrl)) {
|
||||||
|
const path = query.fields.path
|
||||||
|
query.fields.path = `${datasource.config.url}/${path ? path : ""}`
|
||||||
|
}
|
||||||
|
url = buildUrl(query.fields.path, breakQs)
|
||||||
|
schema = schemaToFields(query.schema)
|
||||||
|
bindings = queryParametersToKeyValue(query.parameters)
|
||||||
|
if (!query.fields.disabledHeaders) {
|
||||||
|
query.fields.disabledHeaders = {}
|
||||||
|
}
|
||||||
|
// make sure the disabled headers are set (migration)
|
||||||
|
for (let header of Object.keys(query.fields.headers)) {
|
||||||
|
if (!query.fields.disabledHeaders[header]) {
|
||||||
|
query.fields.disabledHeaders[header] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enabledHeaders = flipHeaderState(query.fields.disabledHeaders)
|
||||||
|
if (query && !query.transformer) {
|
||||||
|
query.transformer = "return data"
|
||||||
|
}
|
||||||
|
if (query && !query.flags) {
|
||||||
|
query.flags = {
|
||||||
|
urlName: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (query && !query.fields.bodyType) {
|
||||||
|
query.fields.bodyType = "none"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if query && queryConfig}
|
||||||
|
<div class="inner">
|
||||||
|
<div class="top">
|
||||||
|
<Layout gap="S">
|
||||||
|
<div class="top-bar">
|
||||||
|
<EditableLabel
|
||||||
|
type="heading"
|
||||||
|
bind:value={query.name}
|
||||||
|
defaultValue="Untitled"
|
||||||
|
on:change={() => (query.flags.urlName = false)}
|
||||||
|
/>
|
||||||
|
<div class="access">
|
||||||
|
<Label>Access level</Label>
|
||||||
|
<AccessLevelSelect {query} {saveId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="url-block">
|
||||||
|
<div class="verb">
|
||||||
|
<Select
|
||||||
|
bind:value={query.queryVerb}
|
||||||
|
on:change={() => {}}
|
||||||
|
options={Object.keys(queryConfig)}
|
||||||
|
getOptionLabel={verb =>
|
||||||
|
queryConfig[verb]?.displayName || capitalise(verb)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="url">
|
||||||
|
<Input bind:value={url} placeholder="http://www.api.com/endpoint" />
|
||||||
|
</div>
|
||||||
|
<Button cta disabled={!url} on:click={runQuery}>Send</Button>
|
||||||
|
</div>
|
||||||
|
<Tabs selected="Bindings" quiet noPadding noHorizPadding>
|
||||||
|
<Tab title="Bindings">
|
||||||
|
<KeyValueBuilder
|
||||||
|
bind:object={bindings}
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Params">
|
||||||
|
<KeyValueBuilder bind:object={breakQs} name="param" headings />
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Headers">
|
||||||
|
<KeyValueBuilder
|
||||||
|
bind:object={query.fields.headers}
|
||||||
|
bind:activity={enabledHeaders}
|
||||||
|
toggle
|
||||||
|
name="header"
|
||||||
|
headings
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Body">
|
||||||
|
<RadioGroup
|
||||||
|
bind:value={query.fields.bodyType}
|
||||||
|
options={bodyTypes}
|
||||||
|
direction="horizontal"
|
||||||
|
getOptionLabel={option => option.name}
|
||||||
|
getOptionValue={option => option.value}
|
||||||
|
/>
|
||||||
|
<RestBodyInput bind:bodyType={query.fields.bodyType} bind:query />
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Transformer">
|
||||||
|
<Layout noPadding>
|
||||||
|
{#if !$flags.queryTransformerBanner}
|
||||||
|
<Banner
|
||||||
|
extraButtonText="Learn more"
|
||||||
|
extraButtonAction={learnMoreBanner}
|
||||||
|
on:change={() =>
|
||||||
|
flags.updateFlag("queryTransformerBanner", true)}
|
||||||
|
>
|
||||||
|
Add a JavaScript function to transform the query result.
|
||||||
|
</Banner>
|
||||||
|
{/if}
|
||||||
|
<CodeMirrorEditor
|
||||||
|
height={200}
|
||||||
|
mode={EditorModes.JSON}
|
||||||
|
value={query.transformer}
|
||||||
|
resize="vertical"
|
||||||
|
on:change={e => (query.transformer = e.detail)}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
<div class="bottom">
|
||||||
|
<Layout paddingY="S" gap="S">
|
||||||
|
<Divider size="S" />
|
||||||
|
{#if !response && Object.keys(schema).length === 0}
|
||||||
|
<Heading size="M">Response</Heading>
|
||||||
|
<div class="placeholder">
|
||||||
|
<div class="placeholder-internal">
|
||||||
|
<img alt="placeholder" src={Placeholder} />
|
||||||
|
<Body size="XS" textAlign="center"
|
||||||
|
>{"enter a url in the textbox above and click send to get a response".toUpperCase()}</Body
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Tabs
|
||||||
|
selected={!response ? "Schema" : "JSON"}
|
||||||
|
quiet
|
||||||
|
noPadding
|
||||||
|
noHorizPadding
|
||||||
|
>
|
||||||
|
{#if response}
|
||||||
|
<Tab title="JSON">
|
||||||
|
<div>
|
||||||
|
<JSONPreview height="300" data={response.rows[0]} />
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
{/if}
|
||||||
|
{#if schema || response}
|
||||||
|
<Tab title="Schema">
|
||||||
|
<KeyValueBuilder
|
||||||
|
bind:object={schema}
|
||||||
|
name="schema"
|
||||||
|
headings
|
||||||
|
options={SchemaTypeOptions}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
{/if}
|
||||||
|
{#if response}
|
||||||
|
<Tab title="Raw">
|
||||||
|
<TextArea disabled value={response.extra?.raw} height="300" />
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Headers">
|
||||||
|
<KeyValueBuilder object={response.extra?.headers} readOnly />
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Preview">
|
||||||
|
<div class="table">
|
||||||
|
{#if response}
|
||||||
|
<Table
|
||||||
|
schema={response?.schema}
|
||||||
|
data={response?.rows}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
<div class="stats">
|
||||||
|
<Label size="L">
|
||||||
|
Status: <span class={responseSuccess ? "green" : "red"}
|
||||||
|
>{response?.info.code}</span
|
||||||
|
>
|
||||||
|
</Label>
|
||||||
|
<Label size="L">
|
||||||
|
Time: <span class={responseSuccess ? "green" : "red"}
|
||||||
|
>{response?.info.time}</span
|
||||||
|
>
|
||||||
|
</Label>
|
||||||
|
<Label size="L">
|
||||||
|
Size: <span class={responseSuccess ? "green" : "red"}
|
||||||
|
>{response?.info.size}</span
|
||||||
|
>
|
||||||
|
</Label>
|
||||||
|
<Button disabled={!responseSuccess} cta on:click={saveQuery}
|
||||||
|
>Save query</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Tabs>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.inner {
|
||||||
|
width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.table {
|
||||||
|
width: 960px;
|
||||||
|
}
|
||||||
|
.url-block {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.verb {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.url {
|
||||||
|
flex: 4;
|
||||||
|
}
|
||||||
|
.top {
|
||||||
|
min-height: 50%;
|
||||||
|
}
|
||||||
|
.bottom {
|
||||||
|
padding-bottom: 50px;
|
||||||
|
}
|
||||||
|
.stats {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
margin-left: auto !important;
|
||||||
|
margin-right: 0;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.green {
|
||||||
|
color: #53a761;
|
||||||
|
}
|
||||||
|
.red {
|
||||||
|
color: #ea7d82;
|
||||||
|
}
|
||||||
|
.top-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.access {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.placeholder-internal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 200px;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.placeholder {
|
||||||
|
display: flex;
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
37
packages/builder/src/stores/backend/flags.js
Normal file
37
packages/builder/src/stores/backend/flags.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
export function createFlagsStore() {
|
||||||
|
const { subscribe, set } = writable({})
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
fetch: async () => {
|
||||||
|
const { doc, response } = await getFlags()
|
||||||
|
set(doc)
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
updateFlag: async (flag, value) => {
|
||||||
|
const response = await api.post("/api/users/flags", {
|
||||||
|
flag,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
if (response.status === 200) {
|
||||||
|
const { doc } = await getFlags()
|
||||||
|
set(doc)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFlags() {
|
||||||
|
const response = await api.get("/api/users/flags")
|
||||||
|
let doc = {}
|
||||||
|
if (response.status === 200) {
|
||||||
|
doc = await response.json()
|
||||||
|
}
|
||||||
|
return { doc, response }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const flags = createFlagsStore()
|
|
@ -7,3 +7,4 @@ export { roles } from "./roles"
|
||||||
export { datasources } from "./datasources"
|
export { datasources } from "./datasources"
|
||||||
export { integrations } from "./integrations"
|
export { integrations } from "./integrations"
|
||||||
export { queries } from "./queries"
|
export { queries } from "./queries"
|
||||||
|
export { flags } from "./flags"
|
||||||
|
|
|
@ -78,6 +78,35 @@ export function createQueriesStore() {
|
||||||
unselect: () => {
|
unselect: () => {
|
||||||
update(state => ({ ...state, selected: null }))
|
update(state => ({ ...state, selected: null }))
|
||||||
},
|
},
|
||||||
|
preview: async query => {
|
||||||
|
const response = await api.post("/api/queries/preview", {
|
||||||
|
fields: query.fields,
|
||||||
|
queryVerb: query.queryVerb,
|
||||||
|
transformer: query.transformer,
|
||||||
|
parameters: query.parameters.reduce(
|
||||||
|
(acc, next) => ({
|
||||||
|
...acc,
|
||||||
|
[next.name]: next.default,
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
datasourceId: query.datasourceId,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
const error = await response.text()
|
||||||
|
throw `Query error: ${error}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await response.json()
|
||||||
|
// Assume all the fields are strings and create a basic schema from the
|
||||||
|
// unique fields returned by the server
|
||||||
|
const schema = {}
|
||||||
|
for (let field of json.schemaFields) {
|
||||||
|
schema[field] = "string"
|
||||||
|
}
|
||||||
|
return { ...json, schema, rows: json.rows || [] }
|
||||||
|
},
|
||||||
delete: async query => {
|
delete: async query => {
|
||||||
const response = await api.delete(
|
const response = await api.delete(
|
||||||
`/api/queries/${query._id}/${query._rev}`
|
`/api/queries/${query._id}/${query._rev}`
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "1.0.18",
|
"version": "1.0.19-alpha.0",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "1.0.18",
|
"version": "1.0.19-alpha.0",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,9 +19,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.18",
|
"@budibase/bbui": "^1.0.19-alpha.0",
|
||||||
"@budibase/standard-components": "^0.9.139",
|
"@budibase/standard-components": "^0.9.139",
|
||||||
"@budibase/string-templates": "^1.0.18",
|
"@budibase/string-templates": "^1.0.19-alpha.0",
|
||||||
"regexparam": "^1.3.0",
|
"regexparam": "^1.3.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"svelte-spa-router": "^3.0.5"
|
"svelte-spa-router": "^3.0.5"
|
||||||
|
|
|
@ -6,6 +6,9 @@ module FetchMock {
|
||||||
return {
|
return {
|
||||||
status,
|
status,
|
||||||
headers: {
|
headers: {
|
||||||
|
raw: () => {
|
||||||
|
return { "content-type": ["application/json"] }
|
||||||
|
},
|
||||||
get: () => {
|
get: () => {
|
||||||
return ["application/json"]
|
return ["application/json"]
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "1.0.18",
|
"version": "1.0.19-alpha.0",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -70,9 +70,9 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "^10.0.3",
|
"@apidevtools/swagger-parser": "^10.0.3",
|
||||||
"@budibase/auth": "^1.0.18",
|
"@budibase/auth": "^1.0.19-alpha.0",
|
||||||
"@budibase/client": "^1.0.18",
|
"@budibase/client": "^1.0.19-alpha.0",
|
||||||
"@budibase/string-templates": "^1.0.18",
|
"@budibase/string-templates": "^1.0.19-alpha.0",
|
||||||
"@bull-board/api": "^3.7.0",
|
"@bull-board/api": "^3.7.0",
|
||||||
"@bull-board/koa": "^3.7.0",
|
"@bull-board/koa": "^3.7.0",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
|
import { Query, QueryParameter } from "../../../../../../definitions/datasource"
|
||||||
import { Query, QueryParameter } from "../../../../../../definitions/common"
|
|
||||||
|
|
||||||
export interface ImportInfo {
|
export interface ImportInfo {
|
||||||
url: string
|
url: string
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ImportInfo } from "./base"
|
import { ImportInfo } from "./base"
|
||||||
import { Query, QueryParameter } from "../../../../../definitions/common"
|
import { Query, QueryParameter } from "../../../../../definitions/datasource"
|
||||||
import { OpenAPIV2 } from "openapi-types"
|
import { OpenAPIV2 } from "openapi-types"
|
||||||
import { OpenAPISource } from "./base/openapi"
|
import { OpenAPISource } from "./base/openapi"
|
||||||
|
|
||||||
|
|
|
@ -123,7 +123,7 @@ async function enrichQueryFields(fields, parameters = {}) {
|
||||||
enrichedQuery.requestBody
|
enrichedQuery.requestBody
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw { message: `JSON Invalid - error: ${err}` }
|
// no json found, ignore
|
||||||
}
|
}
|
||||||
delete enrichedQuery.customData
|
delete enrichedQuery.customData
|
||||||
}
|
}
|
||||||
|
@ -151,7 +151,7 @@ exports.preview = async function (ctx) {
|
||||||
const enrichedQuery = await enrichQueryFields(fields, parameters)
|
const enrichedQuery = await enrichQueryFields(fields, parameters)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { rows, keys } = await Runner.run({
|
const { rows, keys, info, extra } = await Runner.run({
|
||||||
datasource,
|
datasource,
|
||||||
queryVerb,
|
queryVerb,
|
||||||
query: enrichedQuery,
|
query: enrichedQuery,
|
||||||
|
@ -161,6 +161,8 @@ exports.preview = async function (ctx) {
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
rows,
|
rows,
|
||||||
schemaFields: [...new Set(keys)],
|
schemaFields: [...new Set(keys)],
|
||||||
|
info,
|
||||||
|
extra,
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.throw(400, err)
|
ctx.throw(400, err)
|
||||||
|
|
|
@ -19,6 +19,7 @@ exports.queryValidation = () => {
|
||||||
extra: Joi.object().optional(),
|
extra: Joi.object().optional(),
|
||||||
schema: Joi.object({}).required().unknown(true),
|
schema: Joi.object({}).required().unknown(true),
|
||||||
transformer: Joi.string().optional(),
|
transformer: Joi.string().optional(),
|
||||||
|
flags: Joi.object().optional(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ const CouchDB = require("../../db")
|
||||||
const {
|
const {
|
||||||
generateUserMetadataID,
|
generateUserMetadataID,
|
||||||
getUserMetadataParams,
|
getUserMetadataParams,
|
||||||
|
generateUserFlagID,
|
||||||
} = require("../../db/utils")
|
} = require("../../db/utils")
|
||||||
const { InternalTables } = require("../../db/utils")
|
const { InternalTables } = require("../../db/utils")
|
||||||
const { getGlobalUsers, getRawGlobalUser } = require("../../utilities/global")
|
const { getGlobalUsers, getRawGlobalUser } = require("../../utilities/global")
|
||||||
|
@ -195,3 +196,35 @@ exports.destroyMetadata = async function (ctx) {
|
||||||
exports.findMetadata = async function (ctx) {
|
exports.findMetadata = async function (ctx) {
|
||||||
ctx.body = await getFullUser(ctx, ctx.params.id)
|
ctx.body = await getFullUser(ctx, ctx.params.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.setFlag = async function (ctx) {
|
||||||
|
const userId = ctx.user._id
|
||||||
|
const { flag, value } = ctx.request.body
|
||||||
|
if (!flag) {
|
||||||
|
ctx.throw(400, "Must supply a 'flag' field in request body.")
|
||||||
|
}
|
||||||
|
const flagDocId = generateUserFlagID(userId)
|
||||||
|
const db = new CouchDB(ctx.appId)
|
||||||
|
let doc
|
||||||
|
try {
|
||||||
|
doc = await db.get(flagDocId)
|
||||||
|
} catch (err) {
|
||||||
|
doc = { _id: flagDocId }
|
||||||
|
}
|
||||||
|
doc[flag] = value || true
|
||||||
|
await db.put(doc)
|
||||||
|
ctx.body = { message: "Flag set successfully" }
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getFlags = async function (ctx) {
|
||||||
|
const userId = ctx.user._id
|
||||||
|
const docId = generateUserFlagID(userId)
|
||||||
|
const db = new CouchDB(ctx.appId)
|
||||||
|
let doc
|
||||||
|
try {
|
||||||
|
doc = await db.get(docId)
|
||||||
|
} catch (err) {
|
||||||
|
doc = { _id: docId }
|
||||||
|
}
|
||||||
|
ctx.body = doc
|
||||||
|
}
|
||||||
|
|
|
@ -39,5 +39,15 @@ router
|
||||||
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
|
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
|
||||||
controller.syncUser
|
controller.syncUser
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"/api/users/flags",
|
||||||
|
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
|
||||||
|
controller.setFlag
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
"/api/users/flags",
|
||||||
|
authorized(PermissionTypes.USER, PermissionLevels.READ),
|
||||||
|
controller.getFlags
|
||||||
|
)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -40,6 +40,7 @@ const DocumentTypes = {
|
||||||
DEPLOYMENTS: "deployments",
|
DEPLOYMENTS: "deployments",
|
||||||
METADATA: "metadata",
|
METADATA: "metadata",
|
||||||
MEM_VIEW: "view",
|
MEM_VIEW: "view",
|
||||||
|
USER_FLAG: "flag",
|
||||||
}
|
}
|
||||||
|
|
||||||
const ViewNames = {
|
const ViewNames = {
|
||||||
|
@ -339,6 +340,14 @@ exports.getQueryParams = (datasourceId = null, otherProps = {}) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new flag document ID.
|
||||||
|
* @returns {string} The ID of the flag document that was generated.
|
||||||
|
*/
|
||||||
|
exports.generateUserFlagID = userId => {
|
||||||
|
return `${DocumentTypes.USER_FLAG}${SEPARATOR}${userId}`
|
||||||
|
}
|
||||||
|
|
||||||
exports.generateMetadataID = (type, entityId) => {
|
exports.generateMetadataID = (type, entityId) => {
|
||||||
return `${DocumentTypes.METADATA}${SEPARATOR}${type}${SEPARATOR}${entityId}`
|
return `${DocumentTypes.METADATA}${SEPARATOR}${type}${SEPARATOR}${entityId}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { SourceNames } from "./datasource"
|
export { Query, Datasource } from "./datasource"
|
||||||
|
|
||||||
interface Base {
|
export interface Base {
|
||||||
_id?: string
|
_id?: string
|
||||||
_rev?: string
|
_rev?: string
|
||||||
}
|
}
|
||||||
|
@ -93,39 +93,3 @@ export interface Automation extends Base {
|
||||||
trigger?: AutomationStep
|
trigger?: AutomationStep
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Datasource extends Base {
|
|
||||||
type: string
|
|
||||||
name: string
|
|
||||||
source: SourceNames
|
|
||||||
// the config is defined by the schema
|
|
||||||
config: {
|
|
||||||
[key: string]: string | number | boolean
|
|
||||||
}
|
|
||||||
plus: boolean
|
|
||||||
entities?: {
|
|
||||||
[key: string]: Table
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QueryParameter {
|
|
||||||
name: string
|
|
||||||
default: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Query {
|
|
||||||
_id?: string
|
|
||||||
datasourceId: string
|
|
||||||
name: string
|
|
||||||
parameters: QueryParameter[]
|
|
||||||
fields: {
|
|
||||||
headers: object
|
|
||||||
queryString: string | null
|
|
||||||
path: string
|
|
||||||
requestBody: string | undefined
|
|
||||||
}
|
|
||||||
transformer: string | null
|
|
||||||
schema: any
|
|
||||||
readable: boolean
|
|
||||||
queryVerb: string
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Row, Table } from "./common"
|
import { Row, Table, Base } from "./common"
|
||||||
|
|
||||||
export enum Operation {
|
export enum Operation {
|
||||||
CREATE = "CREATE",
|
CREATE = "CREATE",
|
||||||
|
@ -181,3 +181,52 @@ export interface SqlQuery {
|
||||||
export interface QueryOptions {
|
export interface QueryOptions {
|
||||||
disableReturning?: boolean
|
disableReturning?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Datasource extends Base {
|
||||||
|
type: string
|
||||||
|
name: string
|
||||||
|
source: SourceNames
|
||||||
|
// the config is defined by the schema
|
||||||
|
config: {
|
||||||
|
[key: string]: string | number | boolean
|
||||||
|
}
|
||||||
|
plus: boolean
|
||||||
|
entities?: {
|
||||||
|
[key: string]: Table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryParameter {
|
||||||
|
name: string
|
||||||
|
default: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RestQueryFields {
|
||||||
|
path: string
|
||||||
|
queryString?: string
|
||||||
|
headers: { [key: string]: any }
|
||||||
|
disabledHeaders: { [key: string]: any }
|
||||||
|
requestBody: any
|
||||||
|
bodyType: string
|
||||||
|
json: object
|
||||||
|
method: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RestConfig {
|
||||||
|
url: string
|
||||||
|
defaultHeaders: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Query {
|
||||||
|
_id?: string
|
||||||
|
datasourceId: string
|
||||||
|
name: string
|
||||||
|
parameters: QueryParameter[]
|
||||||
|
fields: RestQueryFields | any
|
||||||
|
transformer: string | null
|
||||||
|
schema: any
|
||||||
|
readable: boolean
|
||||||
|
queryVerb: string
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export interface IntegrationBase {
|
export interface IntegrationBase {
|
||||||
create?(query: any): Promise<any[]>
|
create?(query: any): Promise<any[]|any>
|
||||||
read?(query: any): Promise<any[]>
|
read?(query: any): Promise<any[]|any>
|
||||||
update?(query: any): Promise<any[]>
|
update?(query: any): Promise<any[]|any>
|
||||||
delete?(query: any): Promise<any[]>
|
delete?(query: any): Promise<any[]|any>
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,29 +2,58 @@ import {
|
||||||
Integration,
|
Integration,
|
||||||
DatasourceFieldTypes,
|
DatasourceFieldTypes,
|
||||||
QueryTypes,
|
QueryTypes,
|
||||||
|
RestConfig,
|
||||||
|
RestQueryFields as RestQuery,
|
||||||
} from "../definitions/datasource"
|
} from "../definitions/datasource"
|
||||||
import { IntegrationBase } from "./base/IntegrationBase"
|
import { IntegrationBase } from "./base/IntegrationBase"
|
||||||
|
|
||||||
|
const BodyTypes = {
|
||||||
|
NONE: "none",
|
||||||
|
FORM_DATA: "form",
|
||||||
|
ENCODED: "encoded",
|
||||||
|
JSON: "json",
|
||||||
|
TEXT: "text",
|
||||||
|
}
|
||||||
|
|
||||||
|
const coreFields = {
|
||||||
|
path: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
display: "URL",
|
||||||
|
},
|
||||||
|
queryString: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
type: DatasourceFieldTypes.OBJECT,
|
||||||
|
},
|
||||||
|
enabledHeaders: {
|
||||||
|
type: DatasourceFieldTypes.OBJECT,
|
||||||
|
},
|
||||||
|
requestBody: {
|
||||||
|
type: DatasourceFieldTypes.JSON,
|
||||||
|
},
|
||||||
|
bodyType: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
enum: Object.values(BodyTypes),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
module RestModule {
|
module RestModule {
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
|
const { formatBytes } = require("../utilities")
|
||||||
interface RestConfig {
|
const { performance } = require("perf_hooks")
|
||||||
url: string
|
|
||||||
defaultHeaders: {
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const SCHEMA: Integration = {
|
const SCHEMA: Integration = {
|
||||||
docs: "https://github.com/node-fetch/node-fetch",
|
docs: "https://github.com/node-fetch/node-fetch",
|
||||||
description:
|
description:
|
||||||
"Representational state transfer (REST) is a de-facto standard for a software architecture for interactive applications that typically use multiple Web services. ",
|
"With the REST API datasource, you can connect, query and pull data from multiple REST APIs. You can then use the retrieved data to build apps.",
|
||||||
friendlyName: "REST API",
|
friendlyName: "REST API",
|
||||||
datasource: {
|
datasource: {
|
||||||
url: {
|
url: {
|
||||||
type: DatasourceFieldTypes.STRING,
|
type: DatasourceFieldTypes.STRING,
|
||||||
default: "localhost",
|
default: "",
|
||||||
required: true,
|
required: false,
|
||||||
|
deprecated: true,
|
||||||
},
|
},
|
||||||
defaultHeaders: {
|
defaultHeaders: {
|
||||||
type: DatasourceFieldTypes.OBJECT,
|
type: DatasourceFieldTypes.OBJECT,
|
||||||
|
@ -37,97 +66,30 @@ module RestModule {
|
||||||
readable: true,
|
readable: true,
|
||||||
displayName: "POST",
|
displayName: "POST",
|
||||||
type: QueryTypes.FIELDS,
|
type: QueryTypes.FIELDS,
|
||||||
urlDisplay: true,
|
fields: coreFields,
|
||||||
fields: {
|
|
||||||
path: {
|
|
||||||
type: DatasourceFieldTypes.STRING,
|
|
||||||
},
|
|
||||||
queryString: {
|
|
||||||
type: DatasourceFieldTypes.STRING,
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
type: DatasourceFieldTypes.OBJECT,
|
|
||||||
},
|
|
||||||
requestBody: {
|
|
||||||
type: DatasourceFieldTypes.JSON,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
read: {
|
read: {
|
||||||
displayName: "GET",
|
displayName: "GET",
|
||||||
readable: true,
|
readable: true,
|
||||||
type: QueryTypes.FIELDS,
|
type: QueryTypes.FIELDS,
|
||||||
urlDisplay: true,
|
fields: coreFields,
|
||||||
fields: {
|
|
||||||
path: {
|
|
||||||
type: DatasourceFieldTypes.STRING,
|
|
||||||
},
|
|
||||||
queryString: {
|
|
||||||
type: DatasourceFieldTypes.STRING,
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
type: DatasourceFieldTypes.OBJECT,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
displayName: "PUT",
|
displayName: "PUT",
|
||||||
readable: true,
|
readable: true,
|
||||||
type: QueryTypes.FIELDS,
|
type: QueryTypes.FIELDS,
|
||||||
urlDisplay: true,
|
fields: coreFields,
|
||||||
fields: {
|
|
||||||
path: {
|
|
||||||
type: DatasourceFieldTypes.STRING,
|
|
||||||
},
|
|
||||||
queryString: {
|
|
||||||
type: DatasourceFieldTypes.STRING,
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
type: DatasourceFieldTypes.OBJECT,
|
|
||||||
},
|
|
||||||
requestBody: {
|
|
||||||
type: DatasourceFieldTypes.JSON,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
patch: {
|
patch: {
|
||||||
displayName: "PATCH",
|
displayName: "PATCH",
|
||||||
readable: true,
|
readable: true,
|
||||||
type: QueryTypes.FIELDS,
|
type: QueryTypes.FIELDS,
|
||||||
urlDisplay: true,
|
fields: coreFields,
|
||||||
fields: {
|
|
||||||
path: {
|
|
||||||
type: DatasourceFieldTypes.STRING,
|
|
||||||
},
|
|
||||||
queryString: {
|
|
||||||
type: DatasourceFieldTypes.STRING,
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
type: DatasourceFieldTypes.OBJECT,
|
|
||||||
},
|
|
||||||
requestBody: {
|
|
||||||
type: DatasourceFieldTypes.JSON,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
displayName: "DELETE",
|
displayName: "DELETE",
|
||||||
type: QueryTypes.FIELDS,
|
type: QueryTypes.FIELDS,
|
||||||
urlDisplay: true,
|
fields: coreFields,
|
||||||
fields: {
|
|
||||||
path: {
|
|
||||||
type: DatasourceFieldTypes.STRING,
|
|
||||||
},
|
|
||||||
queryString: {
|
|
||||||
type: DatasourceFieldTypes.STRING,
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
type: DatasourceFieldTypes.OBJECT,
|
|
||||||
},
|
|
||||||
requestBody: {
|
|
||||||
type: DatasourceFieldTypes.JSON,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -137,94 +99,106 @@ module RestModule {
|
||||||
private headers: {
|
private headers: {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
} = {}
|
} = {}
|
||||||
|
private startTimeMs: number = performance.now()
|
||||||
|
|
||||||
constructor(config: RestConfig) {
|
constructor(config: RestConfig) {
|
||||||
this.config = config
|
this.config = config
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseResponse(response: any) {
|
async parseResponse(response: any) {
|
||||||
|
let data, raw, headers
|
||||||
const contentType = response.headers.get("content-type")
|
const contentType = response.headers.get("content-type")
|
||||||
if (contentType && contentType.indexOf("application/json") !== -1) {
|
if (contentType && contentType.indexOf("application/json") !== -1) {
|
||||||
return await response.json()
|
data = await response.json()
|
||||||
|
raw = JSON.stringify(data)
|
||||||
} else {
|
} else {
|
||||||
return await response.text()
|
data = await response.text()
|
||||||
|
raw = data
|
||||||
|
}
|
||||||
|
const size = formatBytes(response.headers.get("content-length") || Buffer.byteLength(raw, "utf8"))
|
||||||
|
const time = `${Math.round(performance.now() - this.startTimeMs)}ms`
|
||||||
|
headers = response.headers.raw()
|
||||||
|
for (let [key, value] of Object.entries(headers)) {
|
||||||
|
headers[key] = Array.isArray(value) ? value[0] : value
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
info: {
|
||||||
|
code: response.status,
|
||||||
|
size,
|
||||||
|
time,
|
||||||
|
},
|
||||||
|
extra: {
|
||||||
|
raw,
|
||||||
|
headers,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getUrl(path: string, queryString: string): string {
|
getUrl(path: string, queryString: string): string {
|
||||||
return `${this.config.url}/${path}?${queryString}`
|
const main = `${path}?${queryString}`
|
||||||
|
let complete = main
|
||||||
|
if (this.config.url && !main.startsWith(this.config.url)) {
|
||||||
|
complete = !this.config.url ? main : `${this.config.url}/${main}`
|
||||||
|
}
|
||||||
|
if (!complete.startsWith("http")) {
|
||||||
|
complete = `http://${complete}`
|
||||||
|
}
|
||||||
|
return complete
|
||||||
}
|
}
|
||||||
|
|
||||||
async create({ path = "", queryString = "", headers = {}, json = {} }) {
|
async _req(query: RestQuery) {
|
||||||
|
const { path = "", queryString = "", headers = {}, method = "GET", disabledHeaders, bodyType, requestBody } = query
|
||||||
this.headers = {
|
this.headers = {
|
||||||
...this.config.defaultHeaders,
|
...this.config.defaultHeaders,
|
||||||
...headers,
|
...headers,
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(this.getUrl(path, queryString), {
|
if (disabledHeaders) {
|
||||||
method: "POST",
|
for (let headerKey of Object.keys(this.headers)) {
|
||||||
headers: this.headers,
|
if (disabledHeaders[headerKey]) {
|
||||||
body: JSON.stringify(json),
|
delete this.headers[headerKey]
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let json
|
||||||
|
if (bodyType === BodyTypes.JSON && requestBody) {
|
||||||
|
try {
|
||||||
|
json = JSON.parse(requestBody)
|
||||||
|
} catch (err) {
|
||||||
|
throw "Invalid JSON for request body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const input: any = { method, headers: this.headers }
|
||||||
|
if (json && typeof json === "object" && Object.keys(json).length > 0) {
|
||||||
|
input.body = JSON.stringify(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.startTimeMs = performance.now()
|
||||||
|
const response = await fetch(this.getUrl(path, queryString), input)
|
||||||
return await this.parseResponse(response)
|
return await this.parseResponse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
async read({ path = "", queryString = "", headers = {} }) {
|
async create(opts: RestQuery) {
|
||||||
this.headers = {
|
return this._req({ ...opts, method: "POST" })
|
||||||
...this.config.defaultHeaders,
|
|
||||||
...headers,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(this.getUrl(path, queryString), {
|
async read(opts: RestQuery) {
|
||||||
headers: this.headers,
|
return this._req({ ...opts, method: "GET" })
|
||||||
})
|
|
||||||
|
|
||||||
return await this.parseResponse(response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async update({ path = "", queryString = "", headers = {}, json = {} }) {
|
async update(opts: RestQuery) {
|
||||||
this.headers = {
|
return this._req({ ...opts, method: "PUT" })
|
||||||
...this.config.defaultHeaders,
|
|
||||||
...headers,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(this.getUrl(path, queryString), {
|
async patch(opts: RestQuery) {
|
||||||
method: "PUT",
|
return this._req({ ...opts, method: "PATCH" })
|
||||||
headers: this.headers,
|
|
||||||
body: JSON.stringify(json),
|
|
||||||
})
|
|
||||||
|
|
||||||
return await this.parseResponse(response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async patch({ path = "", queryString = "", headers = {}, json = {} }) {
|
async delete(opts: RestQuery) {
|
||||||
this.headers = {
|
return this._req({ ...opts, method: "DELETE" })
|
||||||
...this.config.defaultHeaders,
|
|
||||||
...headers,
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(this.getUrl(path, queryString), {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: this.headers,
|
|
||||||
body: JSON.stringify(json),
|
|
||||||
})
|
|
||||||
|
|
||||||
return await this.parseResponse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete({ path = "", queryString = "", headers = {} }) {
|
|
||||||
this.headers = {
|
|
||||||
...this.config.defaultHeaders,
|
|
||||||
...headers,
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(this.getUrl(path, queryString), {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: this.headers,
|
|
||||||
})
|
|
||||||
|
|
||||||
return await this.parseResponse(response)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
jest.mock("node-fetch", () =>
|
jest.mock("node-fetch", () =>
|
||||||
jest.fn(() => ({
|
jest.fn(() => ({
|
||||||
headers: {
|
headers: {
|
||||||
|
raw: () => {
|
||||||
|
return { "content-type": ["application/json"] }
|
||||||
|
},
|
||||||
get: () => ["application/json"]
|
get: () => ["application/json"]
|
||||||
},
|
},
|
||||||
json: jest.fn(),
|
json: jest.fn(),
|
||||||
|
@ -24,6 +27,7 @@ describe("REST Integration", () => {
|
||||||
config = new TestConfiguration({
|
config = new TestConfiguration({
|
||||||
url: BASE_URL,
|
url: BASE_URL,
|
||||||
})
|
})
|
||||||
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("calls the create method with the correct params", async () => {
|
it("calls the create method with the correct params", async () => {
|
||||||
|
@ -33,9 +37,10 @@ describe("REST Integration", () => {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
},
|
},
|
||||||
json: {
|
bodyType: "json",
|
||||||
|
requestBody: JSON.stringify({
|
||||||
name: "test",
|
name: "test",
|
||||||
},
|
}),
|
||||||
}
|
}
|
||||||
const response = await config.integration.create(query)
|
const response = await config.integration.create(query)
|
||||||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
|
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
|
||||||
|
@ -60,6 +65,7 @@ describe("REST Integration", () => {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "text/html",
|
Accept: "text/html",
|
||||||
},
|
},
|
||||||
|
method: "GET",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -70,13 +76,14 @@ describe("REST Integration", () => {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
},
|
},
|
||||||
json: {
|
bodyType: "json",
|
||||||
|
requestBody: JSON.stringify({
|
||||||
name: "test",
|
name: "test",
|
||||||
},
|
}),
|
||||||
}
|
}
|
||||||
const response = await config.integration.update(query)
|
const response = await config.integration.update(query)
|
||||||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
|
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
|
||||||
method: "POST",
|
method: "PUT",
|
||||||
body: '{"name":"test"}',
|
body: '{"name":"test"}',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
|
@ -91,9 +98,10 @@ describe("REST Integration", () => {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
},
|
},
|
||||||
json: {
|
bodyType: "json",
|
||||||
|
requestBody: JSON.stringify({
|
||||||
name: "test",
|
name: "test",
|
||||||
},
|
}),
|
||||||
}
|
}
|
||||||
const response = await config.integration.delete(query)
|
const response = await config.integration.delete(query)
|
||||||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
|
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
|
||||||
|
@ -101,6 +109,7 @@ describe("REST Integration", () => {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
},
|
},
|
||||||
|
body: '{"name":"test"}',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,6 +15,15 @@ function formatResponse(resp) {
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasExtraData(response) {
|
||||||
|
return (
|
||||||
|
typeof response === "object" &&
|
||||||
|
!Array.isArray(response) &&
|
||||||
|
response.data != null &&
|
||||||
|
response.info != null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function runAndTransform(datasource, queryVerb, query, transformer) {
|
async function runAndTransform(datasource, queryVerb, query, transformer) {
|
||||||
const Integration = integrations[datasource.source]
|
const Integration = integrations[datasource.source]
|
||||||
if (!Integration) {
|
if (!Integration) {
|
||||||
|
@ -22,7 +31,15 @@ async function runAndTransform(datasource, queryVerb, query, transformer) {
|
||||||
}
|
}
|
||||||
const integration = new Integration(datasource.config)
|
const integration = new Integration(datasource.config)
|
||||||
|
|
||||||
let rows = formatResponse(await integration[queryVerb](query))
|
let output = formatResponse(await integration[queryVerb](query))
|
||||||
|
let rows = output,
|
||||||
|
info = undefined,
|
||||||
|
extra = undefined
|
||||||
|
if (hasExtraData(output)) {
|
||||||
|
rows = output.data
|
||||||
|
info = output.info
|
||||||
|
extra = output.extra
|
||||||
|
}
|
||||||
|
|
||||||
// transform as required
|
// transform as required
|
||||||
if (transformer) {
|
if (transformer) {
|
||||||
|
@ -47,7 +64,7 @@ async function runAndTransform(datasource, queryVerb, query, transformer) {
|
||||||
integration.end()
|
integration.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
return { rows, keys }
|
return { rows, keys, info, extra }
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = (input, callback) => {
|
module.exports = (input, callback) => {
|
||||||
|
|
|
@ -150,3 +150,14 @@ exports.doesDatabaseExist = async dbName => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.formatBytes = bytes => {
|
||||||
|
const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
|
||||||
|
const byteIncrements = 1024
|
||||||
|
let unit = 0
|
||||||
|
let size = parseInt(bytes, 10) || 0
|
||||||
|
while (size >= byteIncrements && ++unit) {
|
||||||
|
size /= byteIncrements
|
||||||
|
}
|
||||||
|
return `${size.toFixed(size < 10 && unit > 0 ? 1 : 0)}${units[unit]}`
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/string-templates",
|
"name": "@budibase/string-templates",
|
||||||
"version": "1.0.18",
|
"version": "1.0.19-alpha.0",
|
||||||
"description": "Handlebars wrapper for Budibase templating.",
|
"description": "Handlebars wrapper for Budibase templating.",
|
||||||
"main": "src/index.cjs",
|
"main": "src/index.cjs",
|
||||||
"module": "dist/bundle.mjs",
|
"module": "dist/bundle.mjs",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/worker",
|
"name": "@budibase/worker",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "1.0.18",
|
"version": "1.0.19-alpha.0",
|
||||||
"description": "Budibase background service",
|
"description": "Budibase background service",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -29,8 +29,8 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/auth": "^1.0.18",
|
"@budibase/auth": "^1.0.19-alpha.0",
|
||||||
"@budibase/string-templates": "^1.0.18",
|
"@budibase/string-templates": "^1.0.19-alpha.0",
|
||||||
"@koa/router": "^8.0.0",
|
"@koa/router": "^8.0.0",
|
||||||
"@sentry/node": "^6.0.0",
|
"@sentry/node": "^6.0.0",
|
||||||
"@techpass/passport-openidconnect": "^0.3.0",
|
"@techpass/passport-openidconnect": "^0.3.0",
|
||||||
|
|
Loading…
Reference in a new issue