Post Sign Up Onboarding Changes (#10701)
* wip * PR Feedback * Fixes * PR Feedback * PR Feedback * PR Feedback
This commit is contained in:
parent
4e16c34366
commit
23ee9f4af8
|
@ -1,255 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
ModalContent,
|
|
||||||
Modal,
|
|
||||||
Body,
|
|
||||||
Layout,
|
|
||||||
Detail,
|
|
||||||
Heading,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import ICONS from "../icons"
|
|
||||||
import { API } from "api"
|
|
||||||
import { IntegrationTypes, DatasourceTypes } from "constants/backend"
|
|
||||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
|
||||||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
|
||||||
import GoogleDatasourceConfigModal from "components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte"
|
|
||||||
import { createRestDatasource } from "builderStore/datasource"
|
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
|
|
||||||
import DatasourceCard from "../_components/DatasourceCard.svelte"
|
|
||||||
|
|
||||||
export let modal
|
|
||||||
let integrations = {}
|
|
||||||
let integration = {}
|
|
||||||
let internalTableModal
|
|
||||||
let externalDatasourceModal
|
|
||||||
let importModal
|
|
||||||
|
|
||||||
$: showImportButton = false
|
|
||||||
$: customIntegrations = Object.entries(integrations).filter(
|
|
||||||
entry => entry[1].custom
|
|
||||||
)
|
|
||||||
$: sortedIntegrations = sortIntegrations(integrations)
|
|
||||||
|
|
||||||
checkShowImport()
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
fetchIntegrations()
|
|
||||||
})
|
|
||||||
|
|
||||||
function selectIntegration(integrationType) {
|
|
||||||
const selected = integrations[integrationType]
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
checkShowImport()
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkShowImport() {
|
|
||||||
showImportButton = integration.type === "REST"
|
|
||||||
}
|
|
||||||
|
|
||||||
function showImportModal() {
|
|
||||||
importModal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function chooseNextModal() {
|
|
||||||
if (integration.type === IntegrationTypes.INTERNAL) {
|
|
||||||
externalDatasourceModal.hide()
|
|
||||||
internalTableModal.show()
|
|
||||||
} else if (integration.type === IntegrationTypes.REST) {
|
|
||||||
try {
|
|
||||||
// Skip modal for rest, create straight away
|
|
||||||
const resp = await createRestDatasource(integration)
|
|
||||||
$goto(`./datasource/${resp._id}`)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error creating datasource")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
externalDatasourceModal.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchIntegrations() {
|
|
||||||
let newIntegrations = {
|
|
||||||
[IntegrationTypes.INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const integrationList = await API.getIntegrations()
|
|
||||||
newIntegrations = {
|
|
||||||
...newIntegrations,
|
|
||||||
...integrationList,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error fetching integrations")
|
|
||||||
}
|
|
||||||
integrations = newIntegrations
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal bind:this={internalTableModal}>
|
|
||||||
<CreateTableModal />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal bind:this={externalDatasourceModal}>
|
|
||||||
{#if integration?.auth?.type === "google"}
|
|
||||||
<GoogleDatasourceConfigModal {integration} {modal} />
|
|
||||||
{:else}
|
|
||||||
<DatasourceConfigModal {integration} {modal} />
|
|
||||||
{/if}
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal bind:this={importModal}>
|
|
||||||
{#if integration.type === "REST"}
|
|
||||||
<ImportRestQueriesModal
|
|
||||||
navigateDatasource={true}
|
|
||||||
createDatasource={true}
|
|
||||||
onCancel={() => modal.show()}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
|
||||||
<ModalContent
|
|
||||||
disabled={!Object.keys(integration).length}
|
|
||||||
title="Add datasource"
|
|
||||||
confirmText="Continue"
|
|
||||||
showSecondaryButton={showImportButton}
|
|
||||||
secondaryButtonText="Import"
|
|
||||||
secondaryAction={() => showImportModal()}
|
|
||||||
showCancelButton={false}
|
|
||||||
size="M"
|
|
||||||
onConfirm={() => {
|
|
||||||
chooseNextModal()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Body size="S">Get started with Budibase DB</Body>
|
|
||||||
<div
|
|
||||||
class:selected={integration.type === IntegrationTypes.INTERNAL}
|
|
||||||
on:click={() => selectIntegration(IntegrationTypes.INTERNAL)}
|
|
||||||
class="item hoverable"
|
|
||||||
>
|
|
||||||
<div class="item-body with-type">
|
|
||||||
<svelte:component this={ICONS.BUDIBASE} height="20" width="20" />
|
|
||||||
<div class="text">
|
|
||||||
<Heading size="XXS">Budibase DB</Heading>
|
|
||||||
<Detail size="S" class="type">Non-relational</Detail>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Body size="S">Connect to an external datasource</Body>
|
|
||||||
<div class="item-list">
|
|
||||||
{#each sortedIntegrations.filter(([key, val]) => key !== IntegrationTypes.INTERNAL && !val.custom) as [integrationType, schema]}
|
|
||||||
<DatasourceCard
|
|
||||||
on:selected={evt => selectIntegration(evt.detail)}
|
|
||||||
{schema}
|
|
||||||
bind:integrationType
|
|
||||||
{integration}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
{#if customIntegrations.length > 0}
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Body size="S">Custom datasource</Body>
|
|
||||||
<div class="item-list">
|
|
||||||
{#each customIntegrations as [integrationType, schema]}
|
|
||||||
<DatasourceCard
|
|
||||||
on:selected={evt => selectIntegration(evt.detail)}
|
|
||||||
{schema}
|
|
||||||
bind:integrationType
|
|
||||||
{integration}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
{/if}
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.item-list {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(150px, 1fr));
|
|
||||||
grid-gap: var(--spectrum-alias-grid-baseline);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text :global(.spectrum-Detail) {
|
|
||||||
color: var(--spectrum-global-color-gray-700);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -11,7 +11,6 @@
|
||||||
import { DatasourceFeature } from "@budibase/types"
|
import { DatasourceFeature } from "@budibase/types"
|
||||||
|
|
||||||
export let integration
|
export let integration
|
||||||
export let modal
|
|
||||||
|
|
||||||
// kill the reference so the input isn't saved
|
// kill the reference so the input isn't saved
|
||||||
let datasource = cloneDeep(integration)
|
let datasource = cloneDeep(integration)
|
||||||
|
@ -62,7 +61,6 @@
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={`Connect to ${name}`}
|
title={`Connect to ${name}`}
|
||||||
onConfirm={() => saveDatasource()}
|
onConfirm={() => saveDatasource()}
|
||||||
onCancel={() => modal.show()}
|
|
||||||
confirmText={datasource.plus ? "Connect" : "Save and continue to query"}
|
confirmText={datasource.plus ? "Connect" : "Save and continue to query"}
|
||||||
cancelText="Back"
|
cancelText="Back"
|
||||||
showSecondaryButton={datasource.plus}
|
showSecondaryButton={datasource.plus}
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
export let integration
|
export let integration
|
||||||
export let modal
|
|
||||||
|
|
||||||
// kill the reference so the input isn't saved
|
// kill the reference so the input isn't saved
|
||||||
let datasource = cloneDeep(integration)
|
let datasource = cloneDeep(integration)
|
||||||
|
@ -21,7 +20,6 @@
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={`Connect to ${IntegrationNames[datasource.type]}`}
|
title={`Connect to ${IntegrationNames[datasource.type]}`}
|
||||||
onCancel={() => modal.show()}
|
|
||||||
cancelText="Back"
|
cancelText="Back"
|
||||||
size="L"
|
size="L"
|
||||||
>
|
>
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { parseFile } from "./utils"
|
import { parseFile } from "./utils"
|
||||||
|
|
||||||
|
let fileInput
|
||||||
let error = null
|
let error = null
|
||||||
let fileName = null
|
let fileName = null
|
||||||
let fileType = null
|
let fileType = null
|
||||||
|
@ -16,6 +17,7 @@
|
||||||
export let schema = {}
|
export let schema = {}
|
||||||
export let allValid = true
|
export let allValid = true
|
||||||
export let displayColumn = null
|
export let displayColumn = null
|
||||||
|
export let promptUpload = false
|
||||||
|
|
||||||
const typeOptions = [
|
const typeOptions = [
|
||||||
{
|
{
|
||||||
|
@ -99,10 +101,19 @@
|
||||||
schema[name].type = e.detail
|
schema[name].type = e.detail
|
||||||
schema[name].constraints = FIELDS[e.detail.toUpperCase()].constraints
|
schema[name].constraints = FIELDS[e.detail.toUpperCase()].constraints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openFileUpload = (promptUpload, fileInput) => {
|
||||||
|
if (promptUpload && fileInput) {
|
||||||
|
fileInput.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: openFileUpload(promptUpload, fileInput)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="dropzone">
|
<div class="dropzone">
|
||||||
<input
|
<input
|
||||||
|
bind:this={fileInput}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
id="file-upload"
|
id="file-upload"
|
||||||
accept="text/csv,application/json"
|
accept="text/csv,application/json"
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
? selectedSource._id
|
? selectedSource._id
|
||||||
: BUDIBASE_INTERNAL_DB_ID
|
: BUDIBASE_INTERNAL_DB_ID
|
||||||
|
|
||||||
|
export let promptUpload = false
|
||||||
export let name
|
export let name
|
||||||
export let beforeSave = async () => {}
|
export let beforeSave = async () => {}
|
||||||
export let afterSave = async table => {
|
export let afterSave = async table => {
|
||||||
|
@ -136,7 +137,13 @@
|
||||||
<Label grey extraSmall
|
<Label grey extraSmall
|
||||||
>Create a Table from a CSV or JSON file (Optional)</Label
|
>Create a Table from a CSV or JSON file (Optional)</Label
|
||||||
>
|
>
|
||||||
<TableDataImport bind:rows bind:schema bind:allValid bind:displayColumn />
|
<TableDataImport
|
||||||
|
{promptUpload}
|
||||||
|
bind:rows
|
||||||
|
bind:schema
|
||||||
|
bind:allValid
|
||||||
|
bind:displayColumn
|
||||||
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
faLock,
|
faLock,
|
||||||
faFileArrowUp,
|
faFileArrowUp,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
|
faCircleInfo,
|
||||||
} from "@fortawesome/free-solid-svg-icons"
|
} from "@fortawesome/free-solid-svg-icons"
|
||||||
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
|
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
|
||||||
|
|
||||||
|
@ -20,7 +21,8 @@
|
||||||
faDiscord,
|
faDiscord,
|
||||||
faEnvelope,
|
faEnvelope,
|
||||||
faFileArrowUp,
|
faFileArrowUp,
|
||||||
faChevronLeft
|
faChevronLeft,
|
||||||
|
faCircleInfo
|
||||||
)
|
)
|
||||||
dom.watch()
|
dom.watch()
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -71,6 +71,9 @@
|
||||||
tourStep.onComplete()
|
tourStep.onComplete()
|
||||||
}
|
}
|
||||||
popover.hide()
|
popover.hide()
|
||||||
|
if (tourStep.endRoute) {
|
||||||
|
$goto(tourStep.endRoute)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,6 +76,7 @@ const getTours = () => {
|
||||||
title: "Publish",
|
title: "Publish",
|
||||||
layout: OnboardingPublish,
|
layout: OnboardingPublish,
|
||||||
route: "/builder/app/:application/design",
|
route: "/builder/app/:application/design",
|
||||||
|
endRoute: "/builder/app/:application/data",
|
||||||
query: ".toprightnav #builder-app-publish-button",
|
query: ".toprightnav #builder-app-publish-button",
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
|
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { writable, get as svelteGet } from "svelte/store"
|
import { writable, get as svelteGet } from "svelte/store"
|
||||||
import {
|
import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui"
|
||||||
notifications,
|
|
||||||
Input,
|
|
||||||
ModalContent,
|
|
||||||
Dropzone,
|
|
||||||
Toggle,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { store, automationStore } from "builderStore"
|
import { store, automationStore } from "builderStore"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { apps, admin, auth } from "stores/portal"
|
import { apps, admin, auth } from "stores/portal"
|
||||||
|
@ -22,7 +16,6 @@
|
||||||
|
|
||||||
let creating = false
|
let creating = false
|
||||||
let defaultAppName
|
let defaultAppName
|
||||||
let includeSampleDB = true
|
|
||||||
|
|
||||||
const values = writable({ name: "", url: null })
|
const values = writable({ name: "", url: null })
|
||||||
const validation = createValidationStore()
|
const validation = createValidationStore()
|
||||||
|
@ -117,8 +110,6 @@
|
||||||
data.append("templateName", template.name)
|
data.append("templateName", template.name)
|
||||||
data.append("templateKey", template.key)
|
data.append("templateKey", template.key)
|
||||||
data.append("templateFile", $values.file)
|
data.append("templateFile", $values.file)
|
||||||
} else {
|
|
||||||
data.append("sampleData", includeSampleDB)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create App
|
// Create App
|
||||||
|
@ -213,15 +204,6 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{#if !template && !template?.fromFile}
|
|
||||||
<span>
|
|
||||||
<Toggle
|
|
||||||
text="Include sample data"
|
|
||||||
bind:value={includeSampleDB}
|
|
||||||
disabled={creating}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script>
|
||||||
|
import { Body, Label } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let title
|
||||||
|
export let description
|
||||||
|
export let disabled
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div on:click class:disabled class="option">
|
||||||
|
<div class="header">
|
||||||
|
<div class="icon">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<Body>{title}</Body>
|
||||||
|
</div>
|
||||||
|
<Label>{description}</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.option {
|
||||||
|
background-color: var(--background);
|
||||||
|
border: 1px solid var(--grey-4);
|
||||||
|
padding: 10px 16px 14px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option :global(label) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option:hover {
|
||||||
|
background-color: var(--background-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,21 +1,20 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, Layout } from "@budibase/bbui"
|
import { Button, Layout } from "@budibase/bbui"
|
||||||
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
|
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
|
||||||
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
|
|
||||||
import Panel from "components/design/Panel.svelte"
|
import Panel from "components/design/Panel.svelte"
|
||||||
|
import { isActive, goto } from "@roxi/routify"
|
||||||
let modal
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- routify:options index=1 -->
|
<!-- routify:options index=1 -->
|
||||||
<div class="data">
|
<div class="data">
|
||||||
|
{#if !$isActive("./new")}
|
||||||
<Panel title="Sources" borderRight>
|
<Panel title="Sources" borderRight>
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||||
<Button cta on:click={modal.show}>Add source</Button>
|
<Button cta on:click={() => $goto("./new")}>Add source</Button>
|
||||||
<CreateDatasourceModal bind:modal />
|
|
||||||
<DatasourceNavigator />
|
<DatasourceNavigator />
|
||||||
</Layout>
|
</Layout>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -1,22 +1,17 @@
|
||||||
<script>
|
<script>
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { admin } from "stores/portal"
|
|
||||||
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
|
|
||||||
import { datasources } from "stores/backend"
|
import { datasources } from "stores/backend"
|
||||||
|
|
||||||
let modal
|
$: hasData =
|
||||||
$: setupComplete =
|
|
||||||
$datasources.list.find(x => (x._id = "bb_internal"))?.entities?.length >
|
$datasources.list.find(x => (x._id = "bb_internal"))?.entities?.length >
|
||||||
1 || $datasources.list.length > 1
|
1 || $datasources.list.length > 1
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!setupComplete && !$admin.isDev) {
|
if (!hasData) {
|
||||||
modal.show()
|
$redirect("./new")
|
||||||
} else {
|
} else {
|
||||||
$redirect("./table")
|
$redirect("./table")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CreateDatasourceModal bind:modal />
|
|
||||||
|
|
|
@ -0,0 +1,257 @@
|
||||||
|
<script>
|
||||||
|
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 {
|
||||||
|
IntegrationTypes,
|
||||||
|
DatasourceTypes,
|
||||||
|
DEFAULT_BB_DATASOURCE_ID,
|
||||||
|
} from "constants/backend"
|
||||||
|
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||||
|
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||||
|
import GoogleDatasourceConfigModal from "components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte"
|
||||||
|
import { createRestDatasource } from "builderStore/datasource"
|
||||||
|
import DatasourceOption from "./_DatasourceOption.svelte"
|
||||||
|
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
||||||
|
import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
|
||||||
|
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
||||||
|
|
||||||
|
let internalTableModal
|
||||||
|
let externalDatasourceModal
|
||||||
|
let integrations = []
|
||||||
|
let integration = null
|
||||||
|
let disabled = false
|
||||||
|
let promptUpload = false
|
||||||
|
|
||||||
|
$: hasData = $datasources.list.length > 1 || $tables.list.length > 1
|
||||||
|
$: hasDefaultData =
|
||||||
|
$datasources.list.findIndex(
|
||||||
|
datasource => datasource._id === DEFAULT_BB_DATASOURCE_ID
|
||||||
|
) !== -1
|
||||||
|
|
||||||
|
const createSampleData = async () => {
|
||||||
|
disabled = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await API.addSampleData($params.application)
|
||||||
|
await tables.fetch()
|
||||||
|
await datasources.fetch()
|
||||||
|
$goto("./table")
|
||||||
|
} catch (e) {
|
||||||
|
disabled = false
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchIntegrations = async () => {
|
||||||
|
const unsortedIntegrations = await API.getIntegrations()
|
||||||
|
integrations = sortIntegrations(unsortedIntegrations)
|
||||||
|
}
|
||||||
|
|
||||||
|
$: fetchIntegrations()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={internalTableModal}>
|
||||||
|
<CreateTableModal {promptUpload} afterSave={handleInternalTableSave} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal bind:this={externalDatasourceModal}>
|
||||||
|
{#if integration?.auth?.type === "google"}
|
||||||
|
<GoogleDatasourceConfigModal {integration} />
|
||||||
|
{:else}
|
||||||
|
<DatasourceConfigModal {integration} />
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
<div class="closeButton">
|
||||||
|
{#if hasData}
|
||||||
|
<Icon hoverable name="Close" on:click={$goto("./table")} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="heading">
|
||||||
|
<Heading weight="light">Add new data source</Heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="subHeading">
|
||||||
|
<Body>Get started with our Budibase DB</Body>
|
||||||
|
<div
|
||||||
|
role="tooltip"
|
||||||
|
title="Budibase DB is built with CouchDB"
|
||||||
|
class="tooltip"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon name="fa-solid fa-circle-info" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="options">
|
||||||
|
<DatasourceOption
|
||||||
|
on:click={handleInternalTable}
|
||||||
|
title="Create new table"
|
||||||
|
description="Non-relational"
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
<svelte:component this={ICONS.BUDIBASE} height="20" width="20" />
|
||||||
|
</DatasourceOption>
|
||||||
|
<DatasourceOption
|
||||||
|
on:click={createSampleData}
|
||||||
|
title="Use sample data"
|
||||||
|
description="Non-relational"
|
||||||
|
disabled={disabled || hasDefaultData}
|
||||||
|
>
|
||||||
|
<svelte:component this={ICONS.BUDIBASE} height="20" width="20" />
|
||||||
|
</DatasourceOption>
|
||||||
|
<DatasourceOption
|
||||||
|
on:click={handleDataImport}
|
||||||
|
title="Upload data"
|
||||||
|
description="Non-relational"
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
<svelte:component this={ICONS.BUDIBASE} height="20" width="20" />
|
||||||
|
</DatasourceOption>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="subHeading">
|
||||||
|
<Body>Or connect to an external datasource</Body>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="options">
|
||||||
|
{#each integrations as [key, value]}
|
||||||
|
<DatasourceOption
|
||||||
|
on:click={() => handleIntegrationSelect(key)}
|
||||||
|
title={value.friendlyName}
|
||||||
|
description={value.type}
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
<IntegrationIcon integrationType={key} schema={value} />
|
||||||
|
</DatasourceOption>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
height: 38px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subHeading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
column-gap: 24px;
|
||||||
|
row-gap: 24px;
|
||||||
|
grid-template-columns: repeat(auto-fit, 235px);
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
max-width: 1050px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,13 +0,0 @@
|
||||||
<script>
|
|
||||||
import PanelHeader from "./PanelHeader.svelte"
|
|
||||||
export let onBack = () => {}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<PanelHeader
|
|
||||||
title="Give it some data"
|
|
||||||
subtitle="Not ready to add yours? Get started with sample data!"
|
|
||||||
{onBack}
|
|
||||||
/>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
|
@ -1,120 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Button, FancyForm, FancyInput, FancyCheckbox } from "@budibase/bbui"
|
|
||||||
import GoogleButton from "components/backend/DatasourceNavigator/_components/GoogleButton.svelte"
|
|
||||||
import { capitalise } from "helpers/helpers"
|
|
||||||
import PanelHeader from "./PanelHeader.svelte"
|
|
||||||
import { helpers } from "@budibase/shared-core"
|
|
||||||
|
|
||||||
export let title = ""
|
|
||||||
export let onBack = null
|
|
||||||
export let onNext = () => {}
|
|
||||||
export let fields = {}
|
|
||||||
export let type = ""
|
|
||||||
|
|
||||||
let errors = {}
|
|
||||||
|
|
||||||
const formatName = name => {
|
|
||||||
if (name === "ca") {
|
|
||||||
return "CA"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "ssl") {
|
|
||||||
return "SSL"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "rejectUnauthorized") {
|
|
||||||
return "Reject Unauthorized"
|
|
||||||
}
|
|
||||||
|
|
||||||
return capitalise(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDefaultValues = fields => {
|
|
||||||
const newValues = {}
|
|
||||||
|
|
||||||
Object.entries(fields).forEach(([name, { default: defaultValue }]) => {
|
|
||||||
if (defaultValue) {
|
|
||||||
newValues[name] = defaultValue
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return newValues
|
|
||||||
}
|
|
||||||
|
|
||||||
const values = getDefaultValues(fields)
|
|
||||||
|
|
||||||
const validateRequired = value => {
|
|
||||||
if (value.length < 1) {
|
|
||||||
return "Required field"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getIsValid = (fields, errors, values) => {
|
|
||||||
for (const [name, { required }] of Object.entries(fields)) {
|
|
||||||
if (required && !values[name]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.values(errors).every(error => !error)
|
|
||||||
}
|
|
||||||
|
|
||||||
$: isValid = getIsValid(fields, errors, values)
|
|
||||||
$: isGoogle = helpers.isGoogleSheets(type)
|
|
||||||
|
|
||||||
const handleNext = async () => {
|
|
||||||
const parsedValues = {}
|
|
||||||
|
|
||||||
Object.entries(values).forEach(([name, value]) => {
|
|
||||||
if (fields[name].type === "number") {
|
|
||||||
parsedValues[name] = parseInt(value, 10)
|
|
||||||
} else {
|
|
||||||
parsedValues[name] = value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isGoogle) {
|
|
||||||
parsedValues.isGoogle = isGoogle
|
|
||||||
}
|
|
||||||
return await onNext(parsedValues)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<PanelHeader
|
|
||||||
{title}
|
|
||||||
subtitle="Fill in the required fields to fetch your tables"
|
|
||||||
{onBack}
|
|
||||||
/>
|
|
||||||
<div class="form">
|
|
||||||
<FancyForm>
|
|
||||||
{#each Object.entries(fields) as [name, { type, default: defaultValue, required }]}
|
|
||||||
{#if type !== "boolean"}
|
|
||||||
<FancyInput
|
|
||||||
bind:value={values[name]}
|
|
||||||
bind:error={errors[name]}
|
|
||||||
validate={required ? validateRequired : () => {}}
|
|
||||||
label={formatName(name)}
|
|
||||||
{type}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
{#each Object.entries(fields) as [name, { type, default: defaultValue, required }]}
|
|
||||||
{#if type === "boolean"}
|
|
||||||
<FancyCheckbox bind:value={values[name]} text={formatName(name)} />
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</FancyForm>
|
|
||||||
</div>
|
|
||||||
{#if isGoogle}
|
|
||||||
<GoogleButton disabled={!isValid} preAuthStep={handleNext} samePage />
|
|
||||||
{:else}
|
|
||||||
<Button cta disabled={!isValid} on:click={handleNext}>Connect</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.form {
|
|
||||||
margin-bottom: 36px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
export let name = ""
|
export let name = ""
|
||||||
export let showData = false
|
|
||||||
|
|
||||||
const rows = [
|
const rows = [
|
||||||
{
|
{
|
||||||
|
@ -49,7 +48,7 @@
|
||||||
<h1>{name}</h1>
|
<h1>{name}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav">Home</div>
|
<div class="nav">Home</div>
|
||||||
<table class={`table ${showData ? "tableVisible" : ""}`}>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>FIRST NAME</th>
|
<th>FIRST NAME</th>
|
||||||
|
@ -71,7 +70,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class={`sidePanel ${showData ? "sidePanelVisible" : ""}`}>
|
<div class="sidePanel">
|
||||||
<h2>{rows[0].firstName}</h2>
|
<h2>{rows[0].firstName}</h2>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="exampleLastName">lastName</label>
|
<label for="exampleLastName">lastName</label>
|
||||||
|
@ -199,14 +198,6 @@
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableVisible {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidePanel {
|
.sidePanel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
@ -216,9 +207,6 @@
|
||||||
top: 0;
|
top: 0;
|
||||||
right: -364px;
|
right: -364px;
|
||||||
padding: 42px 32px;
|
padding: 42px 32px;
|
||||||
}
|
|
||||||
|
|
||||||
.sidePanelVisible {
|
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import PanelHeader from "./PanelHeader.svelte"
|
import PanelHeader from "./PanelHeader.svelte"
|
||||||
import { APP_URL_REGEX } from "constants"
|
import { APP_URL_REGEX } from "constants"
|
||||||
|
|
||||||
|
export let disabled
|
||||||
export let name = ""
|
export let name = ""
|
||||||
export let url = ""
|
export let url = ""
|
||||||
export let onNext = () => {}
|
export let onNext = () => {}
|
||||||
|
@ -71,7 +72,9 @@
|
||||||
{:else}
|
{:else}
|
||||||
<p></p>
|
<p></p>
|
||||||
{/if}
|
{/if}
|
||||||
<Button size="L" cta disabled={!isValid} on:click={onNext}>Lets go!</Button>
|
<Button size="L" cta disabled={!isValid || disabled} on:click={onNext}
|
||||||
|
>Lets go!</Button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,53 +1,31 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import NamePanel from "./_components/NamePanel.svelte"
|
import NamePanel from "./_components/NamePanel.svelte"
|
||||||
import DataPanel from "./_components/DataPanel.svelte"
|
|
||||||
import DatasourceConfigPanel from "./_components/DatasourceConfigPanel.svelte"
|
|
||||||
import ExampleApp from "./_components/ExampleApp.svelte"
|
import ExampleApp from "./_components/ExampleApp.svelte"
|
||||||
import { FancyButton, notifications, Modal, Body } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
|
||||||
import { SplitPage } from "@budibase/frontend-core"
|
import { SplitPage } from "@budibase/frontend-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { store, automationStore } from "builderStore"
|
import { store, automationStore } from "builderStore"
|
||||||
import { saveDatasource } from "builderStore/datasource"
|
import { auth, admin } from "stores/portal"
|
||||||
import { integrations } from "stores/backend"
|
|
||||||
import { auth, admin, organisation } from "stores/portal"
|
|
||||||
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
|
||||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
|
||||||
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
||||||
import { Roles } from "constants/backend"
|
import { Roles } from "constants/backend"
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
|
||||||
import { helpers } from "@budibase/shared-core"
|
|
||||||
import { validateDatasourceConfig } from "builderStore/datasource"
|
|
||||||
import { DatasourceFeature } from "@budibase/types"
|
|
||||||
|
|
||||||
let name = "My first app"
|
let name = "My first app"
|
||||||
let url = "my-first-app"
|
let url = "my-first-app"
|
||||||
let stage = "name"
|
|
||||||
let appId = null
|
let appId = null
|
||||||
|
|
||||||
let plusIntegrations = {}
|
let loading = false
|
||||||
let integrationsLoading = true
|
|
||||||
let creationLoading = false
|
|
||||||
let uploadModal
|
|
||||||
let googleComplete = false
|
|
||||||
|
|
||||||
$: getIntegrations()
|
const createApp = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
const createApp = async useSampleData => {
|
|
||||||
creationLoading = true
|
|
||||||
// Create form data to create app
|
// Create form data to create app
|
||||||
// This is form based and not JSON
|
// This is form based and not JSON
|
||||||
try {
|
|
||||||
let data = new FormData()
|
let data = new FormData()
|
||||||
data.append("name", name.trim())
|
data.append("name", name.trim())
|
||||||
data.append("url", url.trim())
|
data.append("url", url.trim())
|
||||||
data.append("useTemplate", false)
|
data.append("useTemplate", false)
|
||||||
|
|
||||||
if (useSampleData) {
|
|
||||||
data.append("sampleData", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdApp = await API.createApp(data)
|
const createdApp = await API.createApp(data)
|
||||||
|
|
||||||
// Select Correct Application/DB in prep for creating user
|
// Select Correct Application/DB in prep for creating user
|
||||||
|
@ -67,36 +45,6 @@
|
||||||
|
|
||||||
appId = createdApp.instance._id
|
appId = createdApp.instance._id
|
||||||
return createdApp
|
return createdApp
|
||||||
} catch (e) {
|
|
||||||
creationLoading = false
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getIntegrations = async () => {
|
|
||||||
try {
|
|
||||||
await integrations.init()
|
|
||||||
const newPlusIntegrations = {}
|
|
||||||
|
|
||||||
Object.entries($integrations).forEach(([integrationType, schema]) => {
|
|
||||||
// google sheets not available in self-host
|
|
||||||
if (
|
|
||||||
helpers.isGoogleSheets(integrationType) &&
|
|
||||||
!$organisation.googleDatasourceConfigured
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (schema?.plus) {
|
|
||||||
newPlusIntegrations[integrationType] = schema
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
plusIntegrations = newPlusIntegrations
|
|
||||||
} catch (e) {
|
|
||||||
notifications.error("There was a problem communicating with the server.")
|
|
||||||
} finally {
|
|
||||||
integrationsLoading = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToApp = () => {
|
const goToApp = () => {
|
||||||
|
@ -104,152 +52,23 @@
|
||||||
notifications.success(`App created successfully`)
|
notifications.success(`App created successfully`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreateApp = async ({
|
const handleCreateApp = async () => {
|
||||||
datasourceConfig,
|
|
||||||
useSampleData,
|
|
||||||
isGoogle,
|
|
||||||
}) => {
|
|
||||||
let app
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
await createApp()
|
||||||
datasourceConfig &&
|
|
||||||
plusIntegrations[stage].features[DatasourceFeature.CONNECTION_CHECKING]
|
|
||||||
) {
|
|
||||||
const resp = await validateDatasourceConfig({
|
|
||||||
config: datasourceConfig,
|
|
||||||
type: stage,
|
|
||||||
})
|
|
||||||
if (!resp.connected) {
|
|
||||||
notifications.error(
|
|
||||||
`Unable to connect - ${resp.error ?? "Error validating datasource"}`
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app = await createApp(useSampleData)
|
|
||||||
|
|
||||||
let datasource
|
|
||||||
if (datasourceConfig) {
|
|
||||||
datasource = await saveDatasource({
|
|
||||||
plus: true,
|
|
||||||
auth: undefined,
|
|
||||||
name: plusIntegrations[stage].friendlyName,
|
|
||||||
schema: plusIntegrations[stage].datasource,
|
|
||||||
config: datasourceConfig,
|
|
||||||
type: stage,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
store.set()
|
|
||||||
|
|
||||||
if (isGoogle) {
|
|
||||||
googleComplete = true
|
|
||||||
return { datasource, appId: app.appId }
|
|
||||||
} else {
|
|
||||||
goToApp()
|
goToApp()
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
loading = false
|
||||||
creationLoading = false
|
|
||||||
notifications.error("There was a problem creating your app")
|
notifications.error("There was a problem creating your app")
|
||||||
|
|
||||||
// Reset the store so that we don't send up stale headers
|
|
||||||
store.actions.reset()
|
|
||||||
|
|
||||||
// If we successfully created an app, delete it again so that we
|
|
||||||
// can try again once the error has been corrected.
|
|
||||||
// This also ensures onboarding can't be skipped by entering invalid
|
|
||||||
// data credentials.
|
|
||||||
if (app?.appId) {
|
|
||||||
await API.deleteApp(app.appId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={uploadModal}>
|
|
||||||
<CreateTableModal
|
|
||||||
name="Your Data"
|
|
||||||
beforeSave={createApp}
|
|
||||||
afterSave={goToApp}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<div class="full-width">
|
<div class="full-width">
|
||||||
<SplitPage>
|
<SplitPage>
|
||||||
{#if stage === "name"}
|
<NamePanel bind:name bind:url disabled={loading} onNext={handleCreateApp} />
|
||||||
<NamePanel bind:name bind:url onNext={() => (stage = "data")} />
|
|
||||||
{:else if googleComplete}
|
|
||||||
<div class="centered">
|
|
||||||
<Body
|
|
||||||
>Please login to your Google account in the new tab which as opened to
|
|
||||||
continue.</Body
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{:else if integrationsLoading || creationLoading}
|
|
||||||
<div class="centered">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
{:else if stage === "data"}
|
|
||||||
<DataPanel onBack={() => (stage = "name")}>
|
|
||||||
<div class="dataButton">
|
|
||||||
<FancyButton
|
|
||||||
on:click={() => handleCreateApp({ useSampleData: true })}
|
|
||||||
>
|
|
||||||
<div class="dataButtonContent">
|
|
||||||
<div class="dataButtonIcon">
|
|
||||||
<img
|
|
||||||
alt="Budibase Logo"
|
|
||||||
class="budibaseLogo"
|
|
||||||
src={"https://i.imgur.com/Xhdt1YP.png"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
Budibase Sample data
|
|
||||||
</div>
|
|
||||||
</FancyButton>
|
|
||||||
</div>
|
|
||||||
<div class="dataButton">
|
|
||||||
<FancyButton on:click={uploadModal.show}>
|
|
||||||
<div class="dataButtonContent">
|
|
||||||
<div class="dataButtonIcon">
|
|
||||||
<FontAwesomeIcon name="fa-solid fa-file-arrow-up" />
|
|
||||||
</div>
|
|
||||||
Upload data (CSV or JSON)
|
|
||||||
</div>
|
|
||||||
</FancyButton>
|
|
||||||
</div>
|
|
||||||
{#each Object.entries(plusIntegrations) as [integrationType, schema]}
|
|
||||||
<div class="dataButton">
|
|
||||||
<FancyButton on:click={() => (stage = integrationType)}>
|
|
||||||
<div class="dataButtonContent">
|
|
||||||
<div class="dataButtonIcon">
|
|
||||||
<IntegrationIcon {integrationType} {schema} />
|
|
||||||
</div>
|
|
||||||
{schema.friendlyName}
|
|
||||||
</div>
|
|
||||||
</FancyButton>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</DataPanel>
|
|
||||||
{:else if stage in plusIntegrations}
|
|
||||||
<DatasourceConfigPanel
|
|
||||||
title={plusIntegrations[stage].friendlyName}
|
|
||||||
fields={plusIntegrations[stage].datasource}
|
|
||||||
type={stage}
|
|
||||||
onBack={() => (stage = "data")}
|
|
||||||
onNext={data => {
|
|
||||||
const isGoogle = data.isGoogle
|
|
||||||
delete data.isGoogle
|
|
||||||
return handleCreateApp({ datasourceConfig: data, isGoogle })
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<p>There was an problem. Please refresh the page and try again.</p>
|
|
||||||
{/if}
|
|
||||||
<div slot="right">
|
<div slot="right">
|
||||||
<ExampleApp {name} showData={stage !== "name"} />
|
<ExampleApp {name} />
|
||||||
</div>
|
</div>
|
||||||
</SplitPage>
|
</SplitPage>
|
||||||
</div>
|
</div>
|
||||||
|
@ -258,35 +77,4 @@
|
||||||
.full-width {
|
.full-width {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.centered {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dataButton {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dataButtonContent {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.budibaseLogo {
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dataButtonIcon {
|
|
||||||
width: 22px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dataButtonContent :global(svg) {
|
|
||||||
font-size: 18px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -63,9 +63,7 @@ export function createTablesStore() {
|
||||||
|
|
||||||
const savedTable = await API.saveTable(updatedTable)
|
const savedTable = await API.saveTable(updatedTable)
|
||||||
replaceTable(table._id, savedTable)
|
replaceTable(table._id, savedTable)
|
||||||
if (table.type === "external") {
|
|
||||||
await datasources.fetch()
|
await datasources.fetch()
|
||||||
}
|
|
||||||
select(savedTable._id)
|
select(savedTable._id)
|
||||||
return savedTable
|
return savedTable
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,4 +152,10 @@ export const buildAppEndpoints = API => ({
|
||||||
url: `/api/${appId}/components/definitions`,
|
url: `/api/${appId}/components/definitions`,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addSampleData: async appId => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/applications/${appId}/sample`,
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -26,7 +26,10 @@ import {
|
||||||
env as envCore,
|
env as envCore,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { USERS_TABLE_SCHEMA } from "../../constants"
|
import { USERS_TABLE_SCHEMA } from "../../constants"
|
||||||
import { buildDefaultDocs } from "../../db/defaultData/datasource_bb_default"
|
import {
|
||||||
|
DEFAULT_BB_DATASOURCE_ID,
|
||||||
|
buildDefaultDocs,
|
||||||
|
} from "../../db/defaultData/datasource_bb_default"
|
||||||
import { removeAppFromUserRoles } from "../../utilities/workerRequests"
|
import { removeAppFromUserRoles } from "../../utilities/workerRequests"
|
||||||
import { stringToReadStream, isQsTrue } from "../../utilities"
|
import { stringToReadStream, isQsTrue } from "../../utilities"
|
||||||
import { getLocksById, doesUserHaveLock } from "../../utilities/redis"
|
import { getLocksById, doesUserHaveLock } from "../../utilities/redis"
|
||||||
|
@ -111,11 +114,7 @@ function checkAppName(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createInstance(
|
async function createInstance(appId: string, template: any) {
|
||||||
appId: string,
|
|
||||||
template: any,
|
|
||||||
includeSampleData: boolean
|
|
||||||
) {
|
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
await db.put({
|
await db.put({
|
||||||
_id: "_design/database",
|
_id: "_design/database",
|
||||||
|
@ -142,23 +141,27 @@ async function createInstance(
|
||||||
} else {
|
} else {
|
||||||
// create the users table
|
// create the users table
|
||||||
await db.put(USERS_TABLE_SCHEMA)
|
await db.put(USERS_TABLE_SCHEMA)
|
||||||
|
|
||||||
if (includeSampleData) {
|
|
||||||
// create ootb stock db
|
|
||||||
await addDefaultTables(db)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { _id: appId }
|
return { _id: appId }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addDefaultTables(db: Database) {
|
export const addSampleData = async (ctx: UserCtx) => {
|
||||||
|
const db = context.getAppDB()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if default datasource exists before creating it
|
||||||
|
await sdk.datasources.get(DEFAULT_BB_DATASOURCE_ID)
|
||||||
|
} catch (err: any) {
|
||||||
const defaultDbDocs = buildDefaultDocs()
|
const defaultDbDocs = buildDefaultDocs()
|
||||||
|
|
||||||
// add in the default db data docs - tables, datasource, rows and links
|
// add in the default db data docs - tables, datasource, rows and links
|
||||||
await db.bulkDocs([...defaultDbDocs])
|
await db.bulkDocs([...defaultDbDocs])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.status = 200
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetch(ctx: UserCtx) {
|
export async function fetch(ctx: UserCtx) {
|
||||||
const dev = ctx.query && ctx.query.status === AppStatus.DEV
|
const dev = ctx.query && ctx.query.status === AppStatus.DEV
|
||||||
const all = ctx.query && ctx.query.status === AppStatus.ALL
|
const all = ctx.query && ctx.query.status === AppStatus.ALL
|
||||||
|
@ -248,16 +251,11 @@ async function performAppCreate(ctx: UserCtx) {
|
||||||
if (ctx.request.files && ctx.request.files.templateFile) {
|
if (ctx.request.files && ctx.request.files.templateFile) {
|
||||||
instanceConfig.file = ctx.request.files.templateFile
|
instanceConfig.file = ctx.request.files.templateFile
|
||||||
}
|
}
|
||||||
const includeSampleData = isQsTrue(ctx.request.body.sampleData)
|
|
||||||
const tenantId = tenancy.isMultiTenant() ? tenancy.getTenantId() : null
|
const tenantId = tenancy.isMultiTenant() ? tenancy.getTenantId() : null
|
||||||
const appId = generateDevAppID(generateAppID(tenantId))
|
const appId = generateDevAppID(generateAppID(tenantId))
|
||||||
|
|
||||||
return await context.doInAppContext(appId, async () => {
|
return await context.doInAppContext(appId, async () => {
|
||||||
const instance = await createInstance(
|
const instance = await createInstance(appId, instanceConfig)
|
||||||
appId,
|
|
||||||
instanceConfig,
|
|
||||||
includeSampleData
|
|
||||||
)
|
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
|
|
||||||
let newApplication: App = {
|
let newApplication: App = {
|
||||||
|
|
|
@ -38,6 +38,11 @@ router
|
||||||
authorized(permissions.BUILDER),
|
authorized(permissions.BUILDER),
|
||||||
controller.revertClient
|
controller.revertClient
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"/api/applications/:appId/sample",
|
||||||
|
authorized(permissions.BUILDER),
|
||||||
|
controller.addSampleData
|
||||||
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/applications/:appId/publish",
|
"/api/applications/:appId/publish",
|
||||||
authorized(permissions.BUILDER),
|
authorized(permissions.BUILDER),
|
||||||
|
|
Loading…
Reference in New Issue