Merge branch 'develop' of github.com:Budibase/budibase into grid-block
This commit is contained in:
commit
0dbd709438
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.7.7-alpha.2",
|
||||
"version": "2.7.7-alpha.5",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/backend-core",
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
export let disabled = false
|
||||
export let error = null
|
||||
export let validate = null
|
||||
export let indeterminate = false
|
||||
export let compact = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -21,11 +23,19 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<FancyField {error} {value} {validate} {disabled} clickable on:click={onChange}>
|
||||
<FancyField
|
||||
{error}
|
||||
{value}
|
||||
{validate}
|
||||
{disabled}
|
||||
{compact}
|
||||
clickable
|
||||
on:click={onChange}
|
||||
>
|
||||
<span>
|
||||
<Checkbox {disabled} {value} />
|
||||
<Checkbox {disabled} {value} {indeterminate} />
|
||||
</span>
|
||||
<div class="text">
|
||||
<div class="text" class:compact>
|
||||
{#if text}
|
||||
{text}
|
||||
{/if}
|
||||
|
@ -47,6 +57,10 @@
|
|||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.text.compact {
|
||||
font-size: 13px;
|
||||
line-height: 15px;
|
||||
}
|
||||
.text > :global(*) {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
<script>
|
||||
import FancyCheckbox from "./FancyCheckbox.svelte"
|
||||
import FancyForm from "./FancyForm.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let options = []
|
||||
export let selected = []
|
||||
export let showSelectAll = true
|
||||
export let selectAllText = "Select all"
|
||||
|
||||
let selectedBooleans = reset()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: updateSelected(selectedBooleans)
|
||||
$: dispatch("change", selected)
|
||||
$: allSelected = selected?.length === options.length
|
||||
$: noneSelected = !selected?.length
|
||||
|
||||
function reset() {
|
||||
return Array(options.length).fill(true)
|
||||
}
|
||||
|
||||
function updateSelected(selectedArr) {
|
||||
const array = []
|
||||
for (let [i, isSelected] of Object.entries(selectedArr)) {
|
||||
if (isSelected) {
|
||||
array.push(options[i])
|
||||
}
|
||||
}
|
||||
selected = array
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
if (allSelected === true) {
|
||||
selectedBooleans = []
|
||||
} else {
|
||||
selectedBooleans = reset()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if options && Array.isArray(options)}
|
||||
<div class="checkbox-group" class:has-select-all={showSelectAll}>
|
||||
<FancyForm on:change>
|
||||
{#if showSelectAll}
|
||||
<FancyCheckbox
|
||||
bind:value={allSelected}
|
||||
on:change={toggleSelectAll}
|
||||
text={selectAllText}
|
||||
indeterminate={!allSelected && !noneSelected}
|
||||
compact
|
||||
/>
|
||||
{/if}
|
||||
{#each options as option, i}
|
||||
<FancyCheckbox bind:value={selectedBooleans[i]} text={option} compact />
|
||||
{/each}
|
||||
</FancyForm>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.checkbox-group.has-select-all :global(.fancy-field:first-of-type) {
|
||||
background: var(--spectrum-global-color-gray-100);
|
||||
}
|
||||
.checkbox-group.has-select-all :global(.fancy-field:first-of-type:hover) {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
</style>
|
|
@ -11,6 +11,7 @@
|
|||
export let value
|
||||
export let ref
|
||||
export let autoHeight
|
||||
export let compact = false
|
||||
|
||||
const formContext = getContext("fancy-form")
|
||||
const id = Math.random()
|
||||
|
@ -42,6 +43,7 @@
|
|||
class:disabled
|
||||
class:focused
|
||||
class:clickable
|
||||
class:compact
|
||||
class:auto-height={autoHeight}
|
||||
>
|
||||
<div class="content" on:click>
|
||||
|
@ -61,7 +63,6 @@
|
|||
|
||||
<style>
|
||||
.fancy-field {
|
||||
max-width: 400px;
|
||||
background: var(--spectrum-global-color-gray-75);
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-radius: 4px;
|
||||
|
@ -69,6 +70,12 @@
|
|||
transition: border-color 130ms ease-out, background 130ms ease-out,
|
||||
background 130ms ease-out;
|
||||
color: var(--spectrum-global-color-gray-800);
|
||||
--padding: 16px;
|
||||
--height: 64px;
|
||||
}
|
||||
.fancy-field.compact {
|
||||
--padding: 8px;
|
||||
--height: 36px;
|
||||
}
|
||||
.fancy-field:hover {
|
||||
border-color: var(--spectrum-global-color-gray-400);
|
||||
|
@ -91,8 +98,8 @@
|
|||
}
|
||||
.content {
|
||||
position: relative;
|
||||
height: 64px;
|
||||
padding: 0 16px;
|
||||
height: var(--height);
|
||||
padding: 0 var(--padding);
|
||||
}
|
||||
.fancy-field.auto-height .content {
|
||||
height: auto;
|
||||
|
@ -103,7 +110,7 @@
|
|||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
gap: var(--padding);
|
||||
}
|
||||
.field {
|
||||
flex: 1 1 auto;
|
||||
|
|
|
@ -4,4 +4,5 @@ export { default as FancySelect } from "./FancySelect.svelte"
|
|||
export { default as FancyButton } from "./FancyButton.svelte"
|
||||
export { default as FancyForm } from "./FancyForm.svelte"
|
||||
export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte"
|
||||
export { default as FancyCheckboxGroup } from "./FancyCheckboxGroup.svelte"
|
||||
export { default as ErrorMessage } from "./ErrorMessage.svelte"
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
export let text = null
|
||||
export let disabled = false
|
||||
export let size
|
||||
export let indeterminate = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = event => {
|
||||
|
@ -22,6 +23,7 @@
|
|||
class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}"
|
||||
class:is-invalid={!!error}
|
||||
class:checked={value}
|
||||
class:is-indeterminate={indeterminate}
|
||||
>
|
||||
<input
|
||||
checked={value}
|
||||
|
|
|
@ -23,10 +23,11 @@ function prepareData(config) {
|
|||
return datasource
|
||||
}
|
||||
|
||||
export async function saveDatasource(config, skipFetch = false) {
|
||||
export async function saveDatasource(config, { skipFetch, tablesFilter } = {}) {
|
||||
const datasource = prepareData(config)
|
||||
// Create datasource
|
||||
const resp = await datasources.save(datasource, !skipFetch && datasource.plus)
|
||||
const fetchSchema = !skipFetch && datasource.plus
|
||||
const resp = await datasources.save(datasource, { fetchSchema, tablesFilter })
|
||||
|
||||
// update the tables incase datasource plus
|
||||
await tables.fetch()
|
||||
|
@ -41,6 +42,13 @@ export async function createRestDatasource(integration) {
|
|||
|
||||
export async function validateDatasourceConfig(config) {
|
||||
const datasource = prepareData(config)
|
||||
const resp = await API.validateDatasource(datasource)
|
||||
return resp
|
||||
return await API.validateDatasource(datasource)
|
||||
}
|
||||
|
||||
export async function getDatasourceInfo(config) {
|
||||
let datasource = config
|
||||
if (!config._id) {
|
||||
datasource = prepareData(config)
|
||||
}
|
||||
return await API.fetchInfoForDatasource(datasource)
|
||||
}
|
||||
|
|
|
@ -74,6 +74,7 @@ const INITIAL_FRONTEND_STATE = {
|
|||
propertyFocus: null,
|
||||
builderSidePanel: false,
|
||||
hasLock: true,
|
||||
showPreview: false,
|
||||
|
||||
// URL params
|
||||
selectedScreenId: null,
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
notifications,
|
||||
Modal,
|
||||
Table,
|
||||
Toggle,
|
||||
FancyCheckboxGroup,
|
||||
} from "@budibase/bbui"
|
||||
import { datasources, integrations, tables } from "stores/backend"
|
||||
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
|
||||
|
@ -16,7 +16,7 @@
|
|||
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import ValuesList from "components/common/ValuesList.svelte"
|
||||
import { getDatasourceInfo } from "builderStore/datasource"
|
||||
|
||||
export let datasource
|
||||
export let save
|
||||
|
@ -34,7 +34,7 @@
|
|||
let selectedFromRelationship, selectedToRelationship
|
||||
let confirmDialog
|
||||
let specificTables = null
|
||||
let requireSpecificTables = false
|
||||
let tableList
|
||||
|
||||
$: integration = datasource && $integrations[datasource.source]
|
||||
$: plusTables = datasource?.plus
|
||||
|
@ -153,30 +153,28 @@
|
|||
warning={false}
|
||||
title="Confirm table fetch"
|
||||
>
|
||||
<Toggle
|
||||
bind:value={requireSpecificTables}
|
||||
on:change={e => {
|
||||
requireSpecificTables = e.detail
|
||||
specificTables = null
|
||||
}}
|
||||
thin
|
||||
text="Fetch listed tables only (one per line)"
|
||||
/>
|
||||
{#if requireSpecificTables}
|
||||
<ValuesList label="" bind:values={specificTables} />
|
||||
{/if}
|
||||
<br />
|
||||
<Body>
|
||||
If you have fetched tables from this database before, this action may
|
||||
overwrite any changes you made after your initial fetch.
|
||||
</Body>
|
||||
<br />
|
||||
<div class="table-checkboxes">
|
||||
<FancyCheckboxGroup options={tableList} bind:selected={specificTables} />
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
|
||||
<Divider />
|
||||
<div class="query-header">
|
||||
<Heading size="S">Tables</Heading>
|
||||
<div class="table-buttons">
|
||||
<Button secondary on:click={() => confirmDialog.show()}>
|
||||
<Button
|
||||
secondary
|
||||
on:click={async () => {
|
||||
const info = await getDatasourceInfo(datasource)
|
||||
tableList = info.tableNames
|
||||
confirmDialog.show()
|
||||
}}
|
||||
>
|
||||
Fetch tables
|
||||
</Button>
|
||||
<Button cta icon="Add" on:click={createNewTable}>New table</Button>
|
||||
|
@ -246,4 +244,8 @@
|
|||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.table-checkboxes {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -44,6 +44,9 @@ export default ICONS
|
|||
|
||||
export function getIcon(integrationType, schema) {
|
||||
const integrationList = get(integrations)
|
||||
if (!integrationList) {
|
||||
return
|
||||
}
|
||||
if (integrationList[integrationType]?.iconUrl) {
|
||||
return { url: integrationList[integrationType].iconUrl }
|
||||
} else if (schema?.custom || !ICONS[integrationType]) {
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { ModalContent, notifications, Body, Layout } from "@budibase/bbui"
|
||||
import {
|
||||
ModalContent,
|
||||
notifications,
|
||||
Body,
|
||||
Layout,
|
||||
FancyCheckboxGroup,
|
||||
} from "@budibase/bbui"
|
||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||
import { IntegrationNames } from "constants/backend"
|
||||
import cloneDeep from "lodash/cloneDeepWith"
|
||||
import {
|
||||
saveDatasource as save,
|
||||
validateDatasourceConfig,
|
||||
getDatasourceInfo,
|
||||
} from "builderStore/datasource"
|
||||
import { DatasourceFeature } from "@budibase/types"
|
||||
|
||||
|
@ -15,11 +22,24 @@
|
|||
// kill the reference so the input isn't saved
|
||||
let datasource = cloneDeep(integration)
|
||||
let isValid = false
|
||||
let fetchTableStep = false
|
||||
let selectedTables = []
|
||||
let tableList = []
|
||||
|
||||
$: name =
|
||||
IntegrationNames[datasource.type] || datasource.name || datasource.type
|
||||
IntegrationNames[datasource?.type] || datasource?.name || datasource?.type
|
||||
$: datasourcePlus = datasource?.plus
|
||||
$: title = fetchTableStep ? "Fetch your tables" : `Connect to ${name}`
|
||||
$: confirmText = fetchTableStep
|
||||
? "Continue"
|
||||
: datasourcePlus
|
||||
? "Connect"
|
||||
: "Save and continue to query"
|
||||
|
||||
async function validateConfig() {
|
||||
if (!integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||
return true
|
||||
}
|
||||
const displayError = message =>
|
||||
notifications.error(message ?? "Error validating datasource")
|
||||
|
||||
|
@ -37,45 +57,79 @@
|
|||
}
|
||||
|
||||
async function saveDatasource() {
|
||||
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||
const valid = await validateConfig()
|
||||
if (!valid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (!datasource.name) {
|
||||
datasource.name = name
|
||||
}
|
||||
const resp = await save(datasource)
|
||||
const opts = {}
|
||||
if (datasourcePlus && selectedTables) {
|
||||
opts.tablesFilter = selectedTables
|
||||
}
|
||||
const resp = await save(datasource, opts)
|
||||
$goto(`./datasource/${resp._id}`)
|
||||
notifications.success(`Datasource created successfully.`)
|
||||
notifications.success("Datasource created successfully.")
|
||||
} catch (err) {
|
||||
notifications.error(err?.message ?? "Error saving datasource")
|
||||
// prevent the modal from closing
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function nextStep() {
|
||||
let connected = true
|
||||
if (datasourcePlus) {
|
||||
connected = await validateConfig()
|
||||
}
|
||||
if (!connected) {
|
||||
return false
|
||||
}
|
||||
if (datasourcePlus && !fetchTableStep) {
|
||||
notifications.success("Connected to datasource successfully.")
|
||||
const info = await getDatasourceInfo(datasource)
|
||||
tableList = info.tableNames
|
||||
fetchTableStep = true
|
||||
return false
|
||||
} else {
|
||||
await saveDatasource()
|
||||
return true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={`Connect to ${name}`}
|
||||
onConfirm={() => saveDatasource()}
|
||||
confirmText={datasource.plus ? "Connect" : "Save and continue to query"}
|
||||
cancelText="Back"
|
||||
showSecondaryButton={datasource.plus}
|
||||
{title}
|
||||
onConfirm={() => nextStep()}
|
||||
{confirmText}
|
||||
cancelText={fetchTableStep ? "Cancel" : "Back"}
|
||||
showSecondaryButton={datasourcePlus}
|
||||
size="L"
|
||||
disabled={!isValid}
|
||||
>
|
||||
<Layout noPadding>
|
||||
<Body size="XS"
|
||||
>Connect your database to Budibase using the config below.
|
||||
<Body size="XS">
|
||||
{#if !fetchTableStep}
|
||||
Connect your database to Budibase using the config below
|
||||
{:else}
|
||||
Choose what tables you want to sync with Budibase
|
||||
{/if}
|
||||
</Body>
|
||||
</Layout>
|
||||
<IntegrationConfigForm
|
||||
schema={datasource.schema}
|
||||
bind:datasource
|
||||
creating={true}
|
||||
on:valid={e => (isValid = e.detail)}
|
||||
/>
|
||||
{#if !fetchTableStep}
|
||||
<IntegrationConfigForm
|
||||
schema={datasource?.schema}
|
||||
bind:datasource
|
||||
creating={true}
|
||||
on:valid={e => (isValid = e.detail)}
|
||||
/>
|
||||
{:else}
|
||||
<div class="table-checkboxes">
|
||||
<FancyCheckboxGroup options={tableList} bind:selected={selectedTables} />
|
||||
</div>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
.table-checkboxes {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
<script>
|
||||
import {
|
||||
ModalContent,
|
||||
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 { onMount } from "svelte"
|
||||
import { validateDatasourceConfig } from "builderStore/datasource"
|
||||
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 { saveDatasource } from "builderStore/datasource"
|
||||
import { DatasourceFeature } from "@budibase/types"
|
||||
import { API } from "api"
|
||||
|
||||
export let integration
|
||||
export let continueSetupId = false
|
||||
|
@ -24,16 +29,20 @@
|
|||
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",
|
||||
AUTH: "auth",
|
||||
SET_URL: "set_url",
|
||||
SET_SHEETS: "set_sheets",
|
||||
}
|
||||
|
||||
let step = continueSetupId
|
||||
|
@ -42,12 +51,21 @@
|
|||
|
||||
let isValid = false
|
||||
|
||||
const modalConfig = {
|
||||
[GoogleDatasouceConfigStep.AUTH]: {},
|
||||
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 () => {
|
||||
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||
const checkConnection =
|
||||
integration.features[DatasourceFeature.CONNECTION_CHECKING]
|
||||
if (checkConnection) {
|
||||
const resp = await validateDatasourceConfig(datasource)
|
||||
if (!resp.connected) {
|
||||
notifications.error(`Unable to connect - ${resp.error}`)
|
||||
|
@ -56,21 +74,81 @@
|
|||
}
|
||||
|
||||
try {
|
||||
const resp = await saveDatasource(datasource)
|
||||
$goto(`./datasource/${resp._id}`)
|
||||
notifications.success(`Datasource created successfully.`)
|
||||
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={`Connect to ${integrationName}`}
|
||||
title={modalConfig[step].title}
|
||||
cancelText="Cancel"
|
||||
size="L"
|
||||
confirmText={modalConfig[step].confirmButtonText}
|
||||
|
@ -100,11 +178,30 @@
|
|||
<Body size="S">Add the URL of the sheet you want to connect.</Body>
|
||||
|
||||
<IntegrationConfigForm
|
||||
schema={datasource.schema}
|
||||
{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>
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
name: "App",
|
||||
description: "",
|
||||
icon: "Play",
|
||||
action: () => window.open(`/${$store.appId}`),
|
||||
action: () => store.update(state => ({ ...state, showPreview: true })),
|
||||
},
|
||||
{
|
||||
type: "Preview",
|
||||
|
|
|
@ -62,7 +62,10 @@
|
|||
}
|
||||
|
||||
const previewApp = () => {
|
||||
window.open(`/${application}`)
|
||||
store.update(state => ({
|
||||
...state,
|
||||
showPreview: true,
|
||||
}))
|
||||
}
|
||||
|
||||
const viewApp = () => {
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { fade, fly } from "svelte/transition"
|
||||
import { store, selectedScreen } from "builderStore"
|
||||
import { ProgressCircle } from "@budibase/bbui"
|
||||
|
||||
$: route = $selectedScreen?.routing.route || "/"
|
||||
$: src = `/${$store.appId}#${route}`
|
||||
|
||||
const close = () => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
showPreview: false,
|
||||
}))
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.closePreview = () => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
showPreview: false,
|
||||
}))
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="preview-overlay"
|
||||
transition:fade={{ duration: 260 }}
|
||||
on:click|self={close}
|
||||
>
|
||||
<div
|
||||
class="container spectrum {$store.theme}"
|
||||
transition:fly={{ duration: 260, y: 130 }}
|
||||
>
|
||||
<div class="header placeholder" />
|
||||
<div class="loading placeholder">
|
||||
<ProgressCircle />
|
||||
</div>
|
||||
<iframe title="Budibase App Preview" {src} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.preview-overlay {
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 999;
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
padding: 48px;
|
||||
}
|
||||
.container {
|
||||
flex: 1 1 auto;
|
||||
background: var(--spectrum-global-color-gray-75);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-shadow: 0 0 80px 0 rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
iframe {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
z-index: 1;
|
||||
}
|
||||
.header {
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
background: black;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
}
|
||||
.loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateY(-50%) translateX(-50%);
|
||||
}
|
||||
.placeholder {
|
||||
z-index: 0;
|
||||
}
|
||||
</style>
|
|
@ -24,6 +24,7 @@
|
|||
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
|
||||
import UserAvatars from "./_components/UserAvatars.svelte"
|
||||
import { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js"
|
||||
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
||||
|
||||
export let application
|
||||
|
||||
|
@ -140,7 +141,7 @@
|
|||
<BuilderSidePanel />
|
||||
{/if}
|
||||
|
||||
<div class="root">
|
||||
<div class="root" class:blur={$store.showPreview}>
|
||||
<div class="top-nav">
|
||||
{#if $store.initialised}
|
||||
<div class="topleftnav">
|
||||
|
@ -230,6 +231,10 @@
|
|||
{/await}
|
||||
</div>
|
||||
|
||||
{#if $store.showPreview}
|
||||
<PreviewOverlay />
|
||||
{/if}
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
<Modal bind:this={commandPaletteModal}>
|
||||
<CommandPalette />
|
||||
|
@ -248,6 +253,10 @@
|
|||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: filter 260ms ease-out;
|
||||
}
|
||||
.root.blur {
|
||||
filter: blur(8px);
|
||||
}
|
||||
|
||||
.top-nav {
|
||||
|
|
|
@ -57,7 +57,10 @@ export function createDatasourcesStore() {
|
|||
return updateDatasource(response)
|
||||
}
|
||||
|
||||
const save = async (body, fetchSchema = false) => {
|
||||
const save = async (body, { fetchSchema, tablesFilter } = {}) => {
|
||||
if (fetchSchema == null) {
|
||||
fetchSchema = false
|
||||
}
|
||||
let response
|
||||
if (body._id) {
|
||||
response = await API.updateDatasource(body)
|
||||
|
@ -65,6 +68,7 @@ export function createDatasourcesStore() {
|
|||
response = await API.createDatasource({
|
||||
datasource: body,
|
||||
fetchSchema,
|
||||
tablesFilter,
|
||||
})
|
||||
}
|
||||
return updateDatasource(response)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { createAPIClient } from "@budibase/frontend-core"
|
||||
import { notificationStore } from "../stores/notification.js"
|
||||
import { authStore } from "../stores/auth.js"
|
||||
import { devToolsStore } from "../stores/devTools.js"
|
||||
import { notificationStore, devToolsEnabled, devToolsStore } from "../stores/"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export const API = createAPIClient({
|
||||
|
@ -25,9 +24,10 @@ export const API = createAPIClient({
|
|||
}
|
||||
|
||||
// Add role header
|
||||
const devToolsState = get(devToolsStore)
|
||||
if (devToolsState.enabled && devToolsState.role) {
|
||||
headers["x-budibase-role"] = devToolsState.role
|
||||
const $devToolsStore = get(devToolsStore)
|
||||
const $devToolsEnabled = get(devToolsEnabled)
|
||||
if ($devToolsEnabled && $devToolsStore.role) {
|
||||
headers["x-budibase-role"] = $devToolsStore.role
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
appStore,
|
||||
devToolsStore,
|
||||
environmentStore,
|
||||
devToolsEnabled,
|
||||
} from "stores"
|
||||
import NotificationDisplay from "components/overlay/NotificationDisplay.svelte"
|
||||
import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte"
|
||||
|
@ -47,10 +48,7 @@
|
|||
let permissionError = false
|
||||
|
||||
// Determine if we should show devtools or not
|
||||
$: showDevTools =
|
||||
!$builderStore.inBuilder &&
|
||||
$devToolsStore.enabled &&
|
||||
!$routeStore.queryParams?.peek
|
||||
$: showDevTools = $devToolsEnabled && !$routeStore.queryParams?.peek
|
||||
|
||||
// Handle no matching route
|
||||
$: {
|
||||
|
@ -107,6 +105,7 @@
|
|||
lang="en"
|
||||
dir="ltr"
|
||||
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
|
||||
class:builder={$builderStore.inBuilder}
|
||||
>
|
||||
<DeviceBindingsProvider>
|
||||
<UserBindingsProvider>
|
||||
|
@ -223,12 +222,14 @@
|
|||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
#spectrum-root.builder {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#clip-root {
|
||||
max-width: 100%;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Heading, Button, Select } from "@budibase/bbui"
|
||||
import { Heading, Select, ActionButton } from "@budibase/bbui"
|
||||
import { devToolsStore } from "../../stores"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
|
@ -30,7 +30,7 @@
|
|||
</script>
|
||||
|
||||
<div class="dev-preview-header" class:mobile={$context.device.mobile}>
|
||||
<Heading size="XS">Budibase App Preview</Heading>
|
||||
<Heading size="XS">Preview</Heading>
|
||||
<Select
|
||||
quiet
|
||||
options={previewOptions}
|
||||
|
@ -40,36 +40,57 @@
|
|||
on:change={e => devToolsStore.actions.changeRole(e.detail)}
|
||||
/>
|
||||
{#if !$context.device.mobile}
|
||||
<Button
|
||||
<ActionButton
|
||||
quiet
|
||||
overBackground
|
||||
icon="Code"
|
||||
on:click={() => devToolsStore.actions.setVisible(!$devToolsStore.visible)}
|
||||
>
|
||||
{$devToolsStore.visible ? "Close" : "Open"} DevTools
|
||||
</Button>
|
||||
</ActionButton>
|
||||
{/if}
|
||||
<ActionButton
|
||||
quiet
|
||||
icon="Close"
|
||||
on:click={() => window.parent.closePreview?.()}
|
||||
>
|
||||
Close preview
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dev-preview-header {
|
||||
flex: 0 0 50px;
|
||||
height: 50px;
|
||||
flex: 0 0 60px;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
background-color: var(--spectrum-global-color-blue-400);
|
||||
background-color: black;
|
||||
padding: 0 var(--spacing-xl);
|
||||
grid-template-columns: 1fr auto auto;
|
||||
grid-template-columns: 1fr auto auto auto;
|
||||
grid-gap: var(--spacing-xl);
|
||||
}
|
||||
.dev-preview-header.mobile {
|
||||
flex: 0 0 50px;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
}
|
||||
.dev-preview-header :global(.spectrum-Heading),
|
||||
.dev-preview-header :global(.spectrum-Picker-menuIcon),
|
||||
.dev-preview-header :global(.spectrum-Picker-label) {
|
||||
color: white !important;
|
||||
.dev-preview-header :global(.spectrum-Icon),
|
||||
.dev-preview-header :global(.spectrum-Picker-label),
|
||||
.dev-preview-header :global(.spectrum-ActionButton) {
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
.dev-preview-header :global(.spectrum-Picker) {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
transition: background 130ms ease-out;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.dev-preview-header :global(.spectrum-ActionButton:hover),
|
||||
.dev-preview-header :global(.spectrum-Picker:hover),
|
||||
.dev-preview-header :global(.spectrum-Picker.is-open) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.dev-preview-header :global(.spectrum-ActionButton:active) {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
@media print {
|
||||
.dev-preview-header {
|
||||
|
|
|
@ -2,7 +2,6 @@ import ClientApp from "./components/ClientApp.svelte"
|
|||
import {
|
||||
builderStore,
|
||||
appStore,
|
||||
devToolsStore,
|
||||
blockStore,
|
||||
componentStore,
|
||||
environmentStore,
|
||||
|
@ -51,11 +50,6 @@ const loadBudibase = async () => {
|
|||
await environmentStore.actions.fetchEnvironment()
|
||||
}
|
||||
|
||||
// Enable dev tools or not. We need to be using a dev app and not inside
|
||||
// the builder preview to enable them.
|
||||
const enableDevTools = !get(builderStore).inBuilder && get(appStore).isDevApp
|
||||
devToolsStore.actions.setEnabled(enableDevTools)
|
||||
|
||||
// Register handler for runtime events from the builder
|
||||
window.handleBuilderRuntimeEvent = (type, data) => {
|
||||
if (!window["##BUDIBASE_IN_BUILDER##"]) {
|
||||
|
|
|
@ -2,13 +2,14 @@ import { derived } from "svelte/store"
|
|||
import { Constants } from "@budibase/frontend-core"
|
||||
import { devToolsStore } from "../devTools.js"
|
||||
import { authStore } from "../auth.js"
|
||||
import { devToolsEnabled } from "./devToolsEnabled.js"
|
||||
|
||||
// Derive the current role of the logged-in user
|
||||
export const currentRole = derived(
|
||||
[devToolsStore, authStore],
|
||||
([$devToolsStore, $authStore]) => {
|
||||
[devToolsEnabled, devToolsStore, authStore],
|
||||
([$devToolsEnabled, $devToolsStore, $authStore]) => {
|
||||
return (
|
||||
($devToolsStore.enabled && $devToolsStore.role) ||
|
||||
($devToolsEnabled && $devToolsStore.role) ||
|
||||
$authStore?.roleId ||
|
||||
Constants.Roles.PUBLIC
|
||||
)
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { derived } from "svelte/store"
|
||||
import { appStore } from "../app.js"
|
||||
import { builderStore } from "../builder.js"
|
||||
|
||||
export const devToolsEnabled = derived(
|
||||
[appStore, builderStore],
|
||||
([$appStore, $builderStore]) => {
|
||||
return !$builderStore.inBuilder && $appStore.isDevApp
|
||||
}
|
||||
)
|
|
@ -3,3 +3,4 @@
|
|||
// separately we can keep our actual stores lean and performant.
|
||||
export { currentRole } from "./currentRole.js"
|
||||
export { dndComponentPath } from "./dndComponentPath.js"
|
||||
export { devToolsEnabled } from "./devToolsEnabled.js"
|
||||
|
|
|
@ -4,7 +4,6 @@ import { authStore } from "./auth"
|
|||
import { API } from "../api"
|
||||
|
||||
const initialState = {
|
||||
enabled: false,
|
||||
visible: false,
|
||||
allowSelection: false,
|
||||
role: null,
|
||||
|
@ -13,13 +12,6 @@ const initialState = {
|
|||
const createDevToolStore = () => {
|
||||
const store = createLocalStorageStore("bb-devtools", initialState)
|
||||
|
||||
const setEnabled = enabled => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
enabled,
|
||||
}))
|
||||
}
|
||||
|
||||
const setVisible = visible => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
|
@ -46,7 +38,7 @@ const createDevToolStore = () => {
|
|||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
actions: { setEnabled, setVisible, setAllowSelection, changeRole },
|
||||
actions: { setVisible, setAllowSelection, changeRole },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,13 +26,16 @@ export const buildDatasourceEndpoints = API => ({
|
|||
* Creates a datasource
|
||||
* @param datasource the datasource to create
|
||||
* @param fetchSchema whether to fetch the schema or not
|
||||
* @param tablesFilter a list of tables to actually fetch rather than simply
|
||||
* all that are accessible.
|
||||
*/
|
||||
createDatasource: async ({ datasource, fetchSchema }) => {
|
||||
createDatasource: async ({ datasource, fetchSchema, tablesFilter }) => {
|
||||
return await API.post({
|
||||
url: "/api/datasources",
|
||||
body: {
|
||||
datasource,
|
||||
fetchSchema,
|
||||
tablesFilter,
|
||||
},
|
||||
})
|
||||
},
|
||||
|
@ -69,4 +72,15 @@ export const buildDatasourceEndpoints = API => ({
|
|||
body: { datasource },
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch table names available within the datasource, for filtering out undesired tables
|
||||
* @param datasource the datasource configuration to use for fetching tables
|
||||
*/
|
||||
fetchInfoForDatasource: async datasource => {
|
||||
return await API.post({
|
||||
url: `/api/datasources/info`,
|
||||
body: { datasource },
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
|
@ -97,7 +97,7 @@
|
|||
"koa2-ratelimit": "1.1.1",
|
||||
"lodash": "4.17.21",
|
||||
"memorystream": "0.3.1",
|
||||
"mongodb": "4.9",
|
||||
"mongodb": "5.6",
|
||||
"mssql": "6.2.3",
|
||||
"mysql2": "2.3.3",
|
||||
"node-fetch": "2.6.7",
|
||||
|
|
|
@ -103,6 +103,22 @@ async function buildSchemaHelper(datasource: Datasource) {
|
|||
return { tables: connector.tables, error }
|
||||
}
|
||||
|
||||
async function buildFilteredSchema(datasource: Datasource, filter?: string[]) {
|
||||
let { tables, error } = await buildSchemaHelper(datasource)
|
||||
let finalTables = tables
|
||||
if (filter) {
|
||||
finalTables = {}
|
||||
for (let key in tables) {
|
||||
if (
|
||||
filter.some((filter: any) => filter.toLowerCase() === key.toLowerCase())
|
||||
) {
|
||||
finalTables[key] = tables[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
return { tables: finalTables, error }
|
||||
}
|
||||
|
||||
export async function fetch(ctx: UserCtx) {
|
||||
// Get internal tables
|
||||
const db = context.getAppDB()
|
||||
|
@ -174,43 +190,28 @@ export async function information(
|
|||
}
|
||||
const tableNames = await connector.getTableNames()
|
||||
ctx.body = {
|
||||
tableNames,
|
||||
tableNames: tableNames.sort(),
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildSchemaFromDb(ctx: UserCtx) {
|
||||
const db = context.getAppDB()
|
||||
const datasource = await sdk.datasources.get(ctx.params.datasourceId)
|
||||
const tablesFilter = ctx.request.body.tablesFilter
|
||||
const datasource = await sdk.datasources.get(ctx.params.datasourceId)
|
||||
|
||||
let { tables, error } = await buildSchemaHelper(datasource)
|
||||
if (tablesFilter) {
|
||||
if (!datasource.entities) {
|
||||
datasource.entities = {}
|
||||
}
|
||||
for (let key in tables) {
|
||||
if (
|
||||
tablesFilter.some(
|
||||
(filter: any) => filter.toLowerCase() === key.toLowerCase()
|
||||
)
|
||||
) {
|
||||
datasource.entities[key] = tables[key]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
datasource.entities = tables
|
||||
}
|
||||
const { tables, error } = await buildFilteredSchema(datasource, tablesFilter)
|
||||
datasource.entities = tables
|
||||
|
||||
setDefaultDisplayColumns(datasource)
|
||||
const dbResp = await db.put(datasource)
|
||||
datasource._rev = dbResp.rev
|
||||
const cleanedDatasource = await sdk.datasources.removeSecretSingle(datasource)
|
||||
|
||||
const response: any = { datasource: cleanedDatasource }
|
||||
const res: any = { datasource: cleanedDatasource }
|
||||
if (error) {
|
||||
response.error = error
|
||||
res.error = error
|
||||
}
|
||||
ctx.body = response
|
||||
ctx.body = res
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -320,6 +321,7 @@ export async function save(
|
|||
const db = context.getAppDB()
|
||||
const plus = ctx.request.body.datasource.plus
|
||||
const fetchSchema = ctx.request.body.fetchSchema
|
||||
const tablesFilter = ctx.request.body.tablesFilter
|
||||
|
||||
const datasource = {
|
||||
_id: generateDatasourceID({ plus }),
|
||||
|
@ -329,7 +331,10 @@ export async function save(
|
|||
|
||||
let schemaError = null
|
||||
if (fetchSchema) {
|
||||
const { tables, error } = await buildSchemaHelper(datasource)
|
||||
const { tables, error } = await buildFilteredSchema(
|
||||
datasource,
|
||||
tablesFilter
|
||||
)
|
||||
schemaError = error
|
||||
datasource.entities = tables
|
||||
setDefaultDisplayColumns(datasource)
|
||||
|
|
|
@ -26,6 +26,10 @@ export default function process(updateCb?: UpdateCallback) {
|
|||
// if something not found - no changes to perform
|
||||
if (err?.status === 404) {
|
||||
return
|
||||
}
|
||||
// The user has already been sync in another process
|
||||
else if (err?.status === 409) {
|
||||
return
|
||||
} else {
|
||||
logging.logAlert("Failed to perform user/group app sync", err)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import { buildExternalTableId, finaliseExternalTables } from "./utils"
|
|||
import { GoogleSpreadsheet, GoogleSpreadsheetRow } from "google-spreadsheet"
|
||||
import fetch from "node-fetch"
|
||||
import { cache, configs, context, HTTPError } from "@budibase/backend-core"
|
||||
import { dataFilters } from "@budibase/shared-core"
|
||||
import { dataFilters, utils } from "@budibase/shared-core"
|
||||
import { GOOGLE_SHEETS_PRIMARY_KEY } from "../constants"
|
||||
import sdk from "../sdk"
|
||||
|
||||
|
@ -150,7 +150,6 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
|||
|
||||
async testConnection(): Promise<ConnectionInfo> {
|
||||
try {
|
||||
await setupCreationAuth(this.config)
|
||||
await this.connect()
|
||||
return { connected: true }
|
||||
} catch (e: any) {
|
||||
|
@ -211,6 +210,8 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
|||
|
||||
async connect() {
|
||||
try {
|
||||
await setupCreationAuth(this.config)
|
||||
|
||||
// Initialise oAuth client
|
||||
let googleConfig = await configs.getGoogleDatasourceConfig()
|
||||
if (!googleConfig) {
|
||||
|
@ -273,24 +274,24 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
|||
}
|
||||
|
||||
async buildSchema(datasourceId: string, entities: Record<string, Table>) {
|
||||
// not fully configured yet
|
||||
if (!this.config.auth) {
|
||||
return
|
||||
}
|
||||
await this.connect()
|
||||
const sheets = this.client.sheetsByIndex
|
||||
const tables: Record<string, Table> = {}
|
||||
for (let sheet of sheets) {
|
||||
// must fetch rows to determine schema
|
||||
await sheet.getRows()
|
||||
await utils.parallelForeach(
|
||||
sheets,
|
||||
async sheet => {
|
||||
// must fetch rows to determine schema
|
||||
await sheet.getRows({ limit: 0, offset: 0 })
|
||||
|
||||
const id = buildExternalTableId(datasourceId, sheet.title)
|
||||
tables[sheet.title] = this.getTableSchema(
|
||||
sheet.title,
|
||||
sheet.headerValues,
|
||||
id
|
||||
)
|
||||
}
|
||||
const id = buildExternalTableId(datasourceId, sheet.title)
|
||||
tables[sheet.title] = this.getTableSchema(
|
||||
sheet.title,
|
||||
sheet.headerValues,
|
||||
id
|
||||
)
|
||||
},
|
||||
10
|
||||
)
|
||||
const final = finaliseExternalTables(tables, entities)
|
||||
this.tables = final.tables
|
||||
this.schemaErrors = final.errors
|
||||
|
|
|
@ -351,7 +351,7 @@ const SCHEMA: Integration = getSchema()
|
|||
|
||||
class MongoIntegration implements IntegrationBase {
|
||||
private config: MongoDBConfig
|
||||
private client: any
|
||||
private client: MongoClient
|
||||
|
||||
constructor(config: MongoDBConfig) {
|
||||
this.config = config
|
||||
|
@ -372,6 +372,8 @@ class MongoIntegration implements IntegrationBase {
|
|||
response.connected = true
|
||||
} catch (e: any) {
|
||||
response.error = e.message as string
|
||||
} finally {
|
||||
await this.client.close()
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
@ -380,7 +382,7 @@ class MongoIntegration implements IntegrationBase {
|
|||
return this.client.connect()
|
||||
}
|
||||
|
||||
createObjectIds(json: any): object {
|
||||
createObjectIds(json: any) {
|
||||
const self = this
|
||||
function interpolateObjectIds(json: any) {
|
||||
for (let field of Object.keys(json)) {
|
||||
|
|
|
@ -322,7 +322,8 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
|||
await this.openConnection()
|
||||
const columnsResponse: { rows: PostgresColumn[] } =
|
||||
await this.client.query(this.COLUMNS_SQL)
|
||||
return columnsResponse.rows.map(row => row.table_name)
|
||||
const names = columnsResponse.rows.map(row => row.table_name)
|
||||
return [...new Set(names)]
|
||||
} finally {
|
||||
await this.closeConnection()
|
||||
}
|
||||
|
|
|
@ -164,5 +164,6 @@ export function mergeConfigs(update: Datasource, old: Datasource) {
|
|||
delete update.config[key]
|
||||
}
|
||||
}
|
||||
|
||||
return update
|
||||
}
|
||||
|
|
|
@ -4,3 +4,42 @@ export function unreachable(
|
|||
) {
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
export async function parallelForeach<T>(
|
||||
items: T[],
|
||||
task: (item: T) => Promise<void>,
|
||||
maxConcurrency: number
|
||||
): Promise<void> {
|
||||
const promises: Promise<void>[] = []
|
||||
let index = 0
|
||||
|
||||
const processItem = async (item: T) => {
|
||||
try {
|
||||
await task(item)
|
||||
} finally {
|
||||
processNext()
|
||||
}
|
||||
}
|
||||
|
||||
const processNext = () => {
|
||||
if (index >= items.length) {
|
||||
// No more items to process
|
||||
return
|
||||
}
|
||||
|
||||
const item = items[index]
|
||||
index++
|
||||
|
||||
const promise = processItem(item)
|
||||
promises.push(promise)
|
||||
|
||||
if (promises.length >= maxConcurrency) {
|
||||
Promise.race(promises).then(processNext)
|
||||
} else {
|
||||
processNext()
|
||||
}
|
||||
}
|
||||
processNext()
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ export interface UpdateDatasourceResponse {
|
|||
export interface CreateDatasourceRequest {
|
||||
datasource: Datasource
|
||||
fetchSchema?: boolean
|
||||
tablesFilter: string[]
|
||||
}
|
||||
|
||||
export interface VerifyDatasourceRequest {
|
||||
|
|
|
@ -15,6 +15,12 @@ async function generateReport() {
|
|||
return JSON.parse(report)
|
||||
}
|
||||
|
||||
const env = process.argv.slice(2)[0]
|
||||
|
||||
if (!env) {
|
||||
throw new Error("environment argument is required")
|
||||
}
|
||||
|
||||
async function discordResultsNotification(report) {
|
||||
const {
|
||||
numTotalTestSuites,
|
||||
|
@ -39,8 +45,8 @@ async function discordResultsNotification(report) {
|
|||
content: `**Nightly Tests Status**: ${OUTCOME}`,
|
||||
embeds: [
|
||||
{
|
||||
title: "Budi QA Bot",
|
||||
description: `Nightly Tests`,
|
||||
title: `Budi QA Bot - ${env}`,
|
||||
description: `API Integration Tests`,
|
||||
url: GITHUB_ACTIONS_RUN_URL,
|
||||
color: OUTCOME === "success" ? 3066993 : 15548997,
|
||||
timestamp: new Date(),
|
||||
|
|
|
@ -60,8 +60,16 @@ export default class AccountAPI {
|
|||
}
|
||||
|
||||
async delete(accountID: string) {
|
||||
const [response, json] = await this.client.del(`/api/accounts/${accountID}`)
|
||||
expect(response).toHaveStatusCode(200)
|
||||
const [response, json] = await this.client.del(
|
||||
`/api/accounts/${accountID}`,
|
||||
{
|
||||
internal: true,
|
||||
}
|
||||
)
|
||||
// can't use expect here due to use in global teardown
|
||||
if (response.status !== 204) {
|
||||
throw new Error(`Could not delete accountId=${accountID}`)
|
||||
}
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ describe("datasource validators", () => {
|
|||
const result = await integration.testConnection()
|
||||
expect(result).toEqual({
|
||||
connected: false,
|
||||
error: "Error: getaddrinfo ENOTFOUND http",
|
||||
error: "getaddrinfo ENOTFOUND http",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { GenericContainer } from "testcontainers"
|
||||
import postgres from "../../../../packages/server/src/integrations/postgres"
|
||||
|
||||
jest.unmock("pg")
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ const API_OPTS: APIRequestOpts = { doExpect: false }
|
|||
async function deleteAccount() {
|
||||
// @ts-ignore
|
||||
const accountID = global.qa.accountId
|
||||
// can't run 'expect' blocks in teardown
|
||||
await accountsApi.accounts.delete(accountID)
|
||||
}
|
||||
|
||||
|
|
29
yarn.lock
29
yarn.lock
|
@ -7989,12 +7989,10 @@ bson@*:
|
|||
resolved "https://registry.yarnpkg.com/bson/-/bson-5.0.1.tgz#4cd3eeeabf6652ef0d6ab600f9a18212d39baac3"
|
||||
integrity sha512-y09gBGusgHtinMon/GVbv1J6FrXhnr/+6hqLlSmEFzkz6PodqF6TxjyvfvY3AfO+oG1mgUtbC86xSbOlwvM62Q==
|
||||
|
||||
bson@^4.7.0:
|
||||
version "4.7.2"
|
||||
resolved "https://registry.yarnpkg.com/bson/-/bson-4.7.2.tgz#320f4ad0eaf5312dd9b45dc369cc48945e2a5f2e"
|
||||
integrity sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==
|
||||
dependencies:
|
||||
buffer "^5.6.0"
|
||||
bson@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/bson/-/bson-5.3.0.tgz#37b006df4cd91ed125cb686467c1dd6d4606b514"
|
||||
integrity sha512-ukmCZMneMlaC5ebPHXIkP8YJzNl5DC41N5MAIvKDqLggdao342t4McltoJBQfQya/nHBWAcSsYRqlXPoQkTJag==
|
||||
|
||||
buffer-alloc-unsafe@^1.1.0:
|
||||
version "1.1.0"
|
||||
|
@ -18210,7 +18208,7 @@ moment@^2.29.4:
|
|||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
|
||||
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
||||
|
||||
mongodb-connection-string-url@^2.5.3:
|
||||
mongodb-connection-string-url@^2.6.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz#57901bf352372abdde812c81be47b75c6b2ec5cf"
|
||||
integrity sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==
|
||||
|
@ -18218,15 +18216,14 @@ mongodb-connection-string-url@^2.5.3:
|
|||
"@types/whatwg-url" "^8.2.1"
|
||||
whatwg-url "^11.0.0"
|
||||
|
||||
mongodb@4.9:
|
||||
version "4.9.1"
|
||||
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-4.9.1.tgz#0c769448228bcf9a6aa7d16daa3625b48312479e"
|
||||
integrity sha512-ZhgI/qBf84fD7sI4waZBoLBNJYPQN5IOC++SBCiPiyhzpNKOxN/fi0tBHvH2dEC42HXtNEbFB0zmNz4+oVtorQ==
|
||||
mongodb@5.6:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.6.0.tgz#caff5278341bfc0f1ef6f394bb403d207de03d1e"
|
||||
integrity sha512-z8qVs9NfobHJm6uzK56XBZF8XwM9H294iRnB7wNjF0SnY93si5HPziIJn+qqvUR5QOff/4L0gCD6SShdR/GtVQ==
|
||||
dependencies:
|
||||
bson "^4.7.0"
|
||||
denque "^2.1.0"
|
||||
mongodb-connection-string-url "^2.5.3"
|
||||
socks "^2.7.0"
|
||||
bson "^5.3.0"
|
||||
mongodb-connection-string-url "^2.6.0"
|
||||
socks "^2.7.1"
|
||||
optionalDependencies:
|
||||
saslprep "^1.0.3"
|
||||
|
||||
|
@ -23100,7 +23097,7 @@ socks-proxy-agent@^7.0.0:
|
|||
debug "^4.3.3"
|
||||
socks "^2.6.2"
|
||||
|
||||
socks@^2.3.3, socks@^2.6.2, socks@^2.7.0:
|
||||
socks@^2.3.3, socks@^2.6.2, socks@^2.7.1:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55"
|
||||
integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==
|
||||
|
|
Loading…
Reference in New Issue