Refactor Datasource Creation Modal (#10783)
This commit is contained in:
parent
798d5d13ab
commit
1606ca0c84
|
@ -12,7 +12,6 @@
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: updateSelected(selectedBooleans)
|
$: updateSelected(selectedBooleans)
|
||||||
$: dispatch("change", selected)
|
|
||||||
$: allSelected = selected?.length === options.length
|
$: allSelected = selected?.length === options.length
|
||||||
$: noneSelected = !selected?.length
|
$: noneSelected = !selected?.length
|
||||||
|
|
||||||
|
@ -28,6 +27,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
selected = array
|
selected = array
|
||||||
|
dispatch("change", selected)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelectAll() {
|
function toggleSelectAll() {
|
||||||
|
@ -36,6 +36,7 @@
|
||||||
} else {
|
} else {
|
||||||
selectedBooleans = reset()
|
selectedBooleans = reset()
|
||||||
}
|
}
|
||||||
|
dispatch("change", selected)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
"dev:builder": "routify -c dev:vite",
|
"dev:builder": "routify -c dev:vite",
|
||||||
"dev:vite": "vite --host 0.0.0.0",
|
"dev:vite": "vite --host 0.0.0.0",
|
||||||
"rollup": "rollup -c -w",
|
"rollup": "rollup -c -w",
|
||||||
"test": "vitest run"
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"globals": {
|
"globals": {
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
<script>
|
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
import { Heading, Detail } from "@budibase/bbui"
|
|
||||||
import IntegrationIcon from "../IntegrationIcon.svelte"
|
|
||||||
|
|
||||||
export let integration
|
|
||||||
export let integrationType
|
|
||||||
export let schema
|
|
||||||
|
|
||||||
let dispatcher = createEventDispatcher()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class:selected={integration.type === integrationType}
|
|
||||||
on:click={() => dispatcher("selected", integrationType)}
|
|
||||||
class="item hoverable"
|
|
||||||
>
|
|
||||||
<div class="item-body" class:with-type={!!schema.type}>
|
|
||||||
<IntegrationIcon {integrationType} {schema} size="25" />
|
|
||||||
<div class="text">
|
|
||||||
<Heading size="XXS">{schema.friendlyName}</Heading>
|
|
||||||
{#if schema.type}
|
|
||||||
<Detail size="S">{schema.type || ""}</Detail>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.item {
|
|
||||||
cursor: pointer;
|
|
||||||
display: grid;
|
|
||||||
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
|
|
||||||
padding: var(--spectrum-alias-item-padding-s)
|
|
||||||
var(--spectrum-alias-item-padding-m);
|
|
||||||
background: var(--spectrum-alias-background-color-secondary);
|
|
||||||
transition: background 0.13s ease-out;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-width: 2px;
|
|
||||||
}
|
|
||||||
.item:hover,
|
|
||||||
.item.selected {
|
|
||||||
background: var(--spectrum-alias-background-color-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
|
||||||
.item-body.with-type {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.item-body.with-type :global(svg) {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,145 +0,0 @@
|
||||||
<script>
|
|
||||||
export let width = 100
|
|
||||||
export let height = 100
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svg
|
|
||||||
{width}
|
|
||||||
{height}
|
|
||||||
viewBox="0 0 46 46"
|
|
||||||
version="1.1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
|
|
||||||
>
|
|
||||||
<!-- Generator: Sketch 3.3.3 (12081) - http://www.bohemiancoding.com/sketch -->
|
|
||||||
<title>btn_google_dark_normal_ios</title>
|
|
||||||
<desc>Created with Sketch.</desc>
|
|
||||||
<defs>
|
|
||||||
<filter
|
|
||||||
x="-50%"
|
|
||||||
y="-50%"
|
|
||||||
width="200%"
|
|
||||||
height="200%"
|
|
||||||
filterUnits="objectBoundingBox"
|
|
||||||
id="filter-1"
|
|
||||||
>
|
|
||||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1" />
|
|
||||||
<feGaussianBlur
|
|
||||||
stdDeviation="0.5"
|
|
||||||
in="shadowOffsetOuter1"
|
|
||||||
result="shadowBlurOuter1"
|
|
||||||
/>
|
|
||||||
<feColorMatrix
|
|
||||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.168 0"
|
|
||||||
in="shadowBlurOuter1"
|
|
||||||
type="matrix"
|
|
||||||
result="shadowMatrixOuter1"
|
|
||||||
/>
|
|
||||||
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2" />
|
|
||||||
<feGaussianBlur
|
|
||||||
stdDeviation="0.5"
|
|
||||||
in="shadowOffsetOuter2"
|
|
||||||
result="shadowBlurOuter2"
|
|
||||||
/>
|
|
||||||
<feColorMatrix
|
|
||||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.084 0"
|
|
||||||
in="shadowBlurOuter2"
|
|
||||||
type="matrix"
|
|
||||||
result="shadowMatrixOuter2"
|
|
||||||
/>
|
|
||||||
<feMerge>
|
|
||||||
<feMergeNode in="shadowMatrixOuter1" />
|
|
||||||
<feMergeNode in="shadowMatrixOuter2" />
|
|
||||||
<feMergeNode in="SourceGraphic" />
|
|
||||||
</feMerge>
|
|
||||||
</filter>
|
|
||||||
<rect id="path-2" x="0" y="0" width="40" height="40" rx="2" />
|
|
||||||
<rect id="path-3" x="5" y="5" width="38" height="38" rx="1" />
|
|
||||||
</defs>
|
|
||||||
<g
|
|
||||||
id="Google-Button"
|
|
||||||
stroke="none"
|
|
||||||
stroke-width="1"
|
|
||||||
fill="none"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
sketch:type="MSPage"
|
|
||||||
>
|
|
||||||
<g
|
|
||||||
id="9-PATCH"
|
|
||||||
sketch:type="MSArtboardGroup"
|
|
||||||
transform="translate(-608.000000, -219.000000)"
|
|
||||||
/>
|
|
||||||
<g
|
|
||||||
id="btn_google_dark_normal"
|
|
||||||
sketch:type="MSArtboardGroup"
|
|
||||||
transform="translate(-1.000000, -1.000000)"
|
|
||||||
>
|
|
||||||
<g
|
|
||||||
id="button"
|
|
||||||
sketch:type="MSLayerGroup"
|
|
||||||
transform="translate(4.000000, 4.000000)"
|
|
||||||
filter="url(#filter-1)"
|
|
||||||
>
|
|
||||||
<g id="button-bg">
|
|
||||||
<use
|
|
||||||
fill="#4285F4"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
sketch:type="MSShapeGroup"
|
|
||||||
xlink:href="#path-2"
|
|
||||||
/>
|
|
||||||
<use fill="none" xlink:href="#path-2" />
|
|
||||||
<use fill="none" xlink:href="#path-2" />
|
|
||||||
<use fill="none" xlink:href="#path-2" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g id="button-bg-copy">
|
|
||||||
<use
|
|
||||||
fill="#FFFFFF"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
sketch:type="MSShapeGroup"
|
|
||||||
xlink:href="#path-3"
|
|
||||||
/>
|
|
||||||
<use fill="none" xlink:href="#path-3" />
|
|
||||||
<use fill="none" xlink:href="#path-3" />
|
|
||||||
<use fill="none" xlink:href="#path-3" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="logo_googleg_48dp"
|
|
||||||
sketch:type="MSLayerGroup"
|
|
||||||
transform="translate(15.000000, 15.000000)"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M17.64,9.20454545 C17.64,8.56636364 17.5827273,7.95272727 17.4763636,7.36363636 L9,7.36363636 L9,10.845 L13.8436364,10.845 C13.635,11.97 13.0009091,12.9231818 12.0477273,13.5613636 L12.0477273,15.8195455 L14.9563636,15.8195455 C16.6581818,14.2527273 17.64,11.9454545 17.64,9.20454545 L17.64,9.20454545 Z"
|
|
||||||
id="Shape"
|
|
||||||
fill="#4285F4"
|
|
||||||
sketch:type="MSShapeGroup"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M9,18 C11.43,18 13.4672727,17.1940909 14.9563636,15.8195455 L12.0477273,13.5613636 C11.2418182,14.1013636 10.2109091,14.4204545 9,14.4204545 C6.65590909,14.4204545 4.67181818,12.8372727 3.96409091,10.71 L0.957272727,10.71 L0.957272727,13.0418182 C2.43818182,15.9831818 5.48181818,18 9,18 L9,18 Z"
|
|
||||||
id="Shape"
|
|
||||||
fill="#34A853"
|
|
||||||
sketch:type="MSShapeGroup"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M3.96409091,10.71 C3.78409091,10.17 3.68181818,9.59318182 3.68181818,9 C3.68181818,8.40681818 3.78409091,7.83 3.96409091,7.29 L3.96409091,4.95818182 L0.957272727,4.95818182 C0.347727273,6.17318182 0,7.54772727 0,9 C0,10.4522727 0.347727273,11.8268182 0.957272727,13.0418182 L3.96409091,10.71 L3.96409091,10.71 Z"
|
|
||||||
id="Shape"
|
|
||||||
fill="#FBBC05"
|
|
||||||
sketch:type="MSShapeGroup"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M9,3.57954545 C10.3213636,3.57954545 11.5077273,4.03363636 12.4404545,4.92545455 L15.0218182,2.34409091 C13.4631818,0.891818182 11.4259091,0 9,0 C5.48181818,0 2.43818182,2.01681818 0.957272727,4.95818182 L3.96409091,7.29 C4.67181818,5.16272727 6.65590909,3.57954545 9,3.57954545 L9,3.57954545 Z"
|
|
||||||
id="Shape"
|
|
||||||
fill="#EA4335"
|
|
||||||
sketch:type="MSShapeGroup"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M0,0 L18,0 L18,18 L0,18 L0,0 Z"
|
|
||||||
id="Shape"
|
|
||||||
sketch:type="MSShapeGroup"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g id="handles_square" sketch:type="MSLayerGroup" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
|
@ -1,207 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
Body,
|
|
||||||
FancyCheckboxGroup,
|
|
||||||
InlineAlert,
|
|
||||||
Layout,
|
|
||||||
Link,
|
|
||||||
ModalContent,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { IntegrationNames, IntegrationTypes } from "constants/backend"
|
|
||||||
import GoogleButton from "../_components/GoogleButton.svelte"
|
|
||||||
import { organisation } from "stores/portal"
|
|
||||||
import { onDestroy, onMount } from "svelte"
|
|
||||||
import {
|
|
||||||
getDatasourceInfo,
|
|
||||||
saveDatasource,
|
|
||||||
validateDatasourceConfig,
|
|
||||||
} from "builderStore/datasource"
|
|
||||||
import cloneDeep from "lodash/cloneDeepWith"
|
|
||||||
import IntegrationConfigForm from "../TableIntegrationMenu/IntegrationConfigForm.svelte"
|
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
import { DatasourceFeature } from "@budibase/types"
|
|
||||||
import { API } from "api"
|
|
||||||
|
|
||||||
export let integration
|
|
||||||
export let continueSetupId = false
|
|
||||||
|
|
||||||
let datasource = cloneDeep(integration)
|
|
||||||
datasource.config.continueSetupId = continueSetupId
|
|
||||||
|
|
||||||
let { schema } = datasource
|
|
||||||
|
|
||||||
$: isGoogleConfigured = !!$organisation.googleDatasourceConfigured
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await organisation.init()
|
|
||||||
})
|
|
||||||
|
|
||||||
const integrationName = IntegrationNames[IntegrationTypes.GOOGLE_SHEETS]
|
|
||||||
|
|
||||||
export const GoogleDatasouceConfigStep = {
|
|
||||||
AUTH: "auth",
|
|
||||||
SET_URL: "set_url",
|
|
||||||
SET_SHEETS: "set_sheets",
|
|
||||||
}
|
|
||||||
|
|
||||||
let step = continueSetupId
|
|
||||||
? GoogleDatasouceConfigStep.SET_URL
|
|
||||||
: GoogleDatasouceConfigStep.AUTH
|
|
||||||
|
|
||||||
let isValid = false
|
|
||||||
|
|
||||||
let allSheets
|
|
||||||
let selectedSheets
|
|
||||||
let setSheetsErrorTitle, setSheetsErrorMessage
|
|
||||||
|
|
||||||
$: modalConfig = {
|
|
||||||
[GoogleDatasouceConfigStep.AUTH]: {
|
|
||||||
title: `Connect to ${integrationName}`,
|
|
||||||
},
|
|
||||||
[GoogleDatasouceConfigStep.SET_URL]: {
|
|
||||||
title: `Connect your spreadsheet`,
|
|
||||||
confirmButtonText: "Connect",
|
|
||||||
onConfirm: async () => {
|
|
||||||
const checkConnection =
|
|
||||||
integration.features[DatasourceFeature.CONNECTION_CHECKING]
|
|
||||||
if (checkConnection) {
|
|
||||||
const resp = await validateDatasourceConfig(datasource)
|
|
||||||
if (!resp.connected) {
|
|
||||||
notifications.error(`Unable to connect - ${resp.error}`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
datasource = await saveDatasource(datasource, {
|
|
||||||
tablesFilter: selectedSheets,
|
|
||||||
skipFetch: true,
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
notifications.error(err?.message ?? "Error saving datasource")
|
|
||||||
// prevent the modal from closing
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!integration.features[DatasourceFeature.FETCH_TABLE_NAMES]) {
|
|
||||||
notifications.success(`Datasource created successfully.`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const info = await getDatasourceInfo(datasource)
|
|
||||||
allSheets = info.tableNames
|
|
||||||
|
|
||||||
step = GoogleDatasouceConfigStep.SET_SHEETS
|
|
||||||
notifications.success(
|
|
||||||
checkConnection
|
|
||||||
? "Connection Successful"
|
|
||||||
: `Datasource created successfully.`
|
|
||||||
)
|
|
||||||
|
|
||||||
// prevent the modal from closing
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[GoogleDatasouceConfigStep.SET_SHEETS]: {
|
|
||||||
title: `Choose your sheets`,
|
|
||||||
confirmButtonText: selectedSheets?.length
|
|
||||||
? "Fetch sheets"
|
|
||||||
: "Continue without fetching",
|
|
||||||
onConfirm: async () => {
|
|
||||||
try {
|
|
||||||
if (selectedSheets.length) {
|
|
||||||
await API.buildDatasourceSchema({
|
|
||||||
datasourceId: datasource._id,
|
|
||||||
tablesFilter: selectedSheets,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
} catch (err) {
|
|
||||||
const message = err?.message ?? "Error fetching the sheets"
|
|
||||||
// Handling message with format: Error title - error description
|
|
||||||
const indexSeparator = message.indexOf(" - ")
|
|
||||||
if (indexSeparator >= 0) {
|
|
||||||
setSheetsErrorTitle = message.substr(0, indexSeparator)
|
|
||||||
setSheetsErrorMessage =
|
|
||||||
message[indexSeparator + 3].toUpperCase() +
|
|
||||||
message.substr(indexSeparator + 4)
|
|
||||||
} else {
|
|
||||||
setSheetsErrorTitle = null
|
|
||||||
setSheetsErrorMessage = message
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent the modal from closing
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// This will handle the user closing the modal pressing outside the modal
|
|
||||||
onDestroy(() => {
|
|
||||||
if (step === GoogleDatasouceConfigStep.SET_SHEETS) {
|
|
||||||
$goto(`./datasource/${datasource._id}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ModalContent
|
|
||||||
title={modalConfig[step].title}
|
|
||||||
cancelText="Cancel"
|
|
||||||
size="L"
|
|
||||||
confirmText={modalConfig[step].confirmButtonText}
|
|
||||||
showConfirmButton={!!modalConfig[step].onConfirm}
|
|
||||||
onConfirm={modalConfig[step].onConfirm}
|
|
||||||
disabled={!isValid}
|
|
||||||
>
|
|
||||||
{#if step === GoogleDatasouceConfigStep.AUTH}
|
|
||||||
<!-- check true and false directly, don't render until flag is set -->
|
|
||||||
{#if isGoogleConfigured === true}
|
|
||||||
<Layout noPadding>
|
|
||||||
<Body size="S"
|
|
||||||
>Authenticate with your google account to use the {integrationName} integration.</Body
|
|
||||||
>
|
|
||||||
</Layout>
|
|
||||||
<GoogleButton samePage />
|
|
||||||
{:else if isGoogleConfigured === false}
|
|
||||||
<Body size="S"
|
|
||||||
>Google authentication is not enabled, please complete Google SSO
|
|
||||||
configuration.</Body
|
|
||||||
>
|
|
||||||
<Link href="/builder/portal/settings/auth">Configure Google SSO</Link>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{#if step === GoogleDatasouceConfigStep.SET_URL}
|
|
||||||
<Layout noPadding no>
|
|
||||||
<Body size="S">Add the URL of the sheet you want to connect.</Body>
|
|
||||||
|
|
||||||
<IntegrationConfigForm
|
|
||||||
{schema}
|
|
||||||
bind:datasource
|
|
||||||
creating={true}
|
|
||||||
on:valid={e => (isValid = e.detail)}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
{/if}
|
|
||||||
{#if step === GoogleDatasouceConfigStep.SET_SHEETS}
|
|
||||||
<Layout noPadding no>
|
|
||||||
<Body size="S">Select which spreadsheets you want to connect.</Body>
|
|
||||||
|
|
||||||
<FancyCheckboxGroup
|
|
||||||
options={allSheets}
|
|
||||||
bind:selected={selectedSheets}
|
|
||||||
selectAllText="Select all sheets"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if setSheetsErrorTitle || setSheetsErrorMessage}
|
|
||||||
<InlineAlert
|
|
||||||
type="error"
|
|
||||||
header={setSheetsErrorTitle}
|
|
||||||
message={setSheetsErrorMessage}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</Layout>
|
|
||||||
{/if}
|
|
||||||
</ModalContent>
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
<script>
|
||||||
|
import ObjectField from "./fields/Object.svelte"
|
||||||
|
import BooleanField from "./fields/Boolean.svelte"
|
||||||
|
import LongFormField from "./fields/LongForm.svelte"
|
||||||
|
import FieldGroupField from "./fields/FieldGroup.svelte"
|
||||||
|
import StringField from "./fields/String.svelte"
|
||||||
|
|
||||||
|
export let type
|
||||||
|
export let value
|
||||||
|
export let error
|
||||||
|
export let name
|
||||||
|
export let showModal = () => {}
|
||||||
|
|
||||||
|
const selectComponent = type => {
|
||||||
|
if (type === "object") {
|
||||||
|
return ObjectField
|
||||||
|
} else if (type === "boolean") {
|
||||||
|
return BooleanField
|
||||||
|
} else if (type === "longForm") {
|
||||||
|
return LongFormField
|
||||||
|
} else if (type === "fieldGroup") {
|
||||||
|
return FieldGroupField
|
||||||
|
} else {
|
||||||
|
return StringField
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: component = selectComponent(type)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:component
|
||||||
|
this={component}
|
||||||
|
{type}
|
||||||
|
{value}
|
||||||
|
{error}
|
||||||
|
{name}
|
||||||
|
{showModal}
|
||||||
|
on:blur
|
||||||
|
on:change
|
||||||
|
/>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script>
|
||||||
|
import { Label, Toggle } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
export let name
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<Label>{name}</Label>
|
||||||
|
<Toggle on:blur on:change text="" {value} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20% 1fr;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,47 @@
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { Label, Input, Layout, Accordion } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
export let name
|
||||||
|
|
||||||
|
let dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
const handleChange = (updatedFieldKey, updatedFieldValue) => {
|
||||||
|
const updatedValue = value.map(field => {
|
||||||
|
return {
|
||||||
|
key: field.key,
|
||||||
|
value: field.key === updatedFieldKey ? updatedFieldValue : field.value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch("change", updatedValue)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Accordion
|
||||||
|
initialOpen={Object.values(value).some(properties => !!properties.value)}
|
||||||
|
header={name}
|
||||||
|
>
|
||||||
|
<Layout gap="S">
|
||||||
|
{#each value as field}
|
||||||
|
<div class="form-row">
|
||||||
|
<Label>{field.name}</Label>
|
||||||
|
<Input
|
||||||
|
type={field.type}
|
||||||
|
on:change={e => handleChange(field.key, e.detail)}
|
||||||
|
value={field.value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Layout>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20% 1fr;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script>
|
||||||
|
import { Label, TextArea } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let type
|
||||||
|
export let name
|
||||||
|
export let value
|
||||||
|
export let error
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<Label>{name}</Label>
|
||||||
|
<TextArea on:blur on:change {type} {value} {error} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20% 1fr;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,37 @@
|
||||||
|
<script>
|
||||||
|
import { Label, Button } from "@budibase/bbui"
|
||||||
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
|
|
||||||
|
export let name
|
||||||
|
export let value
|
||||||
|
|
||||||
|
let addButton
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="form-row ssl">
|
||||||
|
<Label>{name}</Label>
|
||||||
|
<Button secondary thin outline on:click={addButton.addEntry()}>Add</Button>
|
||||||
|
</div>
|
||||||
|
<KeyValueBuilder
|
||||||
|
on:change
|
||||||
|
on:blur
|
||||||
|
bind:this={addButton}
|
||||||
|
defaults={value}
|
||||||
|
noAddButton={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20% 1fr;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row.ssl {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20% 20%;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,39 @@
|
||||||
|
<script>
|
||||||
|
import { Label, EnvDropdown } from "@budibase/bbui"
|
||||||
|
import { environment, licensing } from "stores/portal"
|
||||||
|
|
||||||
|
export let type
|
||||||
|
export let name
|
||||||
|
export let value
|
||||||
|
export let error
|
||||||
|
export let showModal = () => {}
|
||||||
|
|
||||||
|
async function handleUpgradePanel() {
|
||||||
|
await environment.upgradePanelOpened()
|
||||||
|
$licensing.goToUpgradePage()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<Label>{name}</Label>
|
||||||
|
<EnvDropdown
|
||||||
|
on:change
|
||||||
|
on:blur
|
||||||
|
type={type === "port" ? "string" : type}
|
||||||
|
{value}
|
||||||
|
{error}
|
||||||
|
variables={$environment.variables}
|
||||||
|
environmentVariablesEnabled={$licensing.environmentVariablesEnabled}
|
||||||
|
{showModal}
|
||||||
|
{handleUpgradePanel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20% 1fr;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,114 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
notifications,
|
||||||
|
Body,
|
||||||
|
Layout,
|
||||||
|
ModalContent,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
|
||||||
|
import ConfigInput from "./ConfigInput.svelte"
|
||||||
|
import { createConfigStore } from "./stores/config"
|
||||||
|
import { createValidationStore } from "./stores/validation"
|
||||||
|
import { createValidatedConfigStore } from "./stores/validatedConfig"
|
||||||
|
import { datasources } from "stores/backend"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { environment } from "stores/portal"
|
||||||
|
|
||||||
|
export let integration
|
||||||
|
export let config
|
||||||
|
export let onDatasourceCreated = () => {}
|
||||||
|
|
||||||
|
$: configStore = createConfigStore(integration, config)
|
||||||
|
$: validationStore = createValidationStore(integration)
|
||||||
|
$: validatedConfigStore = createValidatedConfigStore(
|
||||||
|
configStore,
|
||||||
|
validationStore,
|
||||||
|
integration
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
validationStore.markAllFieldsActive()
|
||||||
|
const config = get(configStore)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (await validationStore.validate(config)) {
|
||||||
|
const datasource = await datasources.create({
|
||||||
|
integration,
|
||||||
|
fields: config,
|
||||||
|
})
|
||||||
|
await onDatasourceCreated(datasource)
|
||||||
|
} else {
|
||||||
|
notifications.send("Invalid fields", {
|
||||||
|
type: "error",
|
||||||
|
icon: "Alert",
|
||||||
|
autoDismiss: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Do nothing on errors, alerts are handled by `datasources.create`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent modal closing
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBlur = key => {
|
||||||
|
validationStore.markFieldActive(key)
|
||||||
|
validationStore.validate(get(configStore))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (key, newValue) => {
|
||||||
|
configStore.updateFieldValue(key, newValue)
|
||||||
|
validationStore.validate(get(configStore))
|
||||||
|
}
|
||||||
|
|
||||||
|
let createVariableModal
|
||||||
|
let selectedConfigKey
|
||||||
|
|
||||||
|
const showModal = key => {
|
||||||
|
selectedConfigKey = key
|
||||||
|
createVariableModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(data) {
|
||||||
|
try {
|
||||||
|
await environment.createVariable(data)
|
||||||
|
configStore.updateFieldValue(selectedConfigKey, `{{ env.${data.name} }}`)
|
||||||
|
createVariableModal.hide()
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Failed to create variable: ${err.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
title={`Connect to ${integration.friendlyName}`}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
confirmText={integration.plus ? "Connect" : "Save and continue to query"}
|
||||||
|
cancelText="Back"
|
||||||
|
disabled={$validationStore.allFieldsActive && $validationStore.invalid}
|
||||||
|
size="L"
|
||||||
|
>
|
||||||
|
<Layout noPadding>
|
||||||
|
<Body size="XS">
|
||||||
|
Connect your database to Budibase using the config below.
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
{#each $validatedConfigStore as { type, key, value, error, name }}
|
||||||
|
<ConfigInput
|
||||||
|
{type}
|
||||||
|
{value}
|
||||||
|
{error}
|
||||||
|
{name}
|
||||||
|
showModal={() => showModal(key)}
|
||||||
|
on:blur={() => handleBlur(key)}
|
||||||
|
on:change={e => handleChange(key, e.detail)}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<Modal bind:this={createVariableModal}>
|
||||||
|
<CreateEditVariableModal {save} />
|
||||||
|
</Modal>
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
export const createConfigStore = (integration, config) => {
|
||||||
|
const configStore = writable(config)
|
||||||
|
|
||||||
|
const updateFieldValue = (key, value) => {
|
||||||
|
configStore.update($configStore => {
|
||||||
|
const newStore = { ...$configStore }
|
||||||
|
|
||||||
|
if (integration.datasource[key].type === "fieldGroup") {
|
||||||
|
value.forEach(field => {
|
||||||
|
newStore[field.key] = field.value
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
newStore[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return newStore
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: configStore.subscribe,
|
||||||
|
updateFieldValue,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
import { derived } from "svelte/store"
|
||||||
|
|
||||||
|
export const createValidatedConfigStore = (
|
||||||
|
configStore,
|
||||||
|
validationStore,
|
||||||
|
integration
|
||||||
|
) => {
|
||||||
|
return derived(
|
||||||
|
[configStore, validationStore],
|
||||||
|
([$configStore, $validationStore]) => {
|
||||||
|
return Object.entries(integration.datasource).map(([key, properties]) => {
|
||||||
|
const getValue = () => {
|
||||||
|
if (properties.type === "fieldGroup") {
|
||||||
|
return Object.entries(properties.fields).map(
|
||||||
|
([fieldKey, fieldProperties]) => {
|
||||||
|
return {
|
||||||
|
key: fieldKey,
|
||||||
|
name: capitalise(fieldProperties.display || fieldKey),
|
||||||
|
type: fieldProperties.type,
|
||||||
|
value: $configStore[fieldKey],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $configStore[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
value: getValue(),
|
||||||
|
error: $validationStore.errors[key],
|
||||||
|
name: capitalise(properties.display || key),
|
||||||
|
type: properties.type,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
import { object, string, number } from "yup"
|
||||||
|
import { derived, writable, get } from "svelte/store"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
|
const propertyValidator = type => {
|
||||||
|
if (type === "number") {
|
||||||
|
return number().nullable()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "email") {
|
||||||
|
return string().email().nullable()
|
||||||
|
}
|
||||||
|
|
||||||
|
return string().nullable()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getValidatorFields = integration => {
|
||||||
|
const validatorFields = {}
|
||||||
|
|
||||||
|
Object.entries(integration?.datasource || {}).forEach(([key, properties]) => {
|
||||||
|
if (properties.required) {
|
||||||
|
validatorFields[key] = propertyValidator(properties.type).required()
|
||||||
|
} else {
|
||||||
|
validatorFields[key] = propertyValidator(properties.type).notRequired()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return validatorFields
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createValidationStore = integration => {
|
||||||
|
const allValidators = getValidatorFields(integration)
|
||||||
|
const selectedValidatorsStore = writable({})
|
||||||
|
const errorsStore = writable({})
|
||||||
|
|
||||||
|
const markAllFieldsActive = () => {
|
||||||
|
selectedValidatorsStore.set(allValidators)
|
||||||
|
}
|
||||||
|
|
||||||
|
const markFieldActive = key => {
|
||||||
|
selectedValidatorsStore.update($validatorsStore => ({
|
||||||
|
...$validatorsStore,
|
||||||
|
[key]: allValidators[key],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const validate = async config => {
|
||||||
|
try {
|
||||||
|
await object()
|
||||||
|
.shape(get(selectedValidatorsStore))
|
||||||
|
.validate(config, { abortEarly: false })
|
||||||
|
|
||||||
|
errorsStore.set({})
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
// Yup error
|
||||||
|
if (error.inner) {
|
||||||
|
const errors = {}
|
||||||
|
|
||||||
|
error.inner.forEach(innerError => {
|
||||||
|
errors[innerError.path] = capitalise(innerError.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
errorsStore.set(errors)
|
||||||
|
} else {
|
||||||
|
// Non-yup error
|
||||||
|
notifications.error("Unexpected validation error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const combined = derived(
|
||||||
|
[errorsStore, selectedValidatorsStore],
|
||||||
|
([$errorsStore, $selectedValidatorsStore]) => {
|
||||||
|
return {
|
||||||
|
errors: $errorsStore,
|
||||||
|
invalid: Object.keys($errorsStore).length > 0,
|
||||||
|
allFieldsActive:
|
||||||
|
Object.keys($selectedValidatorsStore).length ===
|
||||||
|
Object.keys(allValidators).length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: combined.subscribe,
|
||||||
|
markAllFieldsActive,
|
||||||
|
markFieldActive,
|
||||||
|
validate,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script>
|
||||||
|
import { ModalContent, Body, Layout, Link } from "@budibase/bbui"
|
||||||
|
import { organisation } from "stores/portal"
|
||||||
|
import GoogleButton from "./GoogleButton.svelte"
|
||||||
|
|
||||||
|
$: isGoogleConfigured = !!$organisation.googleDatasourceConfigured
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
showConfirmButton={false}
|
||||||
|
title={`Connect to Google Sheets`}
|
||||||
|
cancelText="Cancel"
|
||||||
|
size="L"
|
||||||
|
>
|
||||||
|
<!-- check true and false directly, don't render until flag is set -->
|
||||||
|
{#if isGoogleConfigured === true}
|
||||||
|
<Layout noPadding>
|
||||||
|
<Body size="S"
|
||||||
|
>Authenticate with your Google account to use the Google Sheets
|
||||||
|
integration.</Body
|
||||||
|
>
|
||||||
|
</Layout>
|
||||||
|
<GoogleButton samePage />
|
||||||
|
{:else if isGoogleConfigured === false}
|
||||||
|
<Body size="S"
|
||||||
|
>Google authentication is not enabled, please complete Google SSO
|
||||||
|
configuration.</Body
|
||||||
|
>
|
||||||
|
<Link href="/builder/portal/settings/auth">Configure Google SSO</Link>
|
||||||
|
{/if}
|
||||||
|
</ModalContent>
|
|
@ -0,0 +1,62 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
FancyCheckboxGroup,
|
||||||
|
InlineAlert,
|
||||||
|
Layout,
|
||||||
|
ModalContent,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { IntegrationTypes } from "constants/backend"
|
||||||
|
import { createTableSelectionStore } from "./tableSelectionStore"
|
||||||
|
|
||||||
|
export let integration
|
||||||
|
export let datasource
|
||||||
|
export let onComplete = () => {}
|
||||||
|
|
||||||
|
$: store = createTableSelectionStore(integration, datasource)
|
||||||
|
|
||||||
|
$: isSheets = integration.name === IntegrationTypes.GOOGLE_SHEETS
|
||||||
|
$: tableType = isSheets ? "sheets" : "tables"
|
||||||
|
$: title = `Choose your ${tableType}`
|
||||||
|
|
||||||
|
$: confirmText = $store.hasSelected
|
||||||
|
? `Fetch ${tableType}`
|
||||||
|
: "Continue without fetching"
|
||||||
|
|
||||||
|
$: description = isSheets
|
||||||
|
? "Select which spreadsheets you want to connect."
|
||||||
|
: "Choose what tables you want to sync with Budibase"
|
||||||
|
|
||||||
|
$: selectAllText = isSheets ? "Select all sheets" : "Select all"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
{title}
|
||||||
|
cancelText="Cancel"
|
||||||
|
size="L"
|
||||||
|
{confirmText}
|
||||||
|
onConfirm={() => store.importSelectedTables(onComplete)}
|
||||||
|
disabled={$store.loading}
|
||||||
|
>
|
||||||
|
{#if $store.loading}
|
||||||
|
<p>loading...</p>
|
||||||
|
{:else}
|
||||||
|
<Layout noPadding no>
|
||||||
|
<Body size="S">{description}</Body>
|
||||||
|
|
||||||
|
<FancyCheckboxGroup
|
||||||
|
options={$store.tableNames}
|
||||||
|
selected={$store.selectedTableNames}
|
||||||
|
on:change={e => store.setSelectedTableNames(e.detail)}
|
||||||
|
{selectAllText}
|
||||||
|
/>
|
||||||
|
{#if $store.error}
|
||||||
|
<InlineAlert
|
||||||
|
type="error"
|
||||||
|
header={$store.error.title}
|
||||||
|
message={$store.error.description}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
</ModalContent>
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { derived, writable, get } from "svelte/store"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
import { datasources, ImportTableError } from "stores/backend"
|
||||||
|
|
||||||
|
export const createTableSelectionStore = (integration, datasource) => {
|
||||||
|
const tableNamesStore = writable([])
|
||||||
|
const selectedTableNamesStore = writable([])
|
||||||
|
const errorStore = writable(null)
|
||||||
|
const loadingStore = writable(true)
|
||||||
|
|
||||||
|
datasources.getTableNames(datasource).then(tableNames => {
|
||||||
|
tableNamesStore.set(tableNames)
|
||||||
|
selectedTableNamesStore.set(tableNames)
|
||||||
|
loadingStore.set(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
const setSelectedTableNames = selectedTableNames => {
|
||||||
|
selectedTableNamesStore.set(selectedTableNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
const importSelectedTables = async onComplete => {
|
||||||
|
errorStore.set(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await datasources.updateSchema(datasource, get(selectedTableNamesStore))
|
||||||
|
|
||||||
|
notifications.success(`Tables fetched successfully.`)
|
||||||
|
await onComplete()
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ImportTableError) {
|
||||||
|
errorStore.set(err)
|
||||||
|
} else {
|
||||||
|
notifications.error("Error fetching tables.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent modal closing
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const combined = derived(
|
||||||
|
[tableNamesStore, selectedTableNamesStore, errorStore, loadingStore],
|
||||||
|
([
|
||||||
|
$tableNamesStore,
|
||||||
|
$selectedTableNamesStore,
|
||||||
|
$errorStore,
|
||||||
|
$loadingStore,
|
||||||
|
]) => {
|
||||||
|
return {
|
||||||
|
tableNames: $tableNamesStore,
|
||||||
|
selectedTableNames: $selectedTableNamesStore,
|
||||||
|
error: $errorStore,
|
||||||
|
loading: $loadingStore,
|
||||||
|
hasSelected: $selectedTableNamesStore.length > 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: combined.subscribe,
|
||||||
|
setSelectedTableNames,
|
||||||
|
importSelectedTables,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
<script>
|
||||||
|
import { Modal } from "@budibase/bbui"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import { IntegrationTypes } from "constants/backend"
|
||||||
|
import GoogleAuthPrompt from "./GoogleAuthPrompt.svelte"
|
||||||
|
|
||||||
|
import TableImportSelection from "./TableImportSelection/index.svelte"
|
||||||
|
import DatasourceConfigEditor from "./DatasourceConfigEditor/index.svelte"
|
||||||
|
import { datasources } from "stores/backend"
|
||||||
|
import { createOnGoogleAuthStore } from "./stores/onGoogleAuth.js"
|
||||||
|
import { createDatasourceCreationStore } from "./stores/datasourceCreation.js"
|
||||||
|
import { configFromIntegration } from "stores/selectors"
|
||||||
|
|
||||||
|
export let loading = false
|
||||||
|
const store = createDatasourceCreationStore()
|
||||||
|
const onGoogleAuth = createOnGoogleAuthStore()
|
||||||
|
let modal
|
||||||
|
|
||||||
|
const handleStoreChanges = (store, modal, goto) => {
|
||||||
|
store.stage === null ? modal?.hide() : modal?.show()
|
||||||
|
|
||||||
|
if (store.finished) {
|
||||||
|
goto(`./datasource/${store.datasource._id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: handleStoreChanges($store, modal, $goto)
|
||||||
|
|
||||||
|
export function show(integration) {
|
||||||
|
if (integration.name === IntegrationTypes.REST) {
|
||||||
|
// A REST integration is created immediately, we don't need to display a config modal.
|
||||||
|
loading = true
|
||||||
|
datasources
|
||||||
|
.create({ integration, fields: configFromIntegration(integration) })
|
||||||
|
.then(datasource => {
|
||||||
|
store.setIntegration(integration)
|
||||||
|
store.setDatasource(datasource)
|
||||||
|
})
|
||||||
|
.finally(() => (loading = false))
|
||||||
|
} else if (integration.name === IntegrationTypes.GOOGLE_SHEETS) {
|
||||||
|
// This prompt redirects users to the Google OAuth flow, they'll be returned to this modal afterwards
|
||||||
|
// with query params populated that trigger the `onGoogleAuth` store.
|
||||||
|
store.googleAuthStage()
|
||||||
|
} else {
|
||||||
|
// All other integrations can generate config from data in the integration object.
|
||||||
|
store.setIntegration(integration)
|
||||||
|
store.setConfig(configFromIntegration(integration))
|
||||||
|
store.editConfigStage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Triggers opening the config editor whenever Google OAuth returns the user to the page
|
||||||
|
$: $onGoogleAuth((integration, config) => {
|
||||||
|
store.setIntegration(integration)
|
||||||
|
store.setConfig(config)
|
||||||
|
store.editConfigStage()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal on:hide={store.cancel} bind:this={modal}>
|
||||||
|
{#if $store.stage === "googleAuth"}
|
||||||
|
<GoogleAuthPrompt />
|
||||||
|
{:else if $store.stage === "editConfig"}
|
||||||
|
<DatasourceConfigEditor
|
||||||
|
integration={$store.integration}
|
||||||
|
config={$store.config}
|
||||||
|
onDatasourceCreated={store.setDatasource}
|
||||||
|
/>
|
||||||
|
{:else if $store.stage === "selectTables"}
|
||||||
|
<TableImportSelection
|
||||||
|
integration={$store.integration}
|
||||||
|
datasource={$store.datasource}
|
||||||
|
onComplete={store.markAsFinished}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { get, writable } from "svelte/store"
|
||||||
|
import { shouldIntegrationFetchTableNames } from "stores/selectors"
|
||||||
|
|
||||||
|
export const defaultStore = {
|
||||||
|
finished: false,
|
||||||
|
stage: null,
|
||||||
|
integration: null,
|
||||||
|
config: null,
|
||||||
|
datasource: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createDatasourceCreationStore = () => {
|
||||||
|
const store = writable(defaultStore)
|
||||||
|
|
||||||
|
store.cancel = () => {
|
||||||
|
const $store = get(store)
|
||||||
|
// If the datasource has already been created, mark the store as finished.
|
||||||
|
if ($store.stage === "selectTables") {
|
||||||
|
store.markAsFinished()
|
||||||
|
} else {
|
||||||
|
store.set(defaultStore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used only by Google Sheets
|
||||||
|
store.googleAuthStage = () => {
|
||||||
|
store.update($store => ({
|
||||||
|
...$store,
|
||||||
|
stage: "googleAuth",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
store.setIntegration = integration => {
|
||||||
|
store.update($store => ({
|
||||||
|
...$store,
|
||||||
|
integration,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
store.setConfig = config => {
|
||||||
|
store.update($store => ({
|
||||||
|
...$store,
|
||||||
|
config,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for every flow but REST
|
||||||
|
store.editConfigStage = () => {
|
||||||
|
store.update($store => ({
|
||||||
|
...$store,
|
||||||
|
stage: "editConfig",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
store.setDatasource = datasource => {
|
||||||
|
const $store = get(store)
|
||||||
|
store.set({ ...$store, datasource })
|
||||||
|
|
||||||
|
if (shouldIntegrationFetchTableNames($store.integration)) {
|
||||||
|
store.selectTablesStage()
|
||||||
|
} else {
|
||||||
|
store.markAsFinished()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only used for datasource plus
|
||||||
|
store.selectTablesStage = () => {
|
||||||
|
store.update($store => ({
|
||||||
|
...$store,
|
||||||
|
stage: "selectTables",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
store.markAsFinished = () => {
|
||||||
|
store.update($store => ({
|
||||||
|
...$store,
|
||||||
|
finished: true,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
cancel: store.cancel,
|
||||||
|
googleAuthStage: store.googleAuthStage,
|
||||||
|
setIntegration: store.setIntegration,
|
||||||
|
setConfig: store.setConfig,
|
||||||
|
editConfigStage: store.editConfigStage,
|
||||||
|
setDatasource: store.setDatasource,
|
||||||
|
selectTablesStage: store.selectTablesStage,
|
||||||
|
markAsFinished: store.markAsFinished,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { it, expect, describe, beforeEach, vi } from "vitest"
|
||||||
|
import {
|
||||||
|
defaultStore,
|
||||||
|
createDatasourceCreationStore,
|
||||||
|
} from "./datasourceCreation"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { shouldIntegrationFetchTableNames } from "stores/selectors"
|
||||||
|
|
||||||
|
vi.mock("stores/selectors", () => ({
|
||||||
|
shouldIntegrationFetchTableNames: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe("datasource creation store", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
// eslint-disable-next-line no-import-assign
|
||||||
|
ctx.store = createDatasourceCreationStore()
|
||||||
|
|
||||||
|
ctx.integration = { data: "integration" }
|
||||||
|
ctx.config = { data: "config" }
|
||||||
|
ctx.datasource = { data: "datasource" }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("store creation", () => {
|
||||||
|
it("returns the default values", ctx => {
|
||||||
|
expect(get(ctx.store)).toEqual(defaultStore)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("cancel", () => {
|
||||||
|
describe("when at the `selectTables` stage", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
ctx.store.selectTablesStage()
|
||||||
|
ctx.store.cancel()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("marks the store as finished", ctx => {
|
||||||
|
expect(get(ctx.store)).toEqual({
|
||||||
|
...defaultStore,
|
||||||
|
stage: "selectTables",
|
||||||
|
finished: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("When at any previous stage", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
ctx.store.cancel()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("resets to the default values", ctx => {
|
||||||
|
expect(get(ctx.store)).toEqual(defaultStore)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("googleAuthStage", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
ctx.store.googleAuthStage()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("sets the stage", ctx => {
|
||||||
|
expect(get(ctx.store)).toEqual({ ...defaultStore, stage: "googleAuth" })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("setIntegration", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
ctx.store.setIntegration(ctx.integration)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("sets the integration", ctx => {
|
||||||
|
expect(get(ctx.store)).toEqual({
|
||||||
|
...defaultStore,
|
||||||
|
integration: ctx.integration,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("setConfig", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
ctx.store.setConfig(ctx.config)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("sets the config", ctx => {
|
||||||
|
expect(get(ctx.store)).toEqual({
|
||||||
|
...defaultStore,
|
||||||
|
config: ctx.config,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("editConfigStage", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
ctx.store.editConfigStage()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("sets the stage", ctx => {
|
||||||
|
expect(get(ctx.store)).toEqual({ ...defaultStore, stage: "editConfig" })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("markAsFinished", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
ctx.store.markAsFinished()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("marks the store as finished", ctx => {
|
||||||
|
expect(get(ctx.store)).toEqual({
|
||||||
|
...defaultStore,
|
||||||
|
finished: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { derived } from "svelte/store"
|
||||||
|
import { params } from "@roxi/routify"
|
||||||
|
import { integrations } from "stores/backend"
|
||||||
|
import { IntegrationTypes } from "constants/backend"
|
||||||
|
|
||||||
|
export const createOnGoogleAuthStore = () => {
|
||||||
|
return derived([params, integrations], ([$params, $integrations]) => {
|
||||||
|
const id = $params["?continue_google_setup"]
|
||||||
|
|
||||||
|
return callback => {
|
||||||
|
if ($integrations && id) {
|
||||||
|
history.replaceState({}, null, window.location.pathname)
|
||||||
|
const integration = {
|
||||||
|
name: IntegrationTypes.GOOGLE_SHEETS,
|
||||||
|
...$integrations[IntegrationTypes.GOOGLE_SHEETS],
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = { continueSetupId: id, sheetId: "" }
|
||||||
|
|
||||||
|
callback(integration, fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { it, expect, describe, beforeEach, vi } from "vitest"
|
||||||
|
import { createOnGoogleAuthStore } from "./onGoogleAuth"
|
||||||
|
import { writable, get } from "svelte/store"
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { params } from "@roxi/routify"
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { integrations } from "stores/backend"
|
||||||
|
import { IntegrationTypes } from "constants/backend"
|
||||||
|
|
||||||
|
vi.mock("@roxi/routify", () => ({
|
||||||
|
params: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("stores/backend", () => ({
|
||||||
|
integrations: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.stubGlobal("history", { replaceState: vi.fn() })
|
||||||
|
vi.stubGlobal("window", { location: { pathname: "/current-path" } })
|
||||||
|
|
||||||
|
describe("google auth store", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
// eslint-disable-next-line no-import-assign
|
||||||
|
integrations = writable({
|
||||||
|
[IntegrationTypes.GOOGLE_SHEETS]: { data: "integration" },
|
||||||
|
})
|
||||||
|
ctx.callback = vi.fn()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("with id present", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
// eslint-disable-next-line no-import-assign
|
||||||
|
params = writable({ "?continue_google_setup": "googleId" })
|
||||||
|
get(createOnGoogleAuthStore())(ctx.callback)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("invokes the provided callback with an integration and fields", ctx => {
|
||||||
|
expect(ctx.callback).toHaveBeenCalledTimes(1)
|
||||||
|
expect(ctx.callback).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
name: IntegrationTypes.GOOGLE_SHEETS,
|
||||||
|
data: "integration",
|
||||||
|
},
|
||||||
|
{ continueSetupId: "googleId", sheetId: "" }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("clears the query param", () => {
|
||||||
|
expect(history.replaceState).toHaveBeenCalledTimes(1)
|
||||||
|
expect(history.replaceState).toHaveBeenCalledWith(
|
||||||
|
{},
|
||||||
|
null,
|
||||||
|
`/current-path`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("without id present", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
// eslint-disable-next-line no-import-assign
|
||||||
|
params = writable({})
|
||||||
|
get(createOnGoogleAuthStore())(ctx.callback)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("doesn't invoke the provided callback", ctx => {
|
||||||
|
expect(ctx.callback).toHaveBeenCalledTimes(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script>
|
||||||
|
import { Modal, notifications } from "@budibase/bbui"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||||
|
|
||||||
|
let modal
|
||||||
|
let promptUpload = false
|
||||||
|
|
||||||
|
export function show({ promptUpload: newPromptUpload = false }) {
|
||||||
|
promptUpload = newPromptUpload
|
||||||
|
modal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInternalTableSave = table => {
|
||||||
|
notifications.success(`Table created successfully.`)
|
||||||
|
$goto(`./table/${table._id}`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<CreateTableModal {promptUpload} afterSave={handleInternalTableSave} />
|
||||||
|
</Modal>
|
|
@ -1,39 +1,31 @@
|
||||||
<script>
|
<script>
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { tables, datasources } from "stores/backend"
|
|
||||||
|
|
||||||
import { Icon, Modal, notifications, Heading, Body } from "@budibase/bbui"
|
|
||||||
import { params, goto } from "@roxi/routify"
|
|
||||||
import {
|
import {
|
||||||
IntegrationTypes,
|
tables,
|
||||||
DatasourceTypes,
|
datasources,
|
||||||
DEFAULT_BB_DATASOURCE_ID,
|
sortedIntegrations as integrations,
|
||||||
} from "constants/backend"
|
} from "stores/backend"
|
||||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
|
||||||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
import { hasData } from "stores/selectors"
|
||||||
import GoogleDatasourceConfigModal from "components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte"
|
import { Icon, notifications, Heading, Body } from "@budibase/bbui"
|
||||||
import { createRestDatasource } from "builderStore/datasource"
|
import { params, goto } from "@roxi/routify"
|
||||||
|
import CreateExternalDatasourceModal from "./_components/CreateExternalDatasourceModal/index.svelte"
|
||||||
|
import CreateInternalTableModal from "./_components/CreateInternalTableModal.svelte"
|
||||||
import DatasourceOption from "./_components/DatasourceOption.svelte"
|
import DatasourceOption from "./_components/DatasourceOption.svelte"
|
||||||
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
||||||
import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
|
import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
|
||||||
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
||||||
import { onMount } from "svelte"
|
|
||||||
|
|
||||||
let internalTableModal
|
let internalTableModal
|
||||||
let externalDatasourceModal
|
let externalDatasourceModal
|
||||||
let integrations = []
|
|
||||||
let integration = null
|
|
||||||
let disabled = false
|
|
||||||
let promptUpload = false
|
|
||||||
|
|
||||||
$: hasData = $datasources.list.length > 1 || $tables.list.length > 1
|
let sampleDataLoading = false
|
||||||
$: hasDefaultData =
|
let externalDatasourceLoading = false
|
||||||
$datasources.list.findIndex(
|
|
||||||
datasource => datasource._id === DEFAULT_BB_DATASOURCE_ID
|
$: disabled = sampleDataLoading || externalDatasourceLoading
|
||||||
) !== -1
|
|
||||||
|
|
||||||
const createSampleData = async () => {
|
const createSampleData = async () => {
|
||||||
disabled = true
|
sampleDataLoading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await API.addSampleData($params.application)
|
await API.addSampleData($params.application)
|
||||||
|
@ -41,136 +33,22 @@
|
||||||
await datasources.fetch()
|
await datasources.fetch()
|
||||||
$goto("./table")
|
$goto("./table")
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
disabled = false
|
sampleDataLoading = false
|
||||||
notifications.error("Error creating datasource")
|
notifications.error("Error creating datasource")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleIntegrationSelect = integrationType => {
|
|
||||||
const selected = integrations.find(([type]) => type === integrationType)[1]
|
|
||||||
|
|
||||||
// build the schema
|
|
||||||
const config = {}
|
|
||||||
|
|
||||||
for (let key of Object.keys(selected.datasource)) {
|
|
||||||
config[key] = selected.datasource[key].default
|
|
||||||
}
|
|
||||||
|
|
||||||
integration = {
|
|
||||||
type: integrationType,
|
|
||||||
plus: selected.plus,
|
|
||||||
config,
|
|
||||||
schema: selected.datasource,
|
|
||||||
auth: selected.auth,
|
|
||||||
features: selected.features || [],
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selected.friendlyName) {
|
|
||||||
integration.name = selected.friendlyName
|
|
||||||
}
|
|
||||||
|
|
||||||
if (integration.type === IntegrationTypes.REST) {
|
|
||||||
disabled = true
|
|
||||||
|
|
||||||
// Skip modal for rest, create straight away
|
|
||||||
createRestDatasource(integration)
|
|
||||||
.then(response => {
|
|
||||||
$goto(`./datasource/${response._id}`)
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
disabled = false
|
|
||||||
notifications.error("Error creating datasource")
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
externalDatasourceModal.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleInternalTable = () => {
|
|
||||||
promptUpload = false
|
|
||||||
internalTableModal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDataImport = () => {
|
|
||||||
promptUpload = true
|
|
||||||
internalTableModal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleInternalTableSave = table => {
|
|
||||||
notifications.success(`Table created successfully.`)
|
|
||||||
$goto(`./table/${table._id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortIntegrations(integrations) {
|
|
||||||
let integrationsArray = Object.entries(integrations)
|
|
||||||
|
|
||||||
function getTypeOrder(schema) {
|
|
||||||
if (schema.type === DatasourceTypes.API) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema.type === DatasourceTypes.RELATIONAL) {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
return schema.type?.charCodeAt(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
integrationsArray.sort((a, b) => {
|
|
||||||
let typeOrderA = getTypeOrder(a[1])
|
|
||||||
let typeOrderB = getTypeOrder(b[1])
|
|
||||||
|
|
||||||
if (typeOrderA === typeOrderB) {
|
|
||||||
return a[1].friendlyName?.localeCompare(b[1].friendlyName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return typeOrderA < typeOrderB ? -1 : 1
|
|
||||||
})
|
|
||||||
|
|
||||||
return integrationsArray
|
|
||||||
}
|
|
||||||
|
|
||||||
let continueGoogleSetup
|
|
||||||
onMount(() => {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
|
||||||
continueGoogleSetup = urlParams.get("continue_google_setup")
|
|
||||||
})
|
|
||||||
|
|
||||||
const fetchIntegrations = async () => {
|
|
||||||
const unsortedIntegrations = await API.getIntegrations()
|
|
||||||
integrations = sortIntegrations(unsortedIntegrations)
|
|
||||||
|
|
||||||
if (continueGoogleSetup) {
|
|
||||||
handleIntegrationSelect(IntegrationTypes.GOOGLE_SHEETS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: fetchIntegrations()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={internalTableModal}>
|
<CreateInternalTableModal bind:this={internalTableModal} />
|
||||||
<CreateTableModal {promptUpload} afterSave={handleInternalTableSave} />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
<CreateExternalDatasourceModal
|
||||||
|
bind:loading={externalDatasourceLoading}
|
||||||
bind:this={externalDatasourceModal}
|
bind:this={externalDatasourceModal}
|
||||||
on:hide={() => {
|
|
||||||
continueGoogleSetup = null
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#if integration?.auth?.type === "google"}
|
|
||||||
<GoogleDatasourceConfigModal
|
|
||||||
continueSetupId={continueGoogleSetup}
|
|
||||||
{integration}
|
|
||||||
/>
|
/>
|
||||||
{:else}
|
|
||||||
<DatasourceConfigModal {integration} />
|
|
||||||
{/if}
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="closeButton">
|
<div class="closeButton">
|
||||||
{#if hasData}
|
{#if hasData($datasources, $tables)}
|
||||||
<Icon hoverable name="Close" on:click={$goto("./table")} />
|
<Icon hoverable name="Close" on:click={$goto("./table")} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -191,7 +69,7 @@
|
||||||
|
|
||||||
<div class="options">
|
<div class="options">
|
||||||
<DatasourceOption
|
<DatasourceOption
|
||||||
on:click={handleInternalTable}
|
on:click={internalTableModal.show}
|
||||||
title="Create new table"
|
title="Create new table"
|
||||||
description="Non-relational"
|
description="Non-relational"
|
||||||
{disabled}
|
{disabled}
|
||||||
|
@ -202,12 +80,12 @@
|
||||||
on:click={createSampleData}
|
on:click={createSampleData}
|
||||||
title="Use sample data"
|
title="Use sample data"
|
||||||
description="Non-relational"
|
description="Non-relational"
|
||||||
disabled={disabled || hasDefaultData}
|
disabled={disabled || $datasources.hasDefaultData}
|
||||||
>
|
>
|
||||||
<svelte:component this={ICONS.BUDIBASE} height="20" width="20" />
|
<svelte:component this={ICONS.BUDIBASE} height="20" width="20" />
|
||||||
</DatasourceOption>
|
</DatasourceOption>
|
||||||
<DatasourceOption
|
<DatasourceOption
|
||||||
on:click={handleDataImport}
|
on:click={() => internalTableModal.show({ promptUpload: true })}
|
||||||
title="Upload data"
|
title="Upload data"
|
||||||
description="Non-relational"
|
description="Non-relational"
|
||||||
{disabled}
|
{disabled}
|
||||||
|
@ -221,14 +99,17 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="options">
|
<div class="options">
|
||||||
{#each integrations as [key, value]}
|
{#each $integrations as integration}
|
||||||
<DatasourceOption
|
<DatasourceOption
|
||||||
on:click={() => handleIntegrationSelect(key)}
|
on:click={() => externalDatasourceModal.show(integration)}
|
||||||
title={value.friendlyName}
|
title={integration.friendlyName}
|
||||||
description={value.type}
|
description={integration.type}
|
||||||
{disabled}
|
{disabled}
|
||||||
>
|
>
|
||||||
<IntegrationIcon integrationType={key} schema={value} />
|
<IntegrationIcon
|
||||||
|
integrationType={integration.name}
|
||||||
|
schema={integration}
|
||||||
|
/>
|
||||||
</DatasourceOption>
|
</DatasourceOption>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,21 @@
|
||||||
import { writable, derived, get } from "svelte/store"
|
import { writable, derived, get } from "svelte/store"
|
||||||
|
import { IntegrationTypes, DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
|
||||||
import { queries, tables } from "./"
|
import { queries, tables } from "./"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
import { DatasourceFeature } from "@budibase/types"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export class ImportTableError extends Error {
|
||||||
|
constructor(message) {
|
||||||
|
super(message)
|
||||||
|
const [title, description] = message.split(" - ")
|
||||||
|
|
||||||
|
this.name = "TableSelectionError"
|
||||||
|
// Capitalize the first character of both the title and description
|
||||||
|
this.title = title[0].toUpperCase() + title.substr(1)
|
||||||
|
this.description = description[0].toUpperCase() + description.substr(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createDatasourcesStore() {
|
export function createDatasourcesStore() {
|
||||||
const store = writable({
|
const store = writable({
|
||||||
|
@ -8,9 +23,13 @@ export function createDatasourcesStore() {
|
||||||
selectedDatasourceId: null,
|
selectedDatasourceId: null,
|
||||||
schemaError: null,
|
schemaError: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const derivedStore = derived(store, $store => ({
|
const derivedStore = derived(store, $store => ({
|
||||||
...$store,
|
...$store,
|
||||||
selected: $store.list?.find(ds => ds._id === $store.selectedDatasourceId),
|
selected: $store.list?.find(ds => ds._id === $store.selectedDatasourceId),
|
||||||
|
hasDefaultData: $store.list.some(
|
||||||
|
datasource => datasource._id === DEFAULT_BB_DATASOURCE_ID
|
||||||
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const fetch = async () => {
|
const fetch = async () => {
|
||||||
|
@ -50,27 +69,62 @@ export function createDatasourcesStore() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateSchema = async (datasource, tablesFilter) => {
|
const updateSchema = async (datasource, tablesFilter) => {
|
||||||
|
try {
|
||||||
const response = await API.buildDatasourceSchema({
|
const response = await API.buildDatasourceSchema({
|
||||||
datasourceId: datasource?._id,
|
datasourceId: datasource?._id,
|
||||||
tablesFilter,
|
tablesFilter,
|
||||||
})
|
})
|
||||||
return updateDatasource(response)
|
updateDatasource(response)
|
||||||
|
} catch (e) {
|
||||||
|
// buildDatasourceSchema call returns user presentable errors with two parts divided with a " - ".
|
||||||
|
if (e.message.split(" - ").length === 2) {
|
||||||
|
throw new ImportTableError(e.message)
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = async (body, { fetchSchema, tablesFilter } = {}) => {
|
const sourceCount = source => {
|
||||||
if (fetchSchema == null) {
|
return get(store).list.filter(datasource => datasource.source === source)
|
||||||
fetchSchema = false
|
.length
|
||||||
}
|
}
|
||||||
let response
|
|
||||||
if (body._id) {
|
const create = async ({ integration, fields }) => {
|
||||||
response = await API.updateDatasource(body)
|
try {
|
||||||
} else {
|
const datasource = {
|
||||||
response = await API.createDatasource({
|
type: "datasource",
|
||||||
datasource: body,
|
source: integration.name,
|
||||||
fetchSchema,
|
config: fields,
|
||||||
tablesFilter,
|
name: `${integration.friendlyName}-${
|
||||||
|
sourceCount(integration.name) + 1
|
||||||
|
}`,
|
||||||
|
plus: integration.plus && integration.name !== IntegrationTypes.REST,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||||
|
const { connected } = await API.validateDatasource(datasource)
|
||||||
|
if (!connected) throw new Error("Unable to connect")
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await API.createDatasource({
|
||||||
|
datasource,
|
||||||
|
fetchSchema:
|
||||||
|
integration.plus &&
|
||||||
|
integration.name !== IntegrationTypes.GOOGLE_SHEETS,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
notifications.success("Datasource created successfully.")
|
||||||
|
|
||||||
|
return updateDatasource(response)
|
||||||
|
} catch (e) {
|
||||||
|
notifications.error(`Error creating datasource: ${e.message}`)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = async body => {
|
||||||
|
const response = await API.updateDatasource(body)
|
||||||
return updateDatasource(response)
|
return updateDatasource(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,16 +186,23 @@ export function createDatasourcesStore() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTableNames = async datasource => {
|
||||||
|
const info = await API.fetchInfoForDatasource(datasource)
|
||||||
|
return info.tableNames || []
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe: derivedStore.subscribe,
|
subscribe: derivedStore.subscribe,
|
||||||
fetch,
|
fetch,
|
||||||
init: fetch,
|
init: fetch,
|
||||||
select,
|
select,
|
||||||
updateSchema,
|
updateSchema,
|
||||||
|
create,
|
||||||
save,
|
save,
|
||||||
delete: deleteDatasource,
|
delete: deleteDatasource,
|
||||||
removeSchemaError,
|
removeSchemaError,
|
||||||
replaceDatasource,
|
replaceDatasource,
|
||||||
|
getTableNames,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,8 @@ export { tables } from "./tables"
|
||||||
export { views } from "./views"
|
export { views } from "./views"
|
||||||
export { permissions } from "./permissions"
|
export { permissions } from "./permissions"
|
||||||
export { roles } from "./roles"
|
export { roles } from "./roles"
|
||||||
export { datasources } from "./datasources"
|
export { datasources, ImportTableError } from "./datasources"
|
||||||
export { integrations } from "./integrations"
|
export { integrations } from "./integrations"
|
||||||
|
export { sortedIntegrations } from "./sortedIntegrations"
|
||||||
export { queries } from "./queries"
|
export { queries } from "./queries"
|
||||||
export { flags } from "./flags"
|
export { flags } from "./flags"
|
||||||
|
|
|
@ -2,14 +2,16 @@ import { writable } from "svelte/store"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
const createIntegrationsStore = () => {
|
const createIntegrationsStore = () => {
|
||||||
const store = writable(null)
|
const store = writable({})
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
const integrations = await API.getIntegrations()
|
||||||
|
store.set(integrations)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...store,
|
...store,
|
||||||
init: async () => {
|
init,
|
||||||
const integrations = await API.getIntegrations()
|
|
||||||
store.set(integrations)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { integrations } from "./integrations"
|
||||||
|
import { derived } from "svelte/store"
|
||||||
|
|
||||||
|
import { DatasourceTypes } from "constants/backend"
|
||||||
|
|
||||||
|
const getIntegrationOrder = type => {
|
||||||
|
if (type === DatasourceTypes.API) return 1
|
||||||
|
if (type === DatasourceTypes.RELATIONAL) return 2
|
||||||
|
if (type === DatasourceTypes.NON_RELATIONAL) return 3
|
||||||
|
|
||||||
|
// Sort all others arbitrarily by the first character of their name.
|
||||||
|
// Character codes can technically be as low as 0, so make sure the number is at least 4
|
||||||
|
return type.charCodeAt(0) + 4
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSortedIntegrationsStore = () => {
|
||||||
|
return derived(integrations, $integrations => {
|
||||||
|
const integrationsAsArray = Object.entries($integrations).map(
|
||||||
|
([name, integration]) => ({
|
||||||
|
name,
|
||||||
|
...integration,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return integrationsAsArray.sort((integrationA, integrationB) => {
|
||||||
|
const integrationASortOrder = getIntegrationOrder(integrationA.type)
|
||||||
|
const integrationBSortOrder = getIntegrationOrder(integrationB.type)
|
||||||
|
if (integrationASortOrder === integrationBSortOrder) {
|
||||||
|
return integrationA.friendlyName.localeCompare(
|
||||||
|
integrationB.friendlyName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return integrationASortOrder < integrationBSortOrder ? -1 : 1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sortedIntegrations = createSortedIntegrationsStore()
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { it, expect, describe, beforeEach, vi } from "vitest"
|
||||||
|
import { createSortedIntegrationsStore } from "./sortedIntegrations"
|
||||||
|
import { DatasourceTypes } from "constants/backend"
|
||||||
|
|
||||||
|
import { derived } from "svelte/store"
|
||||||
|
import { integrations } from "stores/backend/integrations"
|
||||||
|
|
||||||
|
vi.mock("svelte/store", () => ({
|
||||||
|
derived: vi.fn(() => {}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("stores/backend/integrations", () => ({ integrations: vi.fn() }))
|
||||||
|
|
||||||
|
const inputA = {
|
||||||
|
nonRelationalA: {
|
||||||
|
friendlyName: "non-relational A",
|
||||||
|
type: DatasourceTypes.NON_RELATIONAL,
|
||||||
|
},
|
||||||
|
relationalB: {
|
||||||
|
friendlyName: "relational B",
|
||||||
|
type: DatasourceTypes.RELATIONAL,
|
||||||
|
},
|
||||||
|
relationalA: {
|
||||||
|
friendlyName: "relational A",
|
||||||
|
type: DatasourceTypes.RELATIONAL,
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
friendlyName: "api",
|
||||||
|
type: DatasourceTypes.API,
|
||||||
|
},
|
||||||
|
relationalC: {
|
||||||
|
friendlyName: "relational C",
|
||||||
|
type: DatasourceTypes.RELATIONAL,
|
||||||
|
},
|
||||||
|
nonRelationalB: {
|
||||||
|
friendlyName: "non-relational B",
|
||||||
|
type: DatasourceTypes.NON_RELATIONAL,
|
||||||
|
},
|
||||||
|
otherC: {
|
||||||
|
friendlyName: "other C",
|
||||||
|
type: "random",
|
||||||
|
},
|
||||||
|
otherB: {
|
||||||
|
friendlyName: "other B",
|
||||||
|
type: "arbitrary",
|
||||||
|
},
|
||||||
|
otherA: {
|
||||||
|
friendlyName: "other A",
|
||||||
|
type: "arbitrary",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputB = Object.fromEntries(Object.entries(inputA).reverse())
|
||||||
|
|
||||||
|
const expectedOutput = [
|
||||||
|
{
|
||||||
|
name: "api",
|
||||||
|
friendlyName: "api",
|
||||||
|
type: DatasourceTypes.API,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "relationalA",
|
||||||
|
friendlyName: "relational A",
|
||||||
|
type: DatasourceTypes.RELATIONAL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "relationalB",
|
||||||
|
friendlyName: "relational B",
|
||||||
|
type: DatasourceTypes.RELATIONAL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "relationalC",
|
||||||
|
friendlyName: "relational C",
|
||||||
|
type: DatasourceTypes.RELATIONAL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nonRelationalA",
|
||||||
|
friendlyName: "non-relational A",
|
||||||
|
type: DatasourceTypes.NON_RELATIONAL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nonRelationalB",
|
||||||
|
friendlyName: "non-relational B",
|
||||||
|
type: DatasourceTypes.NON_RELATIONAL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "otherA",
|
||||||
|
friendlyName: "other A",
|
||||||
|
type: "arbitrary",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "otherB",
|
||||||
|
friendlyName: "other B",
|
||||||
|
type: "arbitrary",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "otherC",
|
||||||
|
friendlyName: "other C",
|
||||||
|
type: "random",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
describe("sorted integrations store", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
|
||||||
|
ctx.returnedStore = createSortedIntegrationsStore()
|
||||||
|
|
||||||
|
ctx.derivedCallback = derived.mock.calls[0][1]
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls derived with the correct parameters", () => {
|
||||||
|
expect(derived).toHaveBeenCalledTimes(1)
|
||||||
|
expect(derived).toHaveBeenCalledWith(integrations, expect.toBeFunc())
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("derived callback", () => {
|
||||||
|
it("When no integrations are loaded", ctx => {
|
||||||
|
expect(ctx.derivedCallback({})).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("When integrations are present", ctx => {
|
||||||
|
expect(ctx.derivedCallback(inputA)).toEqual(expectedOutput)
|
||||||
|
expect(ctx.derivedCallback(inputB)).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
|
||||||
|
import { DatasourceFeature } from "@budibase/types"
|
||||||
|
|
||||||
|
export const integrationForDatasource = (integrations, datasource) => ({
|
||||||
|
name: datasource.source,
|
||||||
|
...integrations[datasource.source],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const hasData = (datasources, tables) =>
|
||||||
|
datasources.list.length > 1 || tables.list.length > 1
|
||||||
|
|
||||||
|
export const hasDefaultData = datasources =>
|
||||||
|
datasources.list.some(
|
||||||
|
datasource => datasource._id === DEFAULT_BB_DATASOURCE_ID
|
||||||
|
)
|
||||||
|
|
||||||
|
export const configFromIntegration = integration => {
|
||||||
|
const config = {}
|
||||||
|
|
||||||
|
Object.entries(integration?.datasource || {}).forEach(([key, properties]) => {
|
||||||
|
if (properties.type === "fieldGroup") {
|
||||||
|
Object.keys(properties.fields).forEach(fieldKey => {
|
||||||
|
config[fieldKey] = null
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
config[key] = properties.default ?? null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shouldIntegrationFetchTableNames = integration => {
|
||||||
|
return integration.features?.[DatasourceFeature.FETCH_TABLE_NAMES]
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { it, expect, describe, beforeEach, vi } from "vitest"
|
||||||
|
import { DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
|
||||||
|
import { integrationForDatasource, hasData, hasDefaultData } from "./selectors"
|
||||||
|
|
||||||
|
describe("selectors", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("integrationForDatasource", () => {
|
||||||
|
it("returns the integration corresponding to the given datasource", () => {
|
||||||
|
expect(
|
||||||
|
integrationForDatasource(
|
||||||
|
{ integrationOne: { some: "data" } },
|
||||||
|
{ source: "integrationOne" }
|
||||||
|
)
|
||||||
|
).toEqual({ some: "data", name: "integrationOne" })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("hasData", () => {
|
||||||
|
describe("when the user has created a datasource in addition to the premade Budibase DB source", () => {
|
||||||
|
it("returns true", () => {
|
||||||
|
expect(hasData({ list: [1, 1] }, { list: [] })).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("when the user has created a table in addition to the premade users table", () => {
|
||||||
|
it("returns true", () => {
|
||||||
|
expect(hasData({ list: [] }, { list: [1, 1] })).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("when the user doesn't have data", () => {
|
||||||
|
it("returns false", () => {
|
||||||
|
expect(hasData({ list: [] }, { list: [] })).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("hasDefaultData", () => {
|
||||||
|
describe("when the user has default data", () => {
|
||||||
|
it("returns true", () => {
|
||||||
|
expect(
|
||||||
|
hasDefaultData({ list: [{ _id: DEFAULT_BB_DATASOURCE_ID }] })
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("when the user doesn't have default data", () => {
|
||||||
|
it("returns false", () => {
|
||||||
|
expect(hasDefaultData({ list: [{ _id: "some other id" }] })).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue