Post Sign Up Onboarding Changes (#10701)

* wip

* PR Feedback

* Fixes

* PR Feedback

* PR Feedback

* PR Feedback
This commit is contained in:
Gerard Burns 2023-05-30 12:54:57 +01:00 committed by GitHub
parent 4e16c34366
commit 23ee9f4af8
22 changed files with 414 additions and 712 deletions

View File

@ -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>

View File

@ -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}

View File

@ -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"
> >

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -71,6 +71,9 @@
tourStep.onComplete() tourStep.onComplete()
} }
popover.hide() popover.hide()
if (tourStep.endRoute) {
$goto(tourStep.endRoute)
}
} }
} }

View File

@ -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)

View File

@ -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>

View File

@ -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>

View File

@ -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">
<Panel title="Sources" borderRight> {#if !$isActive("./new")}
<Layout paddingX="L" paddingY="XL" gap="S"> <Panel title="Sources" borderRight>
<Button cta on:click={modal.show}>Add source</Button> <Layout paddingX="L" paddingY="XL" gap="S">
<CreateDatasourceModal bind:modal /> <Button cta on:click={() => $goto("./new")}>Add source</Button>
<DatasourceNavigator /> <DatasourceNavigator />
</Layout> </Layout>
</Panel> </Panel>
{/if}
<div class="content"> <div class="content">
<slot /> <slot />

View File

@ -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 />

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;
} }

View File

@ -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>

View File

@ -1,102 +1,50 @@
<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) { const createdApp = await API.createApp(data)
data.append("sampleData", true)
}
const createdApp = await API.createApp(data) // Select Correct Application/DB in prep for creating user
const pkg = await API.fetchAppPackage(createdApp.instance._id)
await store.actions.initialise(pkg)
await automationStore.actions.fetch()
// Update checklist - in case first app
await admin.init()
// Select Correct Application/DB in prep for creating user // Create user
const pkg = await API.fetchAppPackage(createdApp.instance._id) await auth.setInitInfo({})
await store.actions.initialise(pkg)
await automationStore.actions.fetch()
// Update checklist - in case first app
await admin.init()
// Create user let defaultScreenTemplate = createFromScratchScreen.create()
await auth.setInitInfo({}) defaultScreenTemplate.routing.route = "/home"
defaultScreenTemplate.routing.roldId = Roles.BASIC
await store.actions.screens.save(defaultScreenTemplate)
let defaultScreenTemplate = createFromScratchScreen.create() appId = createdApp.instance._id
defaultScreenTemplate.routing.route = "/home" return createdApp
defaultScreenTemplate.routing.roldId = Roles.BASIC
await store.actions.screens.save(defaultScreenTemplate)
appId = createdApp.instance._id
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) goToApp()
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()
}
} 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>

View File

@ -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
} }

View File

@ -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`,
})
},
}) })

View File

@ -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,21 +141,25 @@ 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 defaultDbDocs = buildDefaultDocs() const db = context.getAppDB()
// add in the default db data docs - tables, datasource, rows and links try {
await db.bulkDocs([...defaultDbDocs]) // Check if default datasource exists before creating it
await sdk.datasources.get(DEFAULT_BB_DATASOURCE_ID)
} catch (err: any) {
const defaultDbDocs = buildDefaultDocs()
// add in the default db data docs - tables, datasource, rows and links
await db.bulkDocs([...defaultDbDocs])
}
ctx.status = 200
} }
export async function fetch(ctx: UserCtx) { export async function fetch(ctx: UserCtx) {
@ -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 = {

View File

@ -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),