Merge branch 'develop' of github.com:Budibase/budibase into develop
This commit is contained in:
commit
650480e3a6
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.0.18",
|
||||
"version": "1.0.19-alpha.0",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/auth",
|
||||
"version": "1.0.18",
|
||||
"version": "1.0.19-alpha.0",
|
||||
"description": "Authentication middlewares for budibase builder and apps",
|
||||
"main": "src/index.js",
|
||||
"author": "Budibase",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "1.0.18",
|
||||
"version": "1.0.19-alpha.0",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
|
|
@ -80,4 +80,8 @@
|
|||
.active svg {
|
||||
color: var(--spectrum-global-color-blue-600);
|
||||
}
|
||||
|
||||
.spectrum-ActionButton-label {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
<span class="spectrum-Switch-switch" />
|
||||
<label class="spectrum-Switch-label" for={id}>{text}</label>
|
||||
{#if text}
|
||||
<label class="spectrum-Switch-label" for={id}>{text}</label>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
export let disabled = false
|
||||
export let error = null
|
||||
export let id = null
|
||||
export let height = null
|
||||
export let minHeight = null
|
||||
export const getCaretPosition = () => ({
|
||||
start: textarea.selectionStart,
|
||||
end: textarea.selectionEnd,
|
||||
|
@ -22,6 +24,8 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
style={(height ? `height: ${height}px;` : "") +
|
||||
(minHeight ? `min-height: ${minHeight}px` : "")}
|
||||
class="spectrum-Textfield spectrum-Textfield--multiline"
|
||||
class:is-invalid={!!error}
|
||||
class:is-disabled={disabled}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
export let labelPosition = "above"
|
||||
export let error = null
|
||||
export let options = []
|
||||
export let direction = "vertical"
|
||||
export let getOptionLabel = option => extractProperty(option, "label")
|
||||
export let getOptionValue = option => extractProperty(option, "value")
|
||||
|
||||
|
@ -31,6 +32,7 @@
|
|||
{disabled}
|
||||
{value}
|
||||
{options}
|
||||
{direction}
|
||||
{getOptionLabel}
|
||||
{getOptionValue}
|
||||
on:change={onChange}
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
export let disabled = false
|
||||
export let error = null
|
||||
export let getCaretPosition = null
|
||||
export let height = null
|
||||
export let minHeight = null
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
|
@ -25,6 +27,8 @@
|
|||
{disabled}
|
||||
{value}
|
||||
{placeholder}
|
||||
{height}
|
||||
{minHeight}
|
||||
on:change={onChange}
|
||||
/>
|
||||
</Field>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
export let size = "M"
|
||||
export let hoverable = false
|
||||
export let disabled = false
|
||||
export let color
|
||||
|
||||
$: rotation = getRotation(direction)
|
||||
|
||||
|
@ -25,7 +26,9 @@
|
|||
focusable="false"
|
||||
aria-hidden={hidden}
|
||||
aria-label={name}
|
||||
style={`transform: rotate(${rotation}deg)`}
|
||||
style={`transform: rotate(${rotation}deg); ${
|
||||
color ? `color: ${color};` : ""
|
||||
}`}
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-{name}" />
|
||||
</svg>
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
<div class="icon-container">
|
||||
<div
|
||||
class="icon"
|
||||
class:icon-small={size === "M" || size === "S"}
|
||||
on:mouseover={() => (showTooltip = true)}
|
||||
on:mouseleave={() => (showTooltip = false)}
|
||||
>
|
||||
|
@ -44,6 +45,7 @@
|
|||
}
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.icon-container {
|
||||
position: relative;
|
||||
|
@ -64,4 +66,8 @@
|
|||
.icon {
|
||||
transform: scale(0.75);
|
||||
}
|
||||
.icon-small {
|
||||
margin-top: -2px;
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
export let icon = ""
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const selected = getContext("tab")
|
||||
let selected = getContext("tab")
|
||||
let tab
|
||||
let tabInfo
|
||||
|
||||
|
@ -16,8 +16,8 @@
|
|||
// We just need to get this off the main thread to fix this, by using
|
||||
// a 0ms timeout.
|
||||
setTimeout(() => {
|
||||
tabInfo = tab.getBoundingClientRect()
|
||||
if ($selected.title === title) {
|
||||
tabInfo = tab?.getBoundingClientRect()
|
||||
if (tabInfo && $selected.title === title) {
|
||||
$selected.info = tabInfo
|
||||
}
|
||||
}, 0)
|
||||
|
|
|
@ -6,9 +6,13 @@
|
|||
export let selected
|
||||
export let vertical = 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 emphasized = false
|
||||
|
||||
let thisSelected = undefined
|
||||
|
||||
let _id = id()
|
||||
const tab = writable({ title: selected, id: _id, emphasized })
|
||||
setContext("tab", tab)
|
||||
|
@ -18,9 +22,19 @@
|
|||
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
|
||||
dispatch("select", selected)
|
||||
dispatch("select", thisSelected)
|
||||
}
|
||||
if ($tab.title !== thisSelected) {
|
||||
tab.update(state => {
|
||||
state.title = thisSelected
|
||||
return state
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,6 +73,7 @@
|
|||
<div
|
||||
bind:this={container}
|
||||
class:quiet
|
||||
class:noHorizPadding
|
||||
class="selected-border spectrum-Tabs {quiet &&
|
||||
'spectrum-Tabs--quiet'} spectrum-Tabs--{vertical
|
||||
? 'vertical'
|
||||
|
@ -99,6 +114,9 @@
|
|||
.spectrum-Tabs--horizontal .spectrum-Tabs-selectionIndicator {
|
||||
bottom: 0 !important;
|
||||
}
|
||||
.noHorizPadding {
|
||||
padding: 0;
|
||||
}
|
||||
.noPadding {
|
||||
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 ColorPicker } from "./ColorPicker/ColorPicker.svelte"
|
||||
export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte"
|
||||
export { default as Banner } from "./Banner/Banner.svelte"
|
||||
|
||||
// Typography
|
||||
export { default as Body } from "./Typography/Body.svelte"
|
||||
|
|
|
@ -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",
|
||||
"version": "1.0.18",
|
||||
"version": "1.0.19-alpha.0",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -65,10 +65,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.18",
|
||||
"@budibase/client": "^1.0.18",
|
||||
"@budibase/bbui": "^1.0.19-alpha.0",
|
||||
"@budibase/client": "^1.0.19-alpha.0",
|
||||
"@budibase/colorpicker": "1.1.2",
|
||||
"@budibase/string-templates": "^1.0.18",
|
||||
"@budibase/string-templates": "^1.0.19-alpha.0",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -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
|
||||
$: {
|
||||
UNSORTABLE_TYPES.forEach(type => {
|
||||
Object.values(schema).forEach(col => {
|
||||
Object.values(schema || {}).forEach(col => {
|
||||
if (col.type === type) {
|
||||
col.sortable = false
|
||||
}
|
||||
|
@ -113,16 +113,16 @@
|
|||
|
||||
<Layout noPadding gap="S">
|
||||
<div>
|
||||
<div class="table-title">
|
||||
{#if title}
|
||||
{#if title}
|
||||
<div class="table-title">
|
||||
<Heading size="S">{title}</Heading>
|
||||
{/if}
|
||||
{#if loading}
|
||||
<div transition:fade|local>
|
||||
<Spinner size="10" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if loading}
|
||||
<div transition:fade|local>
|
||||
<Spinner size="10" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="popovers">
|
||||
<slot />
|
||||
{#if !isUsersTable && selectedRows.length > 0}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
|
||||
import { customQueryIconText, customQueryIconColor } from "helpers/data/utils"
|
||||
import ICONS from "./icons"
|
||||
|
||||
let openDataSources = []
|
||||
|
@ -129,6 +130,8 @@
|
|||
<NavItem
|
||||
indentLevel={1}
|
||||
icon="SQLQuery"
|
||||
iconText={customQueryIconText(datasource, query)}
|
||||
iconColor={customQueryIconColor(datasource, query)}
|
||||
text={query.name}
|
||||
opened={$queries.selected === query._id}
|
||||
selected={$queries.selected === query._id}
|
||||
|
|
|
@ -9,19 +9,53 @@
|
|||
} from "@budibase/bbui"
|
||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
import { capitalise } from "helpers"
|
||||
import { IntegrationTypes } from "constants/backend"
|
||||
|
||||
export let integration
|
||||
export let datasource
|
||||
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
|
||||
|
||||
function getDisplayName(key) {
|
||||
let name
|
||||
if (schema[key]?.display) {
|
||||
name = schema[key].display
|
||||
} else {
|
||||
name = key
|
||||
}
|
||||
return capitalise(name)
|
||||
}
|
||||
</script>
|
||||
|
||||
<form>
|
||||
<Layout gap="S">
|
||||
{#each Object.keys(schema) as configKey}
|
||||
<Layout noPadding gap="S">
|
||||
{#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"}
|
||||
<div class="form-row ssl">
|
||||
<Label>{capitalise(configKey)}</Label>
|
||||
<Label>{getDisplayName(configKey)}</Label>
|
||||
<Button secondary thin outline on:click={addButton.addEntry()}
|
||||
>Add</Button
|
||||
>
|
||||
|
@ -29,30 +63,31 @@
|
|||
<KeyValueBuilder
|
||||
bind:this={addButton}
|
||||
defaults={schema[configKey].default}
|
||||
bind:object={integration[configKey]}
|
||||
bind:object={config[configKey]}
|
||||
on:change
|
||||
noAddButton={true}
|
||||
/>
|
||||
{:else if schema[configKey].type === "boolean"}
|
||||
<div class="form-row">
|
||||
<Label>{capitalise(configKey)}</Label>
|
||||
<Toggle text="" bind:value={integration[configKey]} />
|
||||
<Label>{getDisplayName(configKey)}</Label>
|
||||
<Toggle text="" bind:value={config[configKey]} />
|
||||
</div>
|
||||
{:else if schema[configKey].type === "longForm"}
|
||||
<div class="form-row">
|
||||
<Label>{capitalise(configKey)}</Label>
|
||||
<Label>{getDisplayName(configKey)}</Label>
|
||||
<TextArea
|
||||
type={schema[configKey].type}
|
||||
on:change
|
||||
bind:value={integration[configKey]}
|
||||
bind:value={config[configKey]}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="form-row">
|
||||
<Label>{capitalise(configKey)}</Label>
|
||||
<Label>{getDisplayName(configKey)}</Label>
|
||||
<Input
|
||||
type={schema[configKey].type}
|
||||
on:change
|
||||
bind:value={integration[configKey]}
|
||||
bind:value={config[configKey]}
|
||||
/>
|
||||
</div>
|
||||
{/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 ICONS from "../icons"
|
||||
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 DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||
import { createRestDatasource } from "builderStore/datasource"
|
||||
import { goto } from "@roxi/routify"
|
||||
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
|
||||
|
||||
export let modal
|
||||
|
@ -19,8 +21,6 @@
|
|||
|
||||
checkShowImport()
|
||||
|
||||
const INTERNAL = "BUDIBASE"
|
||||
|
||||
onMount(() => {
|
||||
fetchIntegrations()
|
||||
})
|
||||
|
@ -50,10 +50,14 @@
|
|||
importModal.show()
|
||||
}
|
||||
|
||||
function chooseNextModal() {
|
||||
if (integration.type === INTERNAL) {
|
||||
async function chooseNextModal() {
|
||||
if (integration.type === IntegrationTypes.INTERNAL) {
|
||||
externalDatasourceModal.hide()
|
||||
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 {
|
||||
externalDatasourceModal.show()
|
||||
}
|
||||
|
@ -63,7 +67,7 @@
|
|||
const response = await api.get("/api/integrations")
|
||||
const json = await response.json()
|
||||
integrations = {
|
||||
[INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
||||
[IntegrationTypes.INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
||||
...json,
|
||||
}
|
||||
return json
|
||||
|
@ -108,8 +112,8 @@
|
|||
to your app using Budibase's built-in database.
|
||||
</Body>
|
||||
<div
|
||||
class:selected={integration.type === INTERNAL}
|
||||
on:click={() => selectIntegration(INTERNAL)}
|
||||
class:selected={integration.type === IntegrationTypes.INTERNAL}
|
||||
on:click={() => selectIntegration(IntegrationTypes.INTERNAL)}
|
||||
class="item hoverable"
|
||||
>
|
||||
<div class="item-body">
|
||||
|
@ -124,7 +128,7 @@
|
|||
<Detail size="S">Connect to data source</Detail>
|
||||
</div>
|
||||
<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
|
||||
class:selected={integration.type === integrationType}
|
||||
on:click={() => selectIntegration(integrationType)}
|
||||
|
|
|
@ -1,66 +1,33 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { ModalContent, notifications, Body, Layout } from "@budibase/bbui"
|
||||
import analytics, { Events } from "analytics"
|
||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||
import { datasources, tables } from "stores/backend"
|
||||
import { IntegrationNames } from "constants"
|
||||
import { IntegrationNames } from "constants/backend"
|
||||
import cloneDeep from "lodash/cloneDeepWith"
|
||||
import { saveDatasource as save } from "builderStore/datasource"
|
||||
|
||||
export let integration
|
||||
export let modal
|
||||
|
||||
// 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() {
|
||||
const datasource = prepareData()
|
||||
try {
|
||||
// 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)
|
||||
const resp = await save(datasource)
|
||||
$goto(`./datasource/${resp._id}`)
|
||||
notifications.success(`Datasource updated successfully.`)
|
||||
analytics.captureEvent(Events.DATASOURCE.CREATED, {
|
||||
name: resp.name,
|
||||
source: resp.source,
|
||||
})
|
||||
return true
|
||||
} catch (err) {
|
||||
notifications.error(`Error saving datasource: ${err}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={`Connect to ${IntegrationNames[config.type]}`}
|
||||
title={`Connect to ${IntegrationNames[datasource.type]}`}
|
||||
onConfirm={() => saveDatasource()}
|
||||
onCancel={() => modal.show()}
|
||||
confirmText={config.plus
|
||||
confirmText={datasource.plus
|
||||
? "Fetch tables from database"
|
||||
: "Save and continue to query"}
|
||||
cancelText="Back"
|
||||
|
@ -72,7 +39,11 @@
|
|||
</Body>
|
||||
</Layout>
|
||||
|
||||
<IntegrationConfigForm schema={config.schema} integration={config.config} />
|
||||
<IntegrationConfigForm
|
||||
schema={datasource.schema}
|
||||
bind:datasource
|
||||
creating={true}
|
||||
/>
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { datasources, tables } from "stores/backend"
|
||||
import { datasources, queries, tables } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
|
@ -12,12 +12,18 @@
|
|||
let updateDatasourceDialog
|
||||
|
||||
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
|
||||
await datasources.delete(datasource)
|
||||
notifications.success("Datasource deleted")
|
||||
// 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 (
|
||||
wasSelectedSource === datasource._id ||
|
||||
(entities &&
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { queries } from "stores/backend"
|
||||
import { datasources, queries } from "stores/backend"
|
||||
|
||||
export let query
|
||||
|
||||
let confirmDeleteDialog
|
||||
|
||||
async function deleteQuery() {
|
||||
const wasSelectedQuery = $queries.selected
|
||||
const selectedDatasource = $datasources.selected
|
||||
await queries.delete(query)
|
||||
if (wasSelectedQuery === query._id) {
|
||||
$goto(`./datasource/${selectedDatasource}`)
|
||||
}
|
||||
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",
|
||||
base: "text/html",
|
||||
},
|
||||
Text: {
|
||||
name: "text/html",
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,12 +1,30 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { Icon, Body } from "@budibase/bbui"
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
<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 {
|
||||
color: inherit;
|
||||
position: absolute;
|
||||
|
@ -14,4 +32,10 @@
|
|||
right: var(--spacing-m);
|
||||
border-radius: 55%;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.inner:hover .hidden {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
export let selected = false
|
||||
export let opened = false
|
||||
export let draggable = false
|
||||
export let iconText
|
||||
export let iconColor
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -42,9 +44,13 @@
|
|||
{/if}
|
||||
|
||||
<slot name="icon" />
|
||||
{#if icon}
|
||||
{#if iconText}
|
||||
<div class="iconText" style={iconColor ? `color: ${iconColor};` : ""}>
|
||||
{iconText}
|
||||
</div>
|
||||
{:else if icon}
|
||||
<div class="icon">
|
||||
<Icon size="S" name={icon} />
|
||||
<Icon color={iconColor} size="S" name={icon} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text">{text}</div>
|
||||
|
@ -123,4 +129,9 @@
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.iconText {
|
||||
margin-top: 1px;
|
||||
font-size: var(--spectrum-global-dimension-font-size-50);
|
||||
}
|
||||
</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,
|
||||
} 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 { makePropSafe as safe } from "@budibase/string-templates"
|
||||
|
||||
|
@ -148,15 +148,15 @@
|
|||
/>
|
||||
{#if value?.type === "query"}
|
||||
<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>
|
||||
<DrawerContent slot="body">
|
||||
<Layout noPadding>
|
||||
<Layout noPadding gap="XS">
|
||||
{#if getQueryParams(value).length > 0}
|
||||
<ParameterBuilder
|
||||
<BindingBuilder
|
||||
bind:customParams={tmpQueryParams}
|
||||
parameters={getQueryParams(value)}
|
||||
{bindings}
|
||||
bindings={getQueryParams(value)}
|
||||
bind:bindableOptions={bindings}
|
||||
/>
|
||||
{/if}
|
||||
<IntegrationQueryEditor
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { Select, Layout, Input, Checkbox } from "@budibase/bbui"
|
||||
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"
|
||||
|
||||
export let parameters
|
||||
|
@ -53,10 +53,10 @@
|
|||
|
||||
{#if query?.parameters?.length > 0}
|
||||
<div>
|
||||
<ParameterBuilder
|
||||
<BindingBuilder
|
||||
bind:customParams={parameters.queryParams}
|
||||
parameters={query.parameters}
|
||||
{bindings}
|
||||
bindings={query.parameters}
|
||||
bind:bindableOptions={bindings}
|
||||
/>
|
||||
<IntegrationQueryEditor
|
||||
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>
|
||||
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 object = defaults || {}
|
||||
export let activity = {}
|
||||
export let readOnly
|
||||
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 fieldActivity = buildFieldActivity(activity)
|
||||
|
||||
$: object = fields.reduce(
|
||||
(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() {
|
||||
fields = [...fields, {}]
|
||||
fields = [...fields, { name: "", value: "" }]
|
||||
fieldActivity = [...fieldActivity, true]
|
||||
changed()
|
||||
}
|
||||
|
||||
function deleteEntry(idx) {
|
||||
fields.splice(idx, 1)
|
||||
fieldActivity.splice(idx, 1)
|
||||
changed()
|
||||
}
|
||||
|
||||
function changed() {
|
||||
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>
|
||||
|
||||
<!-- Builds Objects with Key Value Pairs. Useful for building things like Request Headers. -->
|
||||
<div class="container" class:readOnly>
|
||||
{#each fields as field, idx}
|
||||
<Input placeholder="Key" bind:value={field.name} />
|
||||
<Input placeholder="Value" bind:value={field.value} />
|
||||
{#if !readOnly}
|
||||
<Icon hoverable name="Close" on:click={() => deleteEntry(idx)} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{#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}
|
||||
<Input
|
||||
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}
|
||||
<Icon hoverable name="Close" on:click={() => deleteEntry(idx)} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if !readOnly && !noAddButton}
|
||||
<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>
|
||||
{/if}
|
||||
|
||||
|
@ -47,4 +124,10 @@
|
|||
align-items: center;
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
.container-active {
|
||||
grid-template-columns: 1fr 1fr 50px 20px;
|
||||
}
|
||||
.readOnly {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Icon, Body, Button, Input, Heading, Layout } from "@budibase/bbui"
|
||||
import { Body, Button, Heading, Icon, Input, Layout } from "@budibase/bbui"
|
||||
import {
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
|
@ -7,83 +7,82 @@
|
|||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
|
||||
export let bindable = true
|
||||
export let parameters = []
|
||||
export let bindings = []
|
||||
export let bindableOptions = []
|
||||
export let customParams = {}
|
||||
|
||||
function newQueryParameter() {
|
||||
parameters = [...parameters, {}]
|
||||
function newQueryBinding() {
|
||||
bindings = [...bindings, {}]
|
||||
}
|
||||
|
||||
function deleteQueryParameter(idx) {
|
||||
parameters.splice(idx, 1)
|
||||
parameters = parameters
|
||||
function deleteQueryBinding(idx) {
|
||||
bindings.splice(idx, 1)
|
||||
bindings = bindings
|
||||
}
|
||||
|
||||
// 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
|
||||
// for parsing, then converted back so we can display it the readable form in the UI
|
||||
function onBindingChange(param, valueToParse) {
|
||||
const parsedBindingValue = readableToRuntimeBinding(bindings, valueToParse)
|
||||
customParams[param] = parsedBindingValue
|
||||
customParams[param] = readableToRuntimeBinding(
|
||||
bindableOptions,
|
||||
valueToParse
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Layout paddingX="none" gap="S">
|
||||
<div class="controls">
|
||||
<Heading size="XS">Parameters</Heading>
|
||||
<Layout noPadding={bindable} gap="S">
|
||||
<div class="controls" class:height={!bindable}>
|
||||
<Heading size="XS">Bindings</Heading>
|
||||
{#if !bindable}
|
||||
<Button secondary on:click={newQueryParameter}>Add Param</Button>
|
||||
<Button secondary on:click={newQueryBinding}>Add Binding</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<Body size="S">
|
||||
{#if !bindable}
|
||||
Parameters come in two parts: the parameter name, and a default/fallback
|
||||
value.
|
||||
Bindings come in two parts: the binding name, and a default/fallback
|
||||
value. These bindings can be used as Handlebars expressions throughout the
|
||||
query.
|
||||
{: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.
|
||||
{/if}
|
||||
</Body>
|
||||
<div class="parameters" class:bindable>
|
||||
{#each parameters as parameter, idx}
|
||||
<div class="bindings" class:bindable>
|
||||
{#each bindings as binding, idx}
|
||||
<Input
|
||||
placeholder="Parameter Name"
|
||||
placeholder="Binding Name"
|
||||
thin
|
||||
disabled={bindable}
|
||||
bind:value={parameter.name}
|
||||
bind:value={binding.name}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Default"
|
||||
thin
|
||||
disabled={bindable}
|
||||
bind:value={parameter.default}
|
||||
bind:value={binding.default}
|
||||
/>
|
||||
{#if bindable}
|
||||
<DrawerBindableInput
|
||||
title={`Query parameter "${parameter.name}"`}
|
||||
title={`Query binding "${binding.name}"`}
|
||||
placeholder="Value"
|
||||
thin
|
||||
on:change={evt => onBindingChange(parameter.name, evt.detail)}
|
||||
on:change={evt => onBindingChange(binding.name, evt.detail)}
|
||||
value={runtimeToReadableBinding(
|
||||
bindings,
|
||||
customParams?.[parameter.name]
|
||||
bindableOptions,
|
||||
customParams?.[binding.name]
|
||||
)}
|
||||
{bindings}
|
||||
{bindableOptions}
|
||||
/>
|
||||
{:else}
|
||||
<Icon
|
||||
hoverable
|
||||
name="Close"
|
||||
on:click={() => deleteQueryParameter(idx)}
|
||||
/>
|
||||
<Icon hoverable name="Close" on:click={() => deleteQueryBinding(idx)} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.parameters.bindable {
|
||||
.bindings.bindable {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
|
@ -91,13 +90,16 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.parameters {
|
||||
.bindings {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 5%;
|
||||
grid-gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.height {
|
||||
height: 40px;
|
||||
}
|
||||
</style>
|
|
@ -2,29 +2,42 @@
|
|||
import { Label, Layout, Input } from "@budibase/bbui"
|
||||
import Editor from "./QueryEditor.svelte"
|
||||
import KeyValueBuilder from "./KeyValueBuilder.svelte"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
export let fields = {}
|
||||
export let schema
|
||||
export let editable
|
||||
|
||||
$: schemaKeys = Object.keys(schema.fields)
|
||||
$: schemaKeys = Object.keys(schema?.fields || {})
|
||||
|
||||
function updateCustomFields({ detail }) {
|
||||
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>
|
||||
|
||||
<form on:submit|preventDefault>
|
||||
<Layout noPadding gap="S">
|
||||
{#each schemaKeys as field}
|
||||
{#if schema.fields[field]?.type === "object"}
|
||||
<div>
|
||||
<Label small>{field}</Label>
|
||||
<KeyValueBuilder readOnly={!editable} bind:object={fields[field]} />
|
||||
</div>
|
||||
<Label small>{getDisplayName(field)}</Label>
|
||||
<KeyValueBuilder
|
||||
name={getDisplayName(field)}
|
||||
readOnly={!editable}
|
||||
bind:object={fields[field]}
|
||||
/>
|
||||
{:else if schema.fields[field]?.type === "json"}
|
||||
<div>
|
||||
<Label extraSmall grey>{field}</Label>
|
||||
<Label extraSmall grey>{getDisplayName(field)}</Label>
|
||||
<Editor
|
||||
mode="json"
|
||||
on:change={({ detail }) => (fields[field] = detail.value)}
|
||||
|
@ -34,9 +47,9 @@
|
|||
</div>
|
||||
{:else}
|
||||
<div class="horizontal">
|
||||
<Label small>{field}</Label>
|
||||
<Label small>{getDisplayName(field)}</Label>
|
||||
<Input
|
||||
placeholder="Enter {field}"
|
||||
placeholder="Enter {getDisplayName(field)}"
|
||||
outline
|
||||
disabled={!editable}
|
||||
type={schema.fields[field]?.type}
|
||||
|
|
|
@ -14,37 +14,27 @@
|
|||
Tab,
|
||||
} from "@budibase/bbui"
|
||||
import { notifications, Divider } from "@budibase/bbui"
|
||||
import api from "builderStore/api"
|
||||
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
|
||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
|
||||
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
||||
import {
|
||||
datasources,
|
||||
integrations,
|
||||
queries,
|
||||
roles,
|
||||
permissions,
|
||||
} from "stores/backend"
|
||||
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
|
||||
import { datasources, integrations, queries } from "stores/backend"
|
||||
import { capitalise } from "../../helpers"
|
||||
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
||||
import { Roles } from "constants/backend"
|
||||
import { onMount } from "svelte"
|
||||
import JSONPreview from "./JSONPreview.svelte"
|
||||
import { SchemaTypeOptions } from "constants/backend"
|
||||
import KeyValueBuilder from "./KeyValueBuilder.svelte"
|
||||
import { fieldsToSchema, schemaToFields } from "helpers/data/utils"
|
||||
import AccessLevelSelect from "./AccessLevelSelect.svelte"
|
||||
|
||||
export let query
|
||||
|
||||
let fields = query.schema ? schemaToFields(query.schema) : []
|
||||
let fields = query?.schema ? schemaToFields(query.schema) : []
|
||||
let parameters
|
||||
let data = []
|
||||
let roleId
|
||||
let saveId
|
||||
const transformerDocs =
|
||||
"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)
|
||||
$: query.schema = fieldsToSchema(fields)
|
||||
|
@ -60,84 +50,37 @@
|
|||
query.transformer = "return data"
|
||||
}
|
||||
|
||||
function newField() {
|
||||
fields = [...fields, {}]
|
||||
}
|
||||
|
||||
function deleteField(idx) {
|
||||
fields.splice(idx, 1)
|
||||
fields = fields
|
||||
}
|
||||
|
||||
function resetDependentFields() {
|
||||
if (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) {
|
||||
query.fields.extra = extraQueryFields
|
||||
}
|
||||
|
||||
async function previewQuery() {
|
||||
try {
|
||||
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: datasource._id,
|
||||
})
|
||||
const json = await response.json()
|
||||
|
||||
if (response.status !== 200) throw new Error(json.message)
|
||||
|
||||
data = json.rows || []
|
||||
|
||||
if (data.length === 0) {
|
||||
const response = await queries.preview(query)
|
||||
if (response.rows.length === 0) {
|
||||
notifications.info(
|
||||
"Query results empty. Please execute a query with results to create your schema."
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
data = response.rows
|
||||
fields = response.schema
|
||||
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) {
|
||||
notifications.error(`Query Error: ${err.message}`)
|
||||
console.error(err)
|
||||
notifications.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveQuery() {
|
||||
try {
|
||||
const { _id } = await queries.save(query.datasourceId, query)
|
||||
await updateRole(roleId, _id)
|
||||
saveId = _id
|
||||
notifications.success(`Query saved successfully.`)
|
||||
$goto(`../${_id}`)
|
||||
} catch (err) {
|
||||
|
@ -145,38 +88,6 @@
|
|||
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>
|
||||
|
||||
<Layout gap="S" noPadding>
|
||||
|
@ -200,14 +111,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="config-field">
|
||||
<Label>Access level</Label>
|
||||
<Select
|
||||
value={roleId}
|
||||
on:change={e => updateRole(e.detail)}
|
||||
options={$roles}
|
||||
getOptionLabel={x => x.name}
|
||||
getOptionValue={x => x._id}
|
||||
/>
|
||||
<AccessLevelSelect {saveId} {query} label="Access Level" />
|
||||
</div>
|
||||
{#if integrationInfo?.extra && query.queryVerb}
|
||||
<ExtraQueryConfig
|
||||
|
@ -216,7 +120,7 @@
|
|||
config={integrationInfo.extra}
|
||||
/>
|
||||
{/if}
|
||||
<ParameterBuilder bind:parameters={query.parameters} bindable={false} />
|
||||
<BindingBuilder bind:bindings={query.parameters} bindable={false} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if shouldShowQueryConfig}
|
||||
|
@ -271,29 +175,15 @@
|
|||
{#if data}
|
||||
<Tabs selected="JSON">
|
||||
<Tab title="JSON">
|
||||
<pre
|
||||
class="preview">
|
||||
<!-- prettier-ignore -->
|
||||
{#if !data[0]}
|
||||
Please run your query to fetch some data.
|
||||
{:else}
|
||||
{JSON.stringify(data[0], undefined, 2)}
|
||||
{/if}
|
||||
</pre>
|
||||
<JSONPreview data={data[0]} minHeight="120" />
|
||||
</Tab>
|
||||
<Tab title="Schema">
|
||||
<Layout gap="S">
|
||||
{#each fields as field, idx}
|
||||
<div class="field">
|
||||
<Input placeholder="Field Name" bind:value={field.name} />
|
||||
<Select bind:value={field.type} options={typeOptions} />
|
||||
<Icon name="bleClose" on:click={() => deleteField(idx)} />
|
||||
</div>
|
||||
{/each}
|
||||
<div>
|
||||
<Button secondary on:click={newField}>Add Field</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
<KeyValueBuilder
|
||||
bind:object={fields}
|
||||
name="field"
|
||||
headings
|
||||
options={SchemaTypeOptions}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab title="Preview">
|
||||
<ExternalDataSourceTable {query} {data} />
|
||||
|
@ -322,29 +212,11 @@
|
|||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 5%;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
.viewer {
|
||||
min-height: 200px;
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -154,3 +154,58 @@ export const ALLOWABLE_NUMBER_TYPES = ALLOWABLE_NUMBER_OPTIONS.map(
|
|||
export const SWITCHABLE_TYPES = ALLOWABLE_NUMBER_TYPES.concat(
|
||||
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",
|
||||
}
|
||||
|
||||
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
|
||||
export const UNEDITABLE_USER_FIELDS = [
|
||||
"email",
|
||||
|
|
|
@ -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 lowercase = s => s.substring(0, 1).toLowerCase() + s.substring(1)
|
||||
|
||||
export const get_name = s => (!s ? "" : last(s.split("/")))
|
||||
|
||||
export const get_capitalised_name = name => pipe(name, [get_name, capitalise])
|
||||
|
|
|
@ -6,4 +6,5 @@ export {
|
|||
capitalise,
|
||||
get_name,
|
||||
get_capitalised_name,
|
||||
lowercase,
|
||||
} from "./helpers"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
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 DeployModal from "components/deploy/DeployModal.svelte"
|
||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||
|
@ -49,6 +49,7 @@
|
|||
}
|
||||
await automationStore.actions.fetch()
|
||||
await roles.fetch()
|
||||
await flags.fetch()
|
||||
return pkg
|
||||
} else {
|
||||
throw new Error(pkg)
|
||||
|
|
|
@ -1,13 +1,23 @@
|
|||
<script>
|
||||
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) {
|
||||
const query = $queries.list.find(q => q._id === $params.query)
|
||||
if (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>
|
||||
|
||||
<slot />
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<script>
|
||||
import { params } from "@roxi/routify"
|
||||
import { database, queries } from "stores/backend"
|
||||
import { params, goto } from "@roxi/routify"
|
||||
import { database, datasources, queries } from "stores/backend"
|
||||
import QueryInterface from "components/integration/QueryViewer.svelte"
|
||||
import { IntegrationTypes } from "constants/backend"
|
||||
|
||||
let selectedQuery, datasource
|
||||
$: selectedQuery = $queries.list.find(
|
||||
query => query._id === $queries.selected
|
||||
) || {
|
||||
|
@ -11,6 +13,14 @@
|
|||
fields: {},
|
||||
queryVerb: "read",
|
||||
}
|
||||
$: datasource = $datasources.list.find(
|
||||
ds => ds._id === $params.selectedDatasource
|
||||
)
|
||||
$: {
|
||||
if (datasource?.source === IntegrationTypes.REST) {
|
||||
$goto(`../rest/${$params.query}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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,
|
||||
Divider,
|
||||
Layout,
|
||||
notifications,
|
||||
Table,
|
||||
Modal,
|
||||
InlineAlert,
|
||||
ActionButton,
|
||||
} from "@budibase/bbui"
|
||||
import { datasources, integrations, queries, tables } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
|
||||
import CreateExternalTableModal from "./modals/CreateExternalTableModal.svelte"
|
||||
import RestExtraConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/RestExtraConfigForm.svelte"
|
||||
import PlusConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/PlusConfigForm.svelte"
|
||||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||
import { capitalise } from "helpers"
|
||||
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
||||
import VerbRenderer from "./_components/VerbRenderer.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 relationshipModal
|
||||
let createExternalTableModal
|
||||
let selectedFromRelationship, selectedToRelationship
|
||||
|
||||
let baseDatasource, changed
|
||||
const querySchema = {
|
||||
name: {},
|
||||
queryVerb: { displayName: "Method" },
|
||||
}
|
||||
|
||||
$: datasource = $datasources.list.find(ds => ds._id === $datasources.selected)
|
||||
$: integration = datasource && $integrations[datasource.source]
|
||||
$: plusTables = datasource?.plus
|
||||
? Object.values(datasource.entities || {})
|
||||
: []
|
||||
$: relationships = getRelationships(plusTables)
|
||||
$: schemaError = $datasources.schemaError
|
||||
|
||||
function getRelationships(tables) {
|
||||
if (!tables || !Array.isArray(tables)) {
|
||||
return {}
|
||||
$: {
|
||||
if (
|
||||
datasource &&
|
||||
(!baseDatasource || baseDatasource.source !== datasource.source)
|
||||
) {
|
||||
baseDatasource = cloneDeep(datasource)
|
||||
}
|
||||
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
|
||||
}
|
||||
$: queryList = $queries.list.filter(
|
||||
query => query.datasourceId === datasource?._id
|
||||
)
|
||||
$: hasChanged(baseDatasource, datasource)
|
||||
|
||||
function buildRelationshipDisplayString(fromCol, toCol) {
|
||||
function getTableName(tableId) {
|
||||
if (!tableId || typeof tableId !== "string") {
|
||||
return null
|
||||
}
|
||||
return plusTables.find(table => table._id === tableId)?.name || "Unknown"
|
||||
function hasChanged(base, ds) {
|
||||
if (base && ds) {
|
||||
changed = !isEqual(base, ds)
|
||||
}
|
||||
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() {
|
||||
|
@ -86,53 +59,18 @@
|
|||
}
|
||||
await datasources.fetch()
|
||||
notifications.success(`Datasource ${name} updated successfully.`)
|
||||
baseDatasource = cloneDeep(datasource)
|
||||
} catch (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) {
|
||||
queries.select(query)
|
||||
$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>
|
||||
|
||||
<Modal bind:this={relationshipModal}>
|
||||
<CreateEditRelationship
|
||||
{datasource}
|
||||
save={saveDatasource}
|
||||
close={relationshipModal.hide}
|
||||
{plusTables}
|
||||
fromRelationship={selectedFromRelationship}
|
||||
toRelationship={selectedToRelationship}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={importQueriesModal}>
|
||||
{#if datasource.source === "REST"}
|
||||
<ImportRestQueriesModal
|
||||
|
@ -142,10 +80,6 @@
|
|||
{/if}
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={createExternalTableModal}>
|
||||
<CreateExternalTableModal {datasource} />
|
||||
</Modal>
|
||||
|
||||
{#if datasource && integration}
|
||||
<section>
|
||||
<Layout>
|
||||
|
@ -156,125 +90,58 @@
|
|||
height="26"
|
||||
width="26"
|
||||
/>
|
||||
<Heading size="M">{datasource.name}</Heading>
|
||||
<Heading size="M">{baseDatasource.name}</Heading>
|
||||
</header>
|
||||
<Body size="M">{integration.description}</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
<div class="container">
|
||||
<div class="config-header">
|
||||
<Heading size="S">Configuration</Heading>
|
||||
<Button secondary on:click={saveDatasource}>Save</Button>
|
||||
</div>
|
||||
<Body size="S">
|
||||
Connect your database to Budibase using the config below.
|
||||
</Body>
|
||||
<IntegrationConfigForm
|
||||
schema={integration.datasource}
|
||||
integration={datasource.config}
|
||||
/>
|
||||
<Divider size="S" />
|
||||
<div class="config-header">
|
||||
<Heading size="S">Configuration</Heading>
|
||||
<Button disabled={!changed} cta on:click={saveDatasource}>Save</Button>
|
||||
</div>
|
||||
<IntegrationConfigForm
|
||||
on:change={hasChanged}
|
||||
schema={integration.datasource}
|
||||
bind:datasource
|
||||
/>
|
||||
{#if datasource.plus}
|
||||
<Divider />
|
||||
<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}
|
||||
<div class="query-list">
|
||||
{#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>
|
||||
<PlusConfigForm bind:datasource save={saveDatasource} />
|
||||
{/if}
|
||||
<Divider />
|
||||
<Divider size="S" />
|
||||
<div class="query-header">
|
||||
<Heading size="S">Queries</Heading>
|
||||
<div class="query-buttons">
|
||||
{#if datasource.source === "REST"}
|
||||
{#if datasource?.source === IntegrationTypes.REST}
|
||||
<Button secondary on:click={() => importQueriesModal.show()}
|
||||
>Import</Button
|
||||
>
|
||||
{/if}
|
||||
<Button secondary on:click={() => $goto("./new")}>Add Query</Button>
|
||||
<Button cta icon="Add" on:click={() => $goto("./new")}
|
||||
>Add query
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="query-list">
|
||||
{#each $queries.list.filter(query => query.datasourceId === datasource._id) as query}
|
||||
<div class="query-list-item" on:click={() => onClickQuery(query)}>
|
||||
<p class="query-name">{query.name}</p>
|
||||
<p>{capitalise(query.queryVerb)}</p>
|
||||
<p>→</p>
|
||||
</div>
|
||||
{/each}
|
||||
</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">
|
||||
<Table
|
||||
on:click={({ detail }) => onClickQuery(detail)}
|
||||
schema={querySchema}
|
||||
data={queryList}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={false}
|
||||
customRenderers={[{ column: "queryVerb", component: VerbRenderer }]}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if datasource?.source === IntegrationTypes.REST}
|
||||
<RestExtraConfigForm bind:datasource on:change={hasChanged} />
|
||||
{/if}
|
||||
</Layout>
|
||||
</section>
|
||||
{/if}
|
||||
|
@ -297,18 +164,11 @@
|
|||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
border-radius: var(--border-radius-m);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.query-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0 0 var(--spacing-s) 0;
|
||||
}
|
||||
|
||||
.query-buttons {
|
||||
|
@ -321,48 +181,4 @@
|
|||
flex-direction: column;
|
||||
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>
|
||||
|
|
|
@ -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>
|
|
@ -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 { integrations } from "./integrations"
|
||||
export { queries } from "./queries"
|
||||
export { flags } from "./flags"
|
||||
|
|
|
@ -78,6 +78,35 @@ export function createQueriesStore() {
|
|||
unselect: () => {
|
||||
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 => {
|
||||
const response = await api.delete(
|
||||
`/api/queries/${query._id}/${query._rev}`
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "1.0.18",
|
||||
"version": "1.0.19-alpha.0",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "1.0.18",
|
||||
"version": "1.0.19-alpha.0",
|
||||
"license": "MPL-2.0",
|
||||
"module": "dist/budibase-client.js",
|
||||
"main": "dist/budibase-client.js",
|
||||
|
@ -19,9 +19,9 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.18",
|
||||
"@budibase/bbui": "^1.0.19-alpha.0",
|
||||
"@budibase/standard-components": "^0.9.139",
|
||||
"@budibase/string-templates": "^1.0.18",
|
||||
"@budibase/string-templates": "^1.0.19-alpha.0",
|
||||
"regexparam": "^1.3.0",
|
||||
"shortid": "^2.2.15",
|
||||
"svelte-spa-router": "^3.0.5"
|
||||
|
|
|
@ -6,6 +6,9 @@ module FetchMock {
|
|||
return {
|
||||
status,
|
||||
headers: {
|
||||
raw: () => {
|
||||
return { "content-type": ["application/json"] }
|
||||
},
|
||||
get: () => {
|
||||
return ["application/json"]
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "1.0.18",
|
||||
"version": "1.0.19-alpha.0",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/index.ts",
|
||||
"repository": {
|
||||
|
@ -70,9 +70,9 @@
|
|||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.0.3",
|
||||
"@budibase/auth": "^1.0.18",
|
||||
"@budibase/client": "^1.0.18",
|
||||
"@budibase/string-templates": "^1.0.18",
|
||||
"@budibase/auth": "^1.0.19-alpha.0",
|
||||
"@budibase/client": "^1.0.19-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.19-alpha.0",
|
||||
"@bull-board/api": "^3.7.0",
|
||||
"@bull-board/koa": "^3.7.0",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
|
||||
import { Query, QueryParameter } from "../../../../../../definitions/common"
|
||||
import { Query, QueryParameter } from "../../../../../../definitions/datasource"
|
||||
|
||||
export interface ImportInfo {
|
||||
url: string
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ImportInfo } from "./base"
|
||||
import { Query, QueryParameter } from "../../../../../definitions/common"
|
||||
import { Query, QueryParameter } from "../../../../../definitions/datasource"
|
||||
import { OpenAPIV2 } from "openapi-types"
|
||||
import { OpenAPISource } from "./base/openapi"
|
||||
|
||||
|
|
|
@ -123,7 +123,7 @@ async function enrichQueryFields(fields, parameters = {}) {
|
|||
enrichedQuery.requestBody
|
||||
)
|
||||
} catch (err) {
|
||||
throw { message: `JSON Invalid - error: ${err}` }
|
||||
// no json found, ignore
|
||||
}
|
||||
delete enrichedQuery.customData
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ exports.preview = async function (ctx) {
|
|||
const enrichedQuery = await enrichQueryFields(fields, parameters)
|
||||
|
||||
try {
|
||||
const { rows, keys } = await Runner.run({
|
||||
const { rows, keys, info, extra } = await Runner.run({
|
||||
datasource,
|
||||
queryVerb,
|
||||
query: enrichedQuery,
|
||||
|
@ -161,6 +161,8 @@ exports.preview = async function (ctx) {
|
|||
ctx.body = {
|
||||
rows,
|
||||
schemaFields: [...new Set(keys)],
|
||||
info,
|
||||
extra,
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.throw(400, err)
|
||||
|
|
|
@ -19,6 +19,7 @@ exports.queryValidation = () => {
|
|||
extra: Joi.object().optional(),
|
||||
schema: Joi.object({}).required().unknown(true),
|
||||
transformer: Joi.string().optional(),
|
||||
flags: Joi.object().optional(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ const CouchDB = require("../../db")
|
|||
const {
|
||||
generateUserMetadataID,
|
||||
getUserMetadataParams,
|
||||
generateUserFlagID,
|
||||
} = require("../../db/utils")
|
||||
const { InternalTables } = require("../../db/utils")
|
||||
const { getGlobalUsers, getRawGlobalUser } = require("../../utilities/global")
|
||||
|
@ -195,3 +196,35 @@ exports.destroyMetadata = async function (ctx) {
|
|||
exports.findMetadata = async function (ctx) {
|
||||
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),
|
||||
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
|
||||
|
|
|
@ -40,6 +40,7 @@ const DocumentTypes = {
|
|||
DEPLOYMENTS: "deployments",
|
||||
METADATA: "metadata",
|
||||
MEM_VIEW: "view",
|
||||
USER_FLAG: "flag",
|
||||
}
|
||||
|
||||
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) => {
|
||||
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
|
||||
_rev?: string
|
||||
}
|
||||
|
@ -93,39 +93,3 @@ export interface Automation extends Base {
|
|||
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 {
|
||||
CREATE = "CREATE",
|
||||
|
@ -181,3 +181,52 @@ export interface SqlQuery {
|
|||
export interface QueryOptions {
|
||||
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 {
|
||||
create?(query: any): Promise<any[]>
|
||||
read?(query: any): Promise<any[]>
|
||||
update?(query: any): Promise<any[]>
|
||||
delete?(query: any): Promise<any[]>
|
||||
create?(query: any): Promise<any[]|any>
|
||||
read?(query: any): Promise<any[]|any>
|
||||
update?(query: any): Promise<any[]|any>
|
||||
delete?(query: any): Promise<any[]|any>
|
||||
}
|
||||
|
|
|
@ -2,29 +2,58 @@ import {
|
|||
Integration,
|
||||
DatasourceFieldTypes,
|
||||
QueryTypes,
|
||||
RestConfig,
|
||||
RestQueryFields as RestQuery,
|
||||
} from "../definitions/datasource"
|
||||
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 {
|
||||
const fetch = require("node-fetch")
|
||||
|
||||
interface RestConfig {
|
||||
url: string
|
||||
defaultHeaders: {
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
const { formatBytes } = require("../utilities")
|
||||
const { performance } = require("perf_hooks")
|
||||
|
||||
const SCHEMA: Integration = {
|
||||
docs: "https://github.com/node-fetch/node-fetch",
|
||||
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",
|
||||
datasource: {
|
||||
url: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
default: "localhost",
|
||||
required: true,
|
||||
default: "",
|
||||
required: false,
|
||||
deprecated: true,
|
||||
},
|
||||
defaultHeaders: {
|
||||
type: DatasourceFieldTypes.OBJECT,
|
||||
|
@ -37,97 +66,30 @@ module RestModule {
|
|||
readable: true,
|
||||
displayName: "POST",
|
||||
type: QueryTypes.FIELDS,
|
||||
urlDisplay: true,
|
||||
fields: {
|
||||
path: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
},
|
||||
queryString: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
},
|
||||
headers: {
|
||||
type: DatasourceFieldTypes.OBJECT,
|
||||
},
|
||||
requestBody: {
|
||||
type: DatasourceFieldTypes.JSON,
|
||||
},
|
||||
},
|
||||
fields: coreFields,
|
||||
},
|
||||
read: {
|
||||
displayName: "GET",
|
||||
readable: true,
|
||||
type: QueryTypes.FIELDS,
|
||||
urlDisplay: true,
|
||||
fields: {
|
||||
path: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
},
|
||||
queryString: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
},
|
||||
headers: {
|
||||
type: DatasourceFieldTypes.OBJECT,
|
||||
},
|
||||
},
|
||||
fields: coreFields,
|
||||
},
|
||||
update: {
|
||||
displayName: "PUT",
|
||||
readable: true,
|
||||
type: QueryTypes.FIELDS,
|
||||
urlDisplay: true,
|
||||
fields: {
|
||||
path: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
},
|
||||
queryString: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
},
|
||||
headers: {
|
||||
type: DatasourceFieldTypes.OBJECT,
|
||||
},
|
||||
requestBody: {
|
||||
type: DatasourceFieldTypes.JSON,
|
||||
},
|
||||
},
|
||||
fields: coreFields,
|
||||
},
|
||||
patch: {
|
||||
displayName: "PATCH",
|
||||
readable: true,
|
||||
type: QueryTypes.FIELDS,
|
||||
urlDisplay: true,
|
||||
fields: {
|
||||
path: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
},
|
||||
queryString: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
},
|
||||
headers: {
|
||||
type: DatasourceFieldTypes.OBJECT,
|
||||
},
|
||||
requestBody: {
|
||||
type: DatasourceFieldTypes.JSON,
|
||||
},
|
||||
},
|
||||
fields: coreFields,
|
||||
},
|
||||
delete: {
|
||||
displayName: "DELETE",
|
||||
type: QueryTypes.FIELDS,
|
||||
urlDisplay: true,
|
||||
fields: {
|
||||
path: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
},
|
||||
queryString: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
},
|
||||
headers: {
|
||||
type: DatasourceFieldTypes.OBJECT,
|
||||
},
|
||||
requestBody: {
|
||||
type: DatasourceFieldTypes.JSON,
|
||||
},
|
||||
},
|
||||
fields: coreFields,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -137,94 +99,106 @@ module RestModule {
|
|||
private headers: {
|
||||
[key: string]: string
|
||||
} = {}
|
||||
private startTimeMs: number = performance.now()
|
||||
|
||||
constructor(config: RestConfig) {
|
||||
this.config = config
|
||||
}
|
||||
|
||||
async parseResponse(response: any) {
|
||||
let data, raw, headers
|
||||
const contentType = response.headers.get("content-type")
|
||||
if (contentType && contentType.indexOf("application/json") !== -1) {
|
||||
return await response.json()
|
||||
data = await response.json()
|
||||
raw = JSON.stringify(data)
|
||||
} 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 {
|
||||
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.config.defaultHeaders,
|
||||
...headers,
|
||||
}
|
||||
|
||||
const response = await fetch(this.getUrl(path, queryString), {
|
||||
method: "POST",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(json),
|
||||
})
|
||||
if (disabledHeaders) {
|
||||
for (let headerKey of Object.keys(this.headers)) {
|
||||
if (disabledHeaders[headerKey]) {
|
||||
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)
|
||||
}
|
||||
|
||||
async read({ path = "", queryString = "", headers = {} }) {
|
||||
this.headers = {
|
||||
...this.config.defaultHeaders,
|
||||
...headers,
|
||||
}
|
||||
|
||||
const response = await fetch(this.getUrl(path, queryString), {
|
||||
headers: this.headers,
|
||||
})
|
||||
|
||||
return await this.parseResponse(response)
|
||||
async create(opts: RestQuery) {
|
||||
return this._req({ ...opts, method: "POST" })
|
||||
}
|
||||
|
||||
async update({ path = "", queryString = "", headers = {}, json = {} }) {
|
||||
this.headers = {
|
||||
...this.config.defaultHeaders,
|
||||
...headers,
|
||||
}
|
||||
|
||||
const response = await fetch(this.getUrl(path, queryString), {
|
||||
method: "PUT",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(json),
|
||||
})
|
||||
|
||||
return await this.parseResponse(response)
|
||||
async read(opts: RestQuery) {
|
||||
return this._req({ ...opts, method: "GET" })
|
||||
}
|
||||
|
||||
async patch({ path = "", queryString = "", headers = {}, json = {} }) {
|
||||
this.headers = {
|
||||
...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 update(opts: RestQuery) {
|
||||
return this._req({ ...opts, method: "PUT" })
|
||||
}
|
||||
|
||||
async delete({ path = "", queryString = "", headers = {} }) {
|
||||
this.headers = {
|
||||
...this.config.defaultHeaders,
|
||||
...headers,
|
||||
}
|
||||
async patch(opts: RestQuery) {
|
||||
return this._req({ ...opts, method: "PATCH" })
|
||||
}
|
||||
|
||||
const response = await fetch(this.getUrl(path, queryString), {
|
||||
method: "DELETE",
|
||||
headers: this.headers,
|
||||
})
|
||||
|
||||
return await this.parseResponse(response)
|
||||
async delete(opts: RestQuery) {
|
||||
return this._req({ ...opts, method: "DELETE" })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
jest.mock("node-fetch", () =>
|
||||
jest.fn(() => ({
|
||||
headers: {
|
||||
raw: () => {
|
||||
return { "content-type": ["application/json"] }
|
||||
},
|
||||
get: () => ["application/json"]
|
||||
},
|
||||
json: jest.fn(),
|
||||
|
@ -24,6 +27,7 @@ describe("REST Integration", () => {
|
|||
config = new TestConfiguration({
|
||||
url: BASE_URL,
|
||||
})
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls the create method with the correct params", async () => {
|
||||
|
@ -33,9 +37,10 @@ describe("REST Integration", () => {
|
|||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
json: {
|
||||
bodyType: "json",
|
||||
requestBody: JSON.stringify({
|
||||
name: "test",
|
||||
},
|
||||
}),
|
||||
}
|
||||
const response = await config.integration.create(query)
|
||||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
|
||||
|
@ -60,6 +65,7 @@ describe("REST Integration", () => {
|
|||
headers: {
|
||||
Accept: "text/html",
|
||||
},
|
||||
method: "GET",
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -70,13 +76,14 @@ describe("REST Integration", () => {
|
|||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
json: {
|
||||
bodyType: "json",
|
||||
requestBody: JSON.stringify({
|
||||
name: "test",
|
||||
},
|
||||
}),
|
||||
}
|
||||
const response = await config.integration.update(query)
|
||||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
|
||||
method: "POST",
|
||||
method: "PUT",
|
||||
body: '{"name":"test"}',
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
|
@ -91,9 +98,10 @@ describe("REST Integration", () => {
|
|||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
json: {
|
||||
bodyType: "json",
|
||||
requestBody: JSON.stringify({
|
||||
name: "test",
|
||||
},
|
||||
}),
|
||||
}
|
||||
const response = await config.integration.delete(query)
|
||||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
|
||||
|
@ -101,6 +109,7 @@ describe("REST Integration", () => {
|
|||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: '{"name":"test"}',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -15,6 +15,15 @@ function formatResponse(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) {
|
||||
const Integration = integrations[datasource.source]
|
||||
if (!Integration) {
|
||||
|
@ -22,7 +31,15 @@ async function runAndTransform(datasource, queryVerb, query, transformer) {
|
|||
}
|
||||
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
|
||||
if (transformer) {
|
||||
|
@ -47,7 +64,7 @@ async function runAndTransform(datasource, queryVerb, query, transformer) {
|
|||
integration.end()
|
||||
}
|
||||
|
||||
return { rows, keys }
|
||||
return { rows, keys, info, extra }
|
||||
}
|
||||
|
||||
module.exports = (input, callback) => {
|
||||
|
|
|
@ -150,3 +150,14 @@ exports.doesDatabaseExist = async dbName => {
|
|||
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",
|
||||
"version": "1.0.18",
|
||||
"version": "1.0.19-alpha.0",
|
||||
"description": "Handlebars wrapper for Budibase templating.",
|
||||
"main": "src/index.cjs",
|
||||
"module": "dist/bundle.mjs",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/worker",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "1.0.18",
|
||||
"version": "1.0.19-alpha.0",
|
||||
"description": "Budibase background service",
|
||||
"main": "src/index.js",
|
||||
"repository": {
|
||||
|
@ -29,8 +29,8 @@
|
|||
"author": "Budibase",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@budibase/auth": "^1.0.18",
|
||||
"@budibase/string-templates": "^1.0.18",
|
||||
"@budibase/auth": "^1.0.19-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.19-alpha.0",
|
||||
"@koa/router": "^8.0.0",
|
||||
"@sentry/node": "^6.0.0",
|
||||
"@techpass/passport-openidconnect": "^0.3.0",
|
||||
|
|
Loading…
Reference in New Issue