1
0
Fork 0
mirror of synced 2024-07-28 17:46:09 +12:00

Builder data section routing refactor (#8996)

* Improve theming with spectrum badges and dedupe spectrum label usage

* Update data section nav to match designs and use panel component

* Fix main content layout in data section

* Update data section routing for tables

* Improve data section routing for tables to account for edge cases

* Update internal and sample datasource routing

* Update external datasource routing

* Update routing for queries and make a top level concept like everything else

* Update routing for views

* Fix undefined reference when deleting datasource

* Reduce network calls and fix issues with stale datasourcenavigator state

* Update routing for REST queries and unify routes for normal queries and REST queries

* Lint

* Fix links for queries from datasource details page

* Remove redundant API calls and improve table deletion logic

* Improve data entity deletion logic and redirection and fix query details keying

* Improve determination of selected item in datasource tree

* Lint

* Fix BBUI import

* Fix datasource navigator selected state not working for internal DB or sample data
This commit is contained in:
Andrew Kingston 2022-12-17 14:13:06 +00:00 committed by GitHub
parent cf0bc606e4
commit 3b1819952d
49 changed files with 894 additions and 846 deletions

View file

@ -10,10 +10,13 @@
export let green = false
export let active = false
export let inactive = false
export let hoverable = false
</script>
<span
on:click
class="spectrum-Label"
class:hoverable
class:spectrum-Label--small={size === "S"}
class:spectrum-Label--large={size === "L"}
class:spectrum-Label--grey={grey}
@ -27,3 +30,13 @@
>
<slot />
</span>
<style>
.spectrum-Label--grey {
background-color: var(--spectrum-global-color-gray-500);
font-weight: 600;
}
.hoverable:hover {
cursor: pointer;
}
</style>

View file

@ -1,6 +1,7 @@
<script>
import "@spectrum-css/label/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
import Badge from "../Badge/Badge.svelte"
export let row
export let value
@ -24,17 +25,11 @@
{#each relationships as relationship}
{#if relationship?.primaryDisplay}
<span class="spectrum-Label spectrum-Label--grey" on:click={onClick}>
<Badge hoverable grey on:click={onClick}>
{relationship.primaryDisplay}
</span>
</Badge>
{/if}
{/each}
{#if leftover}
<div>+{leftover} more</div>
{/if}
<style>
span:hover {
cursor: pointer;
}
</style>

View file

@ -216,7 +216,6 @@
flex-direction: row;
justify-content: flex-start;
align-items: center;
margin-top: var(--spacing-m);
}
.table-title > div {
margin-left: var(--spacing-xs);

View file

@ -9,13 +9,13 @@
$: views = $tables.list.flatMap(table => Object.keys(table.views || {}))
function saveView() {
const saveView = async () => {
if (views.includes(name)) {
notifications.error(`View exists with name ${name}`)
return
}
try {
viewsStore.save({
await viewsStore.save({
name,
tableId: $tables.selected._id,
field,

View file

@ -1,7 +1,5 @@
<script>
import { onMount } from "svelte"
import { get } from "svelte/store"
import { goto, params } from "@roxi/routify"
import { goto, isActive, params } from "@roxi/routify"
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
import { database, datasources, queries, tables, views } from "stores/backend"
import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte"
@ -14,40 +12,61 @@
customQueryText,
} from "helpers/data/utils"
import IntegrationIcon from "./IntegrationIcon.svelte"
import { notifications } from "@budibase/bbui"
let openDataSources = []
$: enrichedDataSources = Array.isArray($datasources.list)
? $datasources.list.map(datasource => {
const selected = $datasources.selected === datasource._id
const open = openDataSources.includes(datasource._id)
const containsSelected = containsActiveEntity(datasource)
const onlySource = $datasources.list.length === 1
return {
...datasource,
selected,
open: selected || open || containsSelected || onlySource,
}
})
: []
$: enrichedDataSources = enrichDatasources(
$datasources,
$params,
$isActive,
$tables,
$queries,
$views
)
$: openDataSource = enrichedDataSources.find(x => x.open)
$: {
// Ensure the open datasource is always included in the list of open
// datasources
// Ensure the open datasource is always actually open
if (openDataSource) {
openNode(openDataSource)
}
}
function selectDatasource(datasource) {
openNode(datasource)
datasources.select(datasource._id)
$goto(`./datasource/${datasource._id}`)
const enrichDatasources = (
datasources,
params,
isActive,
tables,
queries,
views
) => {
if (!datasources?.list?.length) {
return []
}
return datasources.list.map(datasource => {
const selected =
isActive("./datasource") &&
datasources.selectedDatasourceId === datasource._id
const open = openDataSources.includes(datasource._id)
const containsSelected = containsActiveEntity(
datasource,
params,
isActive,
tables,
queries,
views
)
const onlySource = datasources.list.length === 1
return {
...datasource,
selected,
containsSelected,
open: selected || open || containsSelected || onlySource,
}
})
}
function onClickQuery(query) {
queries.select(query)
$goto(`./datasource/${query.datasourceId}/${query._id}`)
function selectDatasource(datasource) {
openNode(datasource)
$goto(`./datasource/${datasource._id}`)
}
function closeNode(datasource) {
@ -69,21 +88,39 @@
}
}
onMount(async () => {
try {
await datasources.fetch()
await queries.fetch()
} catch (error) {
notifications.error("Error fetching datasources and queries")
}
})
const containsActiveEntity = datasource => {
// If we're view a query then the datasource ID is in the URL
if ($params.selectedDatasource === datasource._id) {
const containsActiveEntity = (
datasource,
params,
isActive,
tables,
queries,
views
) => {
// Check for being on a datasource page
if (params.datasourceId === datasource._id) {
return true
}
// Check for hardcoded datasource edge cases
if (
isActive("./datasource/bb_internal") &&
datasource._id === "bb_internal"
) {
return true
}
if (
isActive("./datasource/datasource_internal_bb_default") &&
datasource._id === "datasource_internal_bb_default"
) {
return true
}
// Check for a matching query
if (params.queryId) {
const query = queries.list?.find(q => q._id === params.queryId)
return datasource._id === query?.datasourceId
}
// If there are no entities it can't contain anything
if (!datasource.entities) {
return false
@ -96,13 +133,13 @@
}
// Check for a matching table
if ($params.selectedTable) {
const selectedTable = get(tables).selected?._id
if (params.tableId) {
const selectedTable = tables.selected?._id
return options.find(x => x._id === selectedTable) != null
}
// Check for a matching view
const selectedView = get(views).selected?.name
const selectedView = views.selected?.name
const table = options.find(table => {
return table.views?.[selectedView] != null
})
@ -117,7 +154,7 @@
border={idx > 0}
text={datasource.name}
opened={datasource.open}
selected={datasource.selected}
selected={$isActive("./datasource") && datasource.selected}
withArrow={true}
on:click={() => selectDatasource(datasource)}
on:iconClick={() => toggleNode(datasource)}
@ -143,11 +180,11 @@
iconText={customQueryIconText(datasource, query)}
iconColor={customQueryIconColor(datasource, query)}
text={customQueryText(datasource, query)}
opened={$queries.selected === query._id}
selected={$queries.selected === query._id}
on:click={() => onClickQuery(query)}
selected={$isActive("./query/:queryId") &&
$queries.selectedQueryId === query._id}
on:click={() => $goto(`./query/${query._id}`)}
>
<EditQueryPopover {query} {onClickQuery} />
<EditQueryPopover {query} />
</NavItem>
{/each}
{/if}
@ -156,6 +193,9 @@
{/if}
<style>
.hierarchy-items-container {
margin: 0 calc(-1 * var(--spacing-xl));
}
.datasource-icon {
display: grid;
place-items: center;

View file

@ -1,6 +1,6 @@
<script>
import { goto } from "@roxi/routify"
import { datasources, queries, tables } from "stores/backend"
import { datasources } from "stores/backend"
import { notifications } from "@budibase/bbui"
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -14,23 +14,10 @@
async function deleteDatasource() {
try {
let wasSelectedSource = $datasources.selected
if (!wasSelectedSource && $queries.selected) {
const queryId = $queries.selected
wasSelectedSource = $datasources.list.find(ds =>
queryId.includes(ds._id)
)?._id
}
const wasSelectedTable = $tables.selected
const isSelected = datasource.selected || datasource.containsSelected
await datasources.delete(datasource)
notifications.success("Datasource deleted")
// Navigate to first index page if the source you are deleting is selected
const entities = Object.values(datasource?.entities || {})
if (
wasSelectedSource === datasource._id ||
(entities &&
entities.find(entity => entity._id === wasSelectedTable?._id))
) {
if (isSelected) {
$goto("./datasource")
}
} catch (error) {

View file

@ -5,23 +5,17 @@
import { datasources, queries } from "stores/backend"
export let query
export let onClickQuery
let confirmDeleteDialog
async function deleteQuery() {
try {
const wasSelectedQuery = $queries.selected
// need to calculate this before the query is deleted
const navigateToDatasource = wasSelectedQuery === query._id
await queries.delete(query)
await datasources.fetch()
if (navigateToDatasource) {
await datasources.select(query.datasourceId)
// Go back to the datasource if we are deleting the active query
if ($queries.selectedQueryId === query._id) {
$goto(`./datasource/${query.datasourceId}`)
}
await queries.delete(query)
await datasources.fetch()
notifications.success("Query deleted")
} catch (error) {
notifications.error("Error deleting query")
@ -31,7 +25,7 @@
async function duplicateQuery() {
try {
const newQuery = await queries.duplicate(query)
onClickQuery(newQuery)
$goto(`./query/${newQuery._id}`)
} catch (error) {
notifications.error("Error duplicating query")
}

View file

@ -1,39 +1,18 @@
<script>
import { goto } from "@roxi/routify"
import { tables, views, database } from "stores/backend"
import { TableNames } from "constants"
import EditTablePopover from "./popovers/EditTablePopover.svelte"
import EditViewPopover from "./popovers/EditViewPopover.svelte"
import NavItem from "components/common/NavItem.svelte"
import { goto, isActive } from "@roxi/routify"
const alphabetical = (a, b) => a.name?.toLowerCase() > b.name?.toLowerCase()
export let sourceId
$: selectedView = $views.selected && $views.selected.name
$: sortedTables = $tables.list
.filter(table => table.sourceId === sourceId)
.sort(alphabetical)
function selectTable(table) {
tables.select(table)
$goto(`./table/${table._id}`)
}
function selectView(view) {
views.select(view)
$goto(`./view/${view.name}`)
}
function onClickView(table, viewName) {
if (selectedView === viewName) {
return
}
selectView({
name: viewName,
...table.views[viewName],
})
}
</script>
{#if $database?._id}
@ -44,8 +23,9 @@
border={idx > 0}
icon={table._id === TableNames.USERS ? "UserGroup" : "Table"}
text={table.name}
selected={$tables.selected?._id === table._id}
on:click={() => selectTable(table)}
selected={$isActive("./table/:tableId") &&
$tables.selected?._id === table._id}
on:click={() => $goto(`./table/${table._id}`)}
>
{#if table._id !== TableNames.USERS}
<EditTablePopover {table} />
@ -56,8 +36,9 @@
indentLevel={2}
icon="Remove"
text={viewName}
selected={selectedView === viewName}
on:click={() => onClickView(table, viewName)}
selected={$isActive("./view/:viewName") &&
$views.selected?.name === viewName}
on:click={() => $goto(`./view/${viewName}`)}
>
<EditViewPopover
view={{ name: viewName, ...table.views[viewName] }}

View file

@ -1,5 +1,5 @@
<script>
import { goto } from "@roxi/routify"
import { goto, params } from "@roxi/routify"
import { store } from "builderStore"
import { cloneDeep } from "lodash/fp"
import { tables, datasources } from "stores/backend"
@ -41,17 +41,16 @@
}
async function deleteTable() {
const wasSelectedTable = $tables.selected
const isSelected = $params.tableId === table._id
try {
await tables.delete(table)
await store.actions.screens.delete(templateScreens)
await tables.fetch()
if (table.type === "external") {
await datasources.fetch()
}
notifications.success("Table deleted")
if (wasSelectedTable && wasSelectedTable._id === table._id) {
$goto("./table")
if (isSelected) {
$goto(`./datasource/${table.datasourceId}`)
}
} catch (error) {
notifications.error("Error deleting table")

View file

@ -1,5 +1,5 @@
<script>
import { goto } from "@roxi/routify"
import { goto, params } from "@roxi/routify"
import { views } from "stores/backend"
import { cloneDeep } from "lodash/fp"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -33,11 +33,14 @@
async function deleteView() {
try {
const isSelected = $params.viewName === $views.selectedViewName
const name = view.name
const id = view.tableId
await views.delete(name)
notifications.success("View deleted")
$goto(`./table/${id}`)
if (isSelected) {
$goto(`./table/${id}`)
}
} catch (error) {
notifications.error("Error deleting view")
}

View file

@ -51,6 +51,7 @@
<style>
.panel {
width: 260px;
flex: 0 0 260px;
background: var(--background);
display: flex;
flex-direction: column;
@ -66,6 +67,7 @@
}
.panel.wide {
width: 420px;
flex: 0 0 420px;
}
.header {
flex: 0 0 48px;

View file

@ -29,11 +29,12 @@
export let query
const transformerDocs = "https://docs.budibase.com/docs/transformers"
let fields = query?.schema ? schemaToFields(query.schema) : []
let parameters
let data = []
let saveId
const transformerDocs = "https://docs.budibase.com/docs/transformers"
$: datasource = $datasources.list.find(ds => ds._id === query.datasourceId)
$: query.schema = fieldsToSchema(fields)
@ -94,132 +95,144 @@
try {
const { _id } = await queries.save(query.datasourceId, query)
saveId = _id
notifications.success(`Query saved successfully.`)
$goto(`../${_id}`)
notifications.success(`Query saved successfully`)
// Go to the correct URL if we just created a new query
if (!query._rev) {
$goto(`../../${_id}`)
}
} catch (error) {
notifications.error("Error creating query")
notifications.error("Error saving query")
}
}
</script>
<Layout gap="S" noPadding>
<Heading size="M">Query {integrationInfo?.friendlyName}</Heading>
<Divider />
<Heading size="S">Config</Heading>
<div class="config">
<div class="config-field">
<Label>Query Name</Label>
<Input bind:value={query.name} />
</div>
{#if queryConfig}
<div class="config-field">
<Label>Function</Label>
<Select
bind:value={query.queryVerb}
on:change={resetDependentFields}
options={Object.keys(queryConfig)}
getOptionLabel={verb =>
queryConfig[verb]?.displayName || capitalise(verb)}
/>
</div>
<div class="config-field">
<AccessLevelSelect {saveId} {query} label="Access Level" />
</div>
{#if integrationInfo?.extra && query.queryVerb}
<ExtraQueryConfig
{query}
{populateExtraQuery}
config={integrationInfo.extra}
/>
{/if}
{#key query.parameters}
<BindingBuilder
queryBindings={query.parameters}
bindable={false}
on:change={e => {
query.parameters = e.detail.map(binding => {
return {
name: binding.name,
default: binding.value,
}
})
}}
/>
{/key}
{/if}
</div>
{#if shouldShowQueryConfig}
<div class="wrapper">
<Layout gap="S" noPadding>
<Heading size="M">Query {integrationInfo?.friendlyName}</Heading>
<Divider />
<Heading size="S">Config</Heading>
<div class="config">
<Heading size="S">Fields</Heading>
<Body size="S">Fill in the fields specific to this query.</Body>
<IntegrationQueryEditor
{datasource}
{query}
height={200}
schema={queryConfig[query.queryVerb]}
bind:parameters
/>
<Divider />
</div>
<div class="config">
<div class="help-heading">
<Heading size="S">Transformer</Heading>
<Icon
on:click={() => window.open(transformerDocs)}
hoverable
name="Help"
size="L"
/>
<div class="config-field">
<Label>Query Name</Label>
<Input bind:value={query.name} />
</div>
<Body size="S"
>Add a JavaScript function to transform the query result.</Body
>
<CodeMirrorEditor
height={200}
label="Transformer"
value={query.transformer}
resize="vertical"
on:change={e => (query.transformer = e.detail)}
/>
<Divider />
</div>
<div class="viewer-controls">
<Heading size="S">Results</Heading>
<ButtonGroup gap="M">
<Button cta disabled={queryInvalid} on:click={saveQuery}>
Save Query
</Button>
<Button secondary on:click={previewQuery}>Run Query</Button>
</ButtonGroup>
</div>
<Body size="S">
Below, you can preview the results from your query and change the schema.
</Body>
<section class="viewer">
{#if data}
<Tabs selected="JSON">
<Tab title="JSON">
<JSONPreview data={data[0]} minHeight="120" />
</Tab>
<Tab title="Schema">
<KeyValueBuilder
bind:object={fields}
name="field"
headings
options={SchemaTypeOptions}
/>
</Tab>
<Tab title="Preview">
<ExternalDataSourceTable {query} {data} />
</Tab>
</Tabs>
{#if queryConfig}
<div class="config-field">
<Label>Function</Label>
<Select
bind:value={query.queryVerb}
on:change={resetDependentFields}
options={Object.keys(queryConfig)}
getOptionLabel={verb =>
queryConfig[verb]?.displayName || capitalise(verb)}
/>
</div>
<div class="config-field">
<AccessLevelSelect {saveId} {query} label="Access Level" />
</div>
{#if integrationInfo?.extra && query.queryVerb}
<ExtraQueryConfig
{query}
{populateExtraQuery}
config={integrationInfo.extra}
/>
{/if}
{#key query.parameters}
<BindingBuilder
queryBindings={query.parameters}
bindable={false}
on:change={e => {
query.parameters = e.detail.map(binding => {
return {
name: binding.name,
default: binding.value,
}
})
}}
/>
{/key}
{/if}
</section>
{/if}
</Layout>
</div>
{#if shouldShowQueryConfig}
<Divider />
<div class="config">
<Heading size="S">Fields</Heading>
<Body size="S">Fill in the fields specific to this query.</Body>
<IntegrationQueryEditor
{datasource}
{query}
height={200}
schema={queryConfig[query.queryVerb]}
bind:parameters
/>
<Divider />
</div>
<div class="config">
<div class="help-heading">
<Heading size="S">Transformer</Heading>
<Icon
on:click={() => window.open(transformerDocs)}
hoverable
name="Help"
size="L"
/>
</div>
<Body size="S"
>Add a JavaScript function to transform the query result.</Body
>
<CodeMirrorEditor
height={200}
label="Transformer"
value={query.transformer}
resize="vertical"
on:change={e => (query.transformer = e.detail)}
/>
<Divider />
</div>
<div class="viewer-controls">
<Heading size="S">Results</Heading>
<ButtonGroup gap="XS">
<Button cta disabled={queryInvalid} on:click={saveQuery}>
Save Query
</Button>
<Button secondary on:click={previewQuery}>Run Query</Button>
</ButtonGroup>
</div>
<Body size="S">
Below, you can preview the results from your query and change the
schema.
</Body>
<section class="viewer">
{#if data}
<Tabs selected="JSON">
<Tab title="JSON">
<JSONPreview data={data[0]} minHeight="120" />
</Tab>
<Tab title="Schema">
<KeyValueBuilder
bind:object={fields}
name="field"
headings
options={SchemaTypeOptions}
/>
</Tab>
<Tab title="Preview">
<ExternalDataSourceTable {query} {data} />
</Tab>
</Tabs>
{/if}
</section>
{/if}
</Layout>
</div>
<style>
.wrapper {
width: 640px;
margin: auto;
}
.config {
display: grid;
grid-gap: var(--spacing-s);

View file

@ -1,5 +1,5 @@
<script>
import { params } from "@roxi/routify"
import { goto, params } from "@roxi/routify"
import { datasources, flags, integrations, queries } from "stores/backend"
import {
Banner,
@ -23,7 +23,7 @@
import CodeMirrorEditor, {
EditorModes,
} from "components/common/CodeMirrorEditor.svelte"
import RestBodyInput from "../../_components/RestBodyInput.svelte"
import RestBodyInput from "./RestBodyInput.svelte"
import { capitalise } from "helpers"
import { onMount } from "svelte"
import restUtils from "helpers/data/utils"
@ -36,7 +36,7 @@
} from "constants/backend"
import JSONPreview from "components/integration/JSONPreview.svelte"
import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte"
import DynamicVariableModal from "../../_components/DynamicVariableModal.svelte"
import DynamicVariableModal from "./DynamicVariableModal.svelte"
import Placeholder from "assets/bb-spaceship.svg"
import { cloneDeep } from "lodash/fp"
@ -49,6 +49,8 @@
toBindingsArray,
} from "builderStore/dataBinding"
export let queryId
let query, datasource
let breakQs = {},
requestBindings = {}
@ -102,8 +104,8 @@
function getSelectedQuery() {
return cloneDeep(
$queries.list.find(q => q._id === $queries.selected) || {
datasourceId: $params.selectedDatasource,
$queries.list.find(q => q._id === queryId) || {
datasourceId: $params.datasourceId,
parameters: [],
fields: {
// only init the objects, everything else is optional strings
@ -159,6 +161,7 @@
async function saveQuery() {
const toSave = buildQuery()
try {
const isNew = !query._rev
const { _id } = await queries.save(toSave.datasourceId, toSave)
saveId = _id
query = getSelectedQuery()
@ -174,6 +177,9 @@
staticVariables,
restBindings
)
if (isNew) {
$goto(`../../${_id}`)
}
} catch (err) {
notifications.error(`Error saving query`)
}
@ -464,8 +470,9 @@
on:click={saveQuery}
tooltip={!hasSchema
? "Saving a query before sending will mean no schema is generated"
: null}>Save</Button
>
: null}
>Save
</Button>
</div>
<Tabs selected="Bindings" quiet noPadding noHorizPadding onTop>
<Tab title="Bindings">
@ -708,26 +715,33 @@
margin: 0 auto;
height: 100%;
}
.table {
width: 960px;
}
.url-block {
display: flex;
gap: var(--spacing-s);
z-index: 200;
}
.verb {
flex: 1;
}
.url {
flex: 4;
}
.top {
min-height: 50%;
}
.bottom {
padding-bottom: 50px;
}
.stats {
display: flex;
gap: var(--spacing-xl);
@ -735,40 +749,49 @@
margin-right: 0;
align-items: center;
}
.green {
color: #53a761;
}
.red {
color: #ea7d82;
}
.top-bar {
display: flex;
justify-content: space-between;
}
.access {
display: flex;
gap: var(--spacing-m);
align-items: center;
}
.placeholder-internal {
display: flex;
flex-direction: column;
width: 200px;
gap: var(--spacing-l);
}
.placeholder {
display: flex;
margin-top: var(--spacing-xl);
justify-content: center;
}
.auth-container {
width: 100%;
display: flex;
justify-content: space-between;
}
.auth-select {
width: 200px;
}
.pagination {
display: grid;
grid-template-columns: 1fr 1fr;

View file

@ -6,6 +6,7 @@ export const syncURLToState = options => {
urlParam,
stateKey,
validate,
update,
baseUrl = "..",
fallbackUrl,
store,
@ -37,8 +38,8 @@ export const syncURLToState = options => {
let cachedRedirect = get(routify.redirect)
let cachedPage = get(routify.page)
let previousParamsHash = null
let debug = false
const log = (...params) => debug && console.log(...params)
let debug = true
const log = (...params) => debug && console.log(`[${urlParam}]`, ...params)
// Navigate to a certain URL
const gotoUrl = (url, params) => {
@ -85,10 +86,16 @@ export const syncURLToState = options => {
// Only update state if we have a new value
if (urlValue !== stateValue) {
log(`state.${stateKey} (${stateValue}) <= url.${urlParam} (${urlValue})`)
store.update(state => {
state[stateKey] = urlValue
return state
})
if (update) {
// Use custom update function if provided
update(urlValue)
} else {
// Otherwise manually update the store
store.update(state => ({
...state,
[stateKey]: urlValue,
}))
}
}
}

View file

@ -1,75 +1,46 @@
<script>
import { redirect } from "@roxi/routify"
import { Button, Tabs, Tab, Layout } from "@budibase/bbui"
import { Button, Layout } from "@budibase/bbui"
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
let selected = "Sources"
import Panel from "components/design/Panel.svelte"
let modal
function selectFirstDatasource() {
$redirect("./table")
}
</script>
<!-- routify:options index=1 -->
<div class="root">
<div class="nav">
<Tabs {selected} on:select={selectFirstDatasource}>
<Tab title="Sources">
<Layout paddingX="L" paddingY="L" gap="S">
<Button dataCy={`new-datasource`} cta wide on:click={modal.show}
>Add source</Button
>
</Layout>
<CreateDatasourceModal bind:modal />
<DatasourceNavigator />
</Tab>
</Tabs>
</div>
<div class="data">
<Panel title="Sources" borderRight>
<Layout paddingX="L" paddingY="XL" gap="S">
<Button dataCy={`new-datasource`} cta on:click={modal.show}>
Add source
</Button>
<CreateDatasourceModal bind:modal />
<DatasourceNavigator />
</Layout>
</Panel>
<div class="content">
<slot />
</div>
</div>
<style>
.root {
.data {
flex: 1 1 auto;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: stretch;
height: 0;
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
}
.content {
flex: 1 1 auto;
padding: var(--spacing-l) 40px 40px 40px;
padding: 28px 40px 40px 40px;
overflow-y: auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-l);
}
.content :global(> span) {
display: contents;
}
.nav {
overflow-y: auto;
background: var(--background);
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
position: relative;
border-right: var(--border-light);
padding-bottom: 60px;
}
.add-button {
position: absolute;
top: var(--spacing-l);
right: var(--spacing-xl);
flex: 1 1 auto;
}
</style>

View file

@ -0,0 +1,23 @@
<script>
import { params } from "@roxi/routify"
import { datasources } from "stores/backend"
import { syncURLToState } from "helpers/urlStateSync"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
const stopSyncing = syncURLToState({
urlParam: "datasourceId",
stateKey: "selectedDatasourceId",
validate: id => $datasources.list?.some(ds => ds._id === id),
update: datasources.select,
fallbackUrl: "../",
store: datasources,
routify,
})
onDestroy(stopSyncing)
</script>
{#key $params.datasourceId}
<slot />
{/key}

View file

@ -21,34 +21,31 @@
import { cloneDeep } from "lodash/fp"
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
let importQueriesModal
let changed,
isValid = true
let integration, baseDatasource, datasource
let queryList
const querySchema = {
name: {},
queryVerb: { displayName: "Method" },
}
$: baseDatasource = $datasources.list.find(
ds => ds._id === $datasources.selected
)
let importQueriesModal
let changed = false
let isValid = true
let integration, baseDatasource, datasource
let queryList
$: baseDatasource = $datasources.selected
$: queryList = $queries.list.filter(
query => query.datasourceId === datasource?._id
)
$: hasChanged(baseDatasource, datasource)
$: updateDatasource(baseDatasource)
function hasChanged(base, ds) {
const hasChanged = (base, ds) => {
if (base && ds) {
changed = !isEqual(base, ds)
}
}
async function saveDatasource() {
const saveDatasource = async () => {
try {
// Create datasource
await datasources.save(datasource)
@ -63,12 +60,7 @@
}
}
function onClickQuery(query) {
queries.select(query)
$goto(`./${query._id}`)
}
function updateDatasource(base) {
const updateDatasource = base => {
if (base) {
datasource = cloneDeep(base)
integration = $integrations[datasource.source]
@ -87,7 +79,7 @@
{#if datasource && integration}
<section>
<Layout>
<Layout noPadding>
<Layout gap="XS" noPadding>
<header>
<svelte:component
@ -95,16 +87,16 @@
height="26"
width="26"
/>
<Heading size="M">{datasource.name}</Heading>
<Heading size="M">{$datasources.selected?.name}</Heading>
</header>
<Body size="M">{integration.description}</Body>
</Layout>
<Divider />
<div class="config-header">
<Heading size="S">Configuration</Heading>
<Button disabled={!changed || !isValid} cta on:click={saveDatasource}
>Save</Button
>
<Button disabled={!changed || !isValid} cta on:click={saveDatasource}>
Save
</Button>
</div>
<IntegrationConfigForm
on:change={hasChanged}
@ -120,12 +112,16 @@
<Heading size="S">Queries</Heading>
<div class="query-buttons">
{#if datasource?.source === IntegrationTypes.REST}
<Button secondary on:click={() => importQueriesModal.show()}
>Import</Button
>
<Button secondary on:click={() => importQueriesModal.show()}>
Import
</Button>
{/if}
<Button cta icon="Add" on:click={() => $goto("./new")}
>Add query
<Button
cta
icon="Add"
on:click={() => $goto(`../../query/new/${datasource._id}`)}
>
Add query
</Button>
</div>
</div>
@ -137,7 +133,7 @@
{#if queryList && queryList.length > 0}
<div class="query-list">
<Table
on:click={({ detail }) => onClickQuery(detail)}
on:click={({ detail }) => $goto(`../../query/${detail._id}`)}
schema={querySchema}
data={queryList}
allowEditColumns={false}

View file

@ -1,23 +0,0 @@
<script>
import { params } from "@roxi/routify"
import { queries, datasources } from "stores/backend"
import { IntegrationTypes } from "constants/backend"
import { redirect } from "@roxi/routify"
let datasourceId
if ($params.query) {
const query = $queries.list.find(q => q._id === $params.query)
if (query) {
queries.select(query)
datasourceId = query.datasourceId
}
}
const datasource = $datasources.list.find(
ds => ds._id === $datasources.selected || ds._id === datasourceId
)
if (datasource?.source === IntegrationTypes.REST) {
$redirect(`../rest/${$params.query}`)
}
</script>
<slot />

View file

@ -1,39 +0,0 @@
<script>
import { params, redirect } from "@roxi/routify"
import { database, datasources, queries } from "stores/backend"
import QueryInterface from "components/integration/QueryViewer.svelte"
import { IntegrationTypes } from "constants/backend"
let selectedQuery, datasource
$: selectedQuery = $queries.list.find(
query => query._id === $queries.selected
) || {
datasourceId: $params.selectedDatasource,
parameters: [],
fields: {},
queryVerb: "read",
}
$: datasource = $datasources.list.find(
ds => ds._id === $params.selectedDatasource
)
$: {
if (datasource?.source === IntegrationTypes.REST) {
$redirect(`../rest/${$params.query}`)
}
}
</script>
<section>
<div class="inner">
{#if $database._id && selectedQuery}
<QueryInterface query={selectedQuery} />
{/if}
</div>
</section>
<style>
.inner {
width: 640px;
margin: 0 auto;
}
</style>

View file

@ -1,17 +0,0 @@
<script>
import { params } from "@roxi/routify"
import { datasources } from "stores/backend"
if ($params.selectedDatasource && !$params.query) {
const datasource = $datasources.list.find(
m => m._id === $params.selectedDatasource
)
if (datasource) {
datasources.select(datasource._id)
}
}
</script>
{#key $params.selectedDatasource}
<slot />
{/key}

View file

@ -1,13 +0,0 @@
<script>
import { params } from "@roxi/routify"
import { queries } from "stores/backend"
if ($params.query) {
const query = $queries.list.find(q => q._id === $params.query)
if (query) {
queries.select(query)
}
}
</script>
<slot />

View file

@ -1,7 +0,0 @@
<script>
import { datasources } from "stores/backend"
datasources.select("bb_internal")
</script>
<slot />

View file

@ -4,13 +4,19 @@
import ICONS from "components/backend/DatasourceNavigator/icons"
import { tables, datasources } from "stores/backend"
import { goto } from "@roxi/routify"
import { onMount } from "svelte"
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
let modal
$: internalTablesBySourceId = $tables.list.filter(
table =>
table.type !== "external" && $datasources.selected === table.sourceId
table.type !== "external" && table.sourceId === BUDIBASE_INTERNAL_DB_ID
)
onMount(() => {
datasources.select(BUDIBASE_INTERNAL_DB_ID)
})
</script>
<Modal bind:this={modal}>
@ -73,7 +79,7 @@
background: var(--background);
border: var(--border-dark);
display: grid;
grid-template-columns: 2fr 0.75fr 20px;
grid-template-columns: 1fr auto;
align-items: center;
padding: var(--spacing-m);
gap: var(--layout-xs);

View file

@ -1,8 +0,0 @@
<script>
import { datasources } from "stores/backend"
import { DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
datasources.select(DEFAULT_BB_DATASOURCE_ID)
</script>
<slot />

View file

@ -4,12 +4,18 @@
import ICONS from "components/backend/DatasourceNavigator/icons"
import { tables, datasources } from "stores/backend"
import { goto } from "@roxi/routify"
import { DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
import { onMount } from "svelte"
let modal
$: internalTablesBySourceId = $tables.list.filter(
table =>
table.type !== "external" && $datasources.selected === table.sourceId
table.type !== "external" && table.sourceId === DEFAULT_BB_DATASOURCE_ID
)
onMount(() => {
datasources.select(DEFAULT_BB_DATASOURCE_ID)
})
</script>
<Modal bind:this={modal}>
@ -23,10 +29,11 @@
<svelte:component this={ICONS.BUDIBASE} height="26" width="26" />
<Heading size="M">Sample Data</Heading>
</header>
<Body size="M">A little something to get you up and running!</Body>
<Body size="M"
>If you have no need for this datasource, feel free to delete it.</Body
>
<Body size="M">
A little something to get you up and running!
<br />
If you have no need for this datasource, feel free to delete it.
</Body>
</Layout>
<Divider />
<Heading size="S">Tables</Heading>
@ -73,7 +80,7 @@
background: var(--background);
border: var(--border-dark);
display: grid;
grid-template-columns: 2fr 0.75fr 20px;
grid-template-columns: 1fr auto;
align-items: center;
padding: var(--spacing-m);
gap: var(--layout-xs);

View file

@ -4,12 +4,16 @@
import { onMount } from "svelte"
onMount(async () => {
// navigate to first table in list, if not already selected
$datasources.list.length > 0 && $redirect(`./${$datasources.list[0]._id}`)
const { list, selected } = $datasources
if (selected) {
$redirect(`./${selected?._id}`)
} else {
$redirect(`./${list[0]._id}`)
}
})
</script>
{#if $datasources.list.length === 0}
{#if !$datasources.list?.length}
<i>Connect your first datasource to start building.</i>
{:else}<i>Select a datasource to edit</i>{/if}

View file

@ -0,0 +1,22 @@
<script>
import { queries } from "stores/backend"
import { syncURLToState } from "helpers/urlStateSync"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
const stopSyncing = syncURLToState({
urlParam: "queryId",
stateKey: "selectedQueryId",
validate: id => id === "new" || $queries.list?.some(q => q._id === id),
update: queries.select,
fallbackUrl: "../",
store: queries,
routify,
})
onDestroy(stopSyncing)
</script>
{#key $queries.selectedQueryId}
<slot />
{/key}

View file

@ -0,0 +1,18 @@
<script>
import { database, queries, datasources } from "stores/backend"
import QueryViewer from "components/integration/QueryViewer.svelte"
import RestQueryViewer from "components/integration/RestQueryViewer.svelte"
import { IntegrationTypes } from "constants/backend"
$: query = $queries.selected
$: datasource = $datasources.list.find(ds => ds._id === query?.datasourceId)
$: isRestQuery = datasource?.source === IntegrationTypes.REST
</script>
{#if $database._id && query}
{#if isRestQuery}
<RestQueryViewer queryId={$queries.selectedQueryId} />
{:else}
<QueryViewer {query} />
{/if}
{/if}

View file

@ -0,0 +1,16 @@
<script>
import { onMount } from "svelte"
import { queries } from "stores/backend"
import { redirect } from "@roxi/routify"
onMount(async () => {
const { list, selected } = $queries
if (selected) {
$redirect(`./${selected?._id}`)
} else if (list?.length) {
$redirect(`./${list[0]._id}`)
} else {
$redirect("../")
}
})
</script>

View file

@ -0,0 +1,38 @@
<script>
import { params, redirect } from "@roxi/routify"
import { database, datasources } from "stores/backend"
import QueryViewer from "components/integration/QueryViewer.svelte"
import RestQueryViewer from "components/integration/RestQueryViewer.svelte"
import { IntegrationTypes } from "constants/backend"
$: datasource = $datasources.list.find(ds => ds._id === $params.datasourceId)
$: {
if (!datasource) {
$redirect("../../../")
}
}
$: isRestQuery = datasource?.source === IntegrationTypes.REST
$: query = buildNewQuery(isRestQuery)
const buildNewQuery = isRestQuery => {
let query = {
datasourceId: $params.datasourceId,
parameters: [],
fields: {},
queryVerb: "read",
}
if (isRestQuery) {
query.flags = {}
query.fields = { disabledHeaders: {}, headers: {} }
}
return query
}
</script>
{#if $database._id && datasource && query}
{#if isRestQuery}
<RestQueryViewer />
{:else}
<QueryViewer {query} />
{/if}
{/if}

View file

@ -0,0 +1,5 @@
<script>
import { redirect } from "@roxi/routify"
$redirect("../")
</script>

View file

@ -1,13 +0,0 @@
<script>
import { params } from "@roxi/routify"
import { tables } from "stores/backend"
if ($params.selectedTable) {
const table = $tables.list.find(m => m._id === $params.selectedTable)
if (table) {
tables.select(table)
}
}
</script>
<slot />

View file

@ -0,0 +1,20 @@
<script>
import { syncURLToState } from "helpers/urlStateSync"
import { tables } from "stores/backend"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
const stopSyncing = syncURLToState({
urlParam: "tableId",
stateKey: "selectedTableId",
validate: id => $tables.list?.some(table => table._id === id),
update: tables.select,
fallbackUrl: "../",
store: tables,
routify,
})
onDestroy(stopSyncing)
</script>
<slot />

View file

@ -3,9 +3,11 @@
import { tables, database } from "stores/backend"
</script>
{#if $database?._id && $tables?.selected?.name}
{#if $database?._id && $tables?.selected}
<TableDataTable />
{:else}<i>Create your first table to start building</i>{/if}
{:else}
<i>Create your first table to start building</i>
{/if}
<style>
i {

View file

@ -4,7 +4,7 @@
</script>
<RelationshipDataTable
tableId={$params.selectedTable}
rowId={$params.selectedRow}
fieldName={decodeURI($params.selectedField)}
tableId={$params.tableId}
rowId={$params.rowId}
fieldName={decodeURI($params.field)}
/>

View file

@ -1,19 +0,0 @@
<script>
import { tables } from "stores/backend"
import { redirect, leftover } from "@roxi/routify"
import { onMount } from "svelte"
onMount(async () => {
// navigate to first table in list, if not already selected
// and this is the final url (i.e. no selectedTable)
if (
!$leftover &&
$tables.list.length > 0 &&
(!$tables.selected || !$tables.selected._id)
) {
$redirect(`./${$tables.list[0]._id}`)
}
})
</script>
<slot />

View file

@ -1,14 +1,19 @@
<script>
import { redirect } from "@roxi/routify"
import { onMount } from "svelte"
import { tables } from "stores/backend"
import { redirect } from "@roxi/routify"
onMount(async () => {
$tables.list.length > 0 && $redirect(`./${$tables.list[0]._id}`)
const { list, selected } = $tables
if (selected) {
$redirect(`./${selected?._id}`)
} else if (list?.length) {
$redirect(`./${list[0]._id}`)
}
})
</script>
{#if $tables.list.length === 0}
{#if !$tables.list?.length}
<i>Create your first table to start building</i>
{:else}<i>Select a table to edit</i>{/if}

View file

@ -1,22 +0,0 @@
<script>
import { params } from "@roxi/routify"
import { tables, views } from "stores/backend"
if ($params.selectedView) {
let view
const viewName = decodeURI($params.selectedView)
for (let table of $tables.list) {
if (table.views && table.views[viewName]) {
view = table.views[viewName]
}
}
if (view) {
views.select({
name: viewName,
...view,
})
}
}
</script>
<slot />

View file

@ -0,0 +1,20 @@
<script>
import { views } from "stores/backend"
import { syncURLToState } from "helpers/urlStateSync"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
const stopSyncing = syncURLToState({
urlParam: "viewName",
stateKey: "selectedViewName",
validate: name => $views.list?.some(view => view.name === name),
update: views.select,
fallbackUrl: "../",
store: views,
routify,
})
onDestroy(stopSyncing)
</script>
<slot />

View file

@ -0,0 +1,16 @@
<script>
import { onMount } from "svelte"
import { views } from "stores/backend"
import { redirect } from "@roxi/routify"
onMount(async () => {
const { list, selected } = $views
if (selected) {
$redirect(`./${selected?.name}`)
} else if (list?.length) {
$redirect(`./${list[0].name}`)
} else {
$redirect("../")
}
})
</script>

View file

@ -1,20 +1,36 @@
import { writable, get } from "svelte/store"
import { queries, tables, views } from "./"
import { writable, derived } from "svelte/store"
import { queries, tables } from "./"
import { API } from "api"
export const INITIAL_DATASOURCE_VALUES = {
list: [],
selected: null,
schemaError: null,
}
export function createDatasourcesStore() {
const store = writable(INITIAL_DATASOURCE_VALUES)
const { subscribe, update, set } = store
const store = writable({
list: [],
selectedDatasourceId: null,
schemaError: null,
})
const derivedStore = derived(store, $store => ({
...$store,
selected: $store.list?.find(ds => ds._id === $store.selectedDatasourceId),
}))
async function updateDatasource(response) {
const fetch = async () => {
const datasources = await API.getDatasources()
store.update(state => ({
...state,
list: datasources,
}))
}
const select = id => {
store.update(state => ({
...state,
selectedDatasourceId: id,
}))
}
const updateDatasource = async response => {
const { datasource, error } = response
update(state => {
store.update(state => {
const currentIdx = state.list.findIndex(ds => ds._id === datasource._id)
const sources = state.list
if (currentIdx >= 0) {
@ -24,82 +40,64 @@ export function createDatasourcesStore() {
}
return {
list: sources,
selected: datasource._id,
selectedDatasourceId: datasource._id,
schemaError: error,
}
})
return datasource
}
const updateSchema = async (datasource, tablesFilter) => {
const response = await API.buildDatasourceSchema({
datasourceId: datasource?._id,
tablesFilter,
})
return await updateDatasource(response)
}
const save = async (body, fetchSchema = false) => {
let response
if (body._id) {
response = await API.updateDatasource(body)
} else {
response = await API.createDatasource({
datasource: body,
fetchSchema,
})
}
return updateDatasource(response)
}
const deleteDatasource = async datasource => {
await API.deleteDatasource({
datasourceId: datasource?._id,
datasourceRev: datasource?._rev,
})
store.update(state => {
const sources = state.list.filter(
existing => existing._id !== datasource._id
)
return { list: sources, selected: null }
})
await queries.fetch()
await tables.fetch()
}
const removeSchemaError = () => {
store.update(state => {
return { ...state, schemaError: null }
})
}
return {
subscribe,
update,
init: async () => {
const datasources = await API.getDatasources()
set({
list: datasources,
selected: null,
})
},
fetch: async () => {
const datasources = await API.getDatasources()
// Clear selected if it no longer exists, otherwise keep it
const selected = get(store).selected
let nextSelected = null
if (selected && datasources.find(source => source._id === selected)) {
nextSelected = selected
}
update(state => ({ ...state, list: datasources, selected: nextSelected }))
},
select: datasourceId => {
update(state => ({ ...state, selected: datasourceId }))
queries.unselect()
tables.unselect()
views.unselect()
},
unselect: () => {
update(state => ({ ...state, selected: null }))
},
updateSchema: async (datasource, tablesFilter) => {
const response = await API.buildDatasourceSchema({
datasourceId: datasource?._id,
tablesFilter,
})
return await updateDatasource(response)
},
save: async (body, fetchSchema = false) => {
let response
if (body._id) {
response = await API.updateDatasource(body)
} else {
response = await API.createDatasource({
datasource: body,
fetchSchema,
})
}
return updateDatasource(response)
},
delete: async datasource => {
await API.deleteDatasource({
datasourceId: datasource?._id,
datasourceRev: datasource?._rev,
})
update(state => {
const sources = state.list.filter(
existing => existing._id !== datasource._id
)
return { list: sources, selected: null }
})
await queries.fetch()
await tables.fetch()
},
removeSchemaError: () => {
update(state => {
return { ...state, schemaError: null }
})
},
subscribe: derivedStore.subscribe,
fetch,
init: fetch,
select,
updateSchema,
save,
delete: deleteDatasource,
removeSchemaError,
}
}

View file

@ -1,5 +1,5 @@
import { writable, get } from "svelte/store"
import { datasources, integrations, tables, views } from "./"
import { writable, get, derived } from "svelte/store"
import { datasources, integrations } from "./"
import { API } from "api"
import { duplicateName } from "helpers/duplicate"
@ -10,125 +10,127 @@ const sortQueries = queryList => {
}
export function createQueriesStore() {
const store = writable({ list: [], selected: null })
const { subscribe, set, update } = store
const store = writable({
list: [],
selectedQueryId: null,
})
const derivedStore = derived(store, $store => ({
...$store,
selected: $store.list?.find(q => q._id === $store.selectedQueryId),
}))
const actions = {
init: async () => {
const queries = await API.getQueries()
set({
list: queries,
selected: null,
})
},
fetch: async () => {
const queries = await API.getQueries()
const fetch = async () => {
const queries = await API.getQueries()
sortQueries(queries)
store.update(state => ({
...state,
list: queries,
}))
}
const save = async (datasourceId, query) => {
const _integrations = get(integrations)
const dataSource = get(datasources).list.filter(
ds => ds._id === datasourceId
)
// Check if readable attribute is found
if (dataSource.length !== 0) {
const integration = _integrations[dataSource[0].source]
const readable = integration.query[query.queryVerb].readable
if (readable) {
query.readable = readable
}
}
query.datasourceId = datasourceId
const savedQuery = await API.saveQuery(query)
store.update(state => {
const idx = state.list.findIndex(query => query._id === savedQuery._id)
const queries = state.list
if (idx >= 0) {
queries.splice(idx, 1, savedQuery)
} else {
queries.push(savedQuery)
}
sortQueries(queries)
update(state => ({
...state,
return {
list: queries,
}))
},
save: async (datasourceId, query) => {
const _integrations = get(integrations)
const dataSource = get(datasources).list.filter(
ds => ds._id === datasourceId
)
// Check if readable attribute is found
if (dataSource.length !== 0) {
const integration = _integrations[dataSource[0].source]
const readable = integration.query[query.queryVerb].readable
if (readable) {
query.readable = readable
}
selectedQueryId: savedQuery._id,
}
query.datasourceId = datasourceId
const savedQuery = await API.saveQuery(query)
update(state => {
const idx = state.list.findIndex(query => query._id === savedQuery._id)
const queries = state.list
if (idx >= 0) {
queries.splice(idx, 1, savedQuery)
} else {
queries.push(savedQuery)
}
sortQueries(queries)
return {
list: queries,
selected: savedQuery._id,
}
})
return savedQuery
},
import: async ({ data, datasourceId }) => {
return await API.importQueries({
datasourceId,
data,
})
},
select: query => {
update(state => ({ ...state, selected: query._id }))
views.unselect()
tables.unselect()
datasources.unselect()
},
unselect: () => {
update(state => ({ ...state, selected: null }))
},
preview: async query => {
const parameters = query.parameters.reduce(
(acc, next) => ({
...acc,
[next.name]: next.default,
}),
{}
)
const result = await API.previewQuery({
...query,
parameters,
})
// Assume all the fields are strings and create a basic schema from the
// unique fields returned by the server
const schema = {}
for (let [field, type] of Object.entries(result.schemaFields)) {
schema[field] = type || "string"
}
return { ...result, schema, rows: result.rows || [] }
},
delete: async query => {
await API.deleteQuery({
queryId: query?._id,
queryRev: query?._rev,
})
update(state => {
state.list = state.list.filter(existing => existing._id !== query._id)
if (state.selected === query._id) {
state.selected = null
}
return state
})
},
duplicate: async query => {
let list = get(store).list
const newQuery = { ...query }
const datasourceId = query.datasourceId
})
return savedQuery
}
delete newQuery._id
delete newQuery._rev
newQuery.name = duplicateName(
query.name,
list.map(q => q.name)
)
const importQueries = async ({ data, datasourceId }) => {
return await API.importQueries({
datasourceId,
data,
})
}
return actions.save(datasourceId, newQuery)
},
const select = id => {
store.update(state => ({
...state,
selectedQueryId: id,
}))
}
const preview = async query => {
const parameters = query.parameters.reduce(
(acc, next) => ({
...acc,
[next.name]: next.default,
}),
{}
)
const result = await API.previewQuery({
...query,
parameters,
})
// Assume all the fields are strings and create a basic schema from the
// unique fields returned by the server
const schema = {}
for (let [field, type] of Object.entries(result.schemaFields)) {
schema[field] = type || "string"
}
return { ...result, schema, rows: result.rows || [] }
}
const deleteQuery = async query => {
await API.deleteQuery({
queryId: query?._id,
queryRev: query?._rev,
})
store.update(state => {
state.list = state.list.filter(existing => existing._id !== query._id)
return state
})
}
const duplicate = async query => {
let list = get(store).list
const newQuery = { ...query }
const datasourceId = query.datasourceId
delete newQuery._id
delete newQuery._rev
newQuery.name = duplicateName(
query.name,
list.map(q => q.name)
)
return await save(datasourceId, newQuery)
}
return {
subscribe,
set,
update,
...actions,
subscribe: derivedStore.subscribe,
fetch,
init: fetch,
select,
save,
import: importQueries,
delete: deleteQuery,
preview,
duplicate,
}
}

View file

@ -1,41 +1,35 @@
import { get, writable } from "svelte/store"
import { datasources, queries, views } from "./"
import { get, writable, derived } from "svelte/store"
import { datasources } from "./"
import { cloneDeep } from "lodash/fp"
import { API } from "api"
import { SWITCHABLE_TYPES } from "constants/backend"
export function createTablesStore() {
const store = writable({})
const { subscribe, update, set } = store
const store = writable({
list: [],
selectedTableId: null,
})
const derivedStore = derived(store, $store => ({
...$store,
selected: $store.list?.find(table => table._id === $store.selectedTableId),
}))
async function fetch() {
const fetch = async () => {
const tables = await API.getTables()
update(state => ({
store.update(state => ({
...state,
list: tables,
}))
return tables
}
async function select(table) {
if (!table) {
update(state => ({
...state,
selected: {},
}))
} else {
update(state => ({
...state,
selected: table,
draft: cloneDeep(table),
}))
views.unselect()
queries.unselect()
datasources.unselect()
}
const select = tableId => {
store.update(state => ({
...state,
selectedTableId: tableId,
}))
}
async function save(table) {
const save = async table => {
const updatedTable = cloneDeep(table)
const oldTable = get(store).list.filter(t => t._id === table._id)[0]
@ -72,96 +66,84 @@ export function createTablesStore() {
if (table.type === "external") {
await datasources.fetch()
}
await select(savedTable)
await select(savedTable._id)
return savedTable
}
const deleteTable = async table => {
await API.deleteTable({
tableId: table?._id,
tableRev: table?._rev,
})
await fetch()
}
const saveField = async ({
originalName,
field,
primaryDisplay = false,
indexes,
}) => {
let promise
store.update(state => {
// delete the original if renaming
// need to handle if the column had no name, empty string
if (originalName != null && originalName !== field.name) {
delete state.draft.schema[originalName]
state.draft._rename = {
old: originalName,
updated: field.name,
}
}
// Optionally set display column
if (primaryDisplay) {
state.draft.primaryDisplay = field.name
} else if (state.draft.primaryDisplay === originalName) {
const fields = Object.keys(state.draft.schema)
// pick another display column randomly if unselecting
state.draft.primaryDisplay = fields.filter(
name => name !== originalName || name !== field
)[0]
}
if (indexes) {
state.draft.indexes = indexes
}
state.draft.schema = {
...state.draft.schema,
[field.name]: cloneDeep(field),
}
promise = save(state.draft)
return state
})
if (promise) {
await promise
}
}
const deleteField = async field => {
let promise
store.update(state => {
delete state.draft.schema[field.name]
promise = save(state.draft)
return state
})
if (promise) {
await promise
}
}
return {
subscribe,
update,
subscribe: derivedStore.subscribe,
fetch,
init: fetch,
select,
unselect: () => {
update(state => ({
...state,
selected: null,
}))
},
save,
init: async () => {
const tables = await API.getTables()
set({
list: tables,
selected: {},
draft: {},
})
},
delete: async table => {
await API.deleteTable({
tableId: table?._id,
tableRev: table?._rev,
})
update(state => ({
...state,
list: state.list.filter(existing => existing._id !== table._id),
selected: {},
}))
},
saveField: async ({
originalName,
field,
primaryDisplay = false,
indexes,
}) => {
let promise
update(state => {
// delete the original if renaming
// need to handle if the column had no name, empty string
if (originalName != null && originalName !== field.name) {
delete state.draft.schema[originalName]
state.draft._rename = {
old: originalName,
updated: field.name,
}
}
// Optionally set display column
if (primaryDisplay) {
state.draft.primaryDisplay = field.name
} else if (state.draft.primaryDisplay === originalName) {
const fields = Object.keys(state.draft.schema)
// pick another display column randomly if unselecting
state.draft.primaryDisplay = fields.filter(
name => name !== originalName || name !== field
)[0]
}
if (indexes) {
state.draft.indexes = indexes
}
state.draft.schema = {
...state.draft.schema,
[field.name]: cloneDeep(field),
}
promise = save(state.draft)
return state
})
if (promise) {
await promise
}
},
deleteField: async field => {
let promise
update(state => {
delete state.draft.schema[field.name]
promise = save(state.draft)
return state
})
if (promise) {
await promise
}
},
delete: deleteTable,
saveField,
deleteField,
}
}

View file

@ -1,52 +1,54 @@
import { writable, get } from "svelte/store"
import { tables, datasources, queries } from "./"
import { writable, get, derived } from "svelte/store"
import { tables } from "./"
import { API } from "api"
export function createViewsStore() {
const { subscribe, update } = writable({
list: [],
selected: null,
const store = writable({
selectedViewName: null,
})
const derivedStore = derived([store, tables], ([$store, $tables]) => {
let list = []
$tables.list?.forEach(table => {
list = list.concat(Object.values(table?.views || {}))
})
return {
...$store,
list,
selected: list.find(view => view.name === $store.selectedViewName),
}
})
const select = name => {
store.update(state => ({
...state,
selectedViewName: name,
}))
}
const deleteView = async view => {
await API.deleteView(view)
await tables.fetch()
}
const save = async view => {
const savedView = await API.saveView(view)
const viewMeta = {
name: view.name,
...savedView,
}
const viewTable = get(tables).list.find(table => table._id === view.tableId)
if (view.originalName) delete viewTable.views[view.originalName]
viewTable.views[view.name] = viewMeta
await tables.save(viewTable)
}
return {
subscribe,
update,
select: view => {
update(state => ({
...state,
selected: view,
}))
tables.unselect()
queries.unselect()
datasources.unselect()
},
unselect: () => {
update(state => ({
...state,
selected: null,
}))
},
delete: async view => {
await API.deleteView(view)
await tables.fetch()
},
save: async view => {
const savedView = await API.saveView(view)
const viewMeta = {
name: view.name,
...savedView,
}
const viewTable = get(tables).list.find(
table => table._id === view.tableId
)
if (view.originalName) delete viewTable.views[view.originalName]
viewTable.views[view.name] = viewMeta
await tables.save(viewTable)
update(state => ({ ...state, selected: viewMeta }))
},
subscribe: derivedStore.subscribe,
select,
delete: deleteView,
save,
}
}