Implementing UI to support the backend put in place.

This commit is contained in:
mike12345567 2021-07-02 14:33:05 +01:00
parent 3eb8f3c445
commit 10730d8053
7 changed files with 274 additions and 118 deletions

View File

@ -10,9 +10,10 @@
export let sourceId export let sourceId
$: selectedView = $views.selected && $views.selected.name $: selectedView = $views.selected && $views.selected.name
$: sortedTables = $tables.list.filter(table => table.sourceId === sourceId).sort(alphabetical) $: sortedTables = $tables.list
.filter(table => table.sourceId === sourceId)
.sort(alphabetical)
function selectTable(table) { function selectTable(table) {
tables.select(table) tables.select(table)

View File

@ -1,25 +1,31 @@
<script> <script>
import { RelationshipTypes } from "constants/backend" import { RelationshipTypes } from "constants/backend"
import { Menu, MenuItem, MenuSection, Button, Input, Icon, ModalContent, RadioGroup, Heading, Select } from "@budibase/bbui" import { Button, Input, ModalContent, Select } from "@budibase/bbui"
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { uuid } from "builderStore/uuid"
export let save export let save
export let datasource export let datasource
export let from export let plusTables = []
export let plusTables export let fromRelationship = {}
export let relationship = {} export let toRelationship = {}
export let close export let close
let originalName = relationship.name let originalFromName = fromRelationship.name, originalToName = toRelationship.name
function isValid(relationship) {
if (relationship.relationshipType === RelationshipTypes.MANY_TO_MANY && !relationship.through) {
return false
}
return relationship.name && relationship.tableId && relationship.relationshipType
}
$: tableOptions = plusTables.map(table => ({ label: table.name, value: table._id })) $: tableOptions = plusTables.map(table => ({ label: table.name, value: table._id }))
$: valid = relationship.name && relationship.tableId && relationship.relationshipType $: fromTable = plusTables.find(table => table._id === toRelationship?.tableId)
$: from = plusTables.find(table => table._id === relationship.source) $: toTable = plusTables.find(table => table._id === fromRelationship?.tableId)
$: to = plusTables.find(table => table._id === relationship.tableId) $: through = plusTables.find(table => table._id === fromRelationship?.through)
$: through = plusTables.find(table => table._id === relationship.through) $: valid = toTable && fromTable && isValid(fromRelationship)
$: linkTable = through || to $: linkTable = through || toTable
$: relationshipTypes = [ $: relationshipTypes = [
{ {
label: "Many", label: "Many",
@ -27,53 +33,89 @@
}, },
{ {
label: "One", label: "One",
value: RelationshipTypes.ONE_TO_MANY, value: RelationshipTypes.MANY_TO_ONE,
} }
] ]
$: updateRelationshipType(fromRelationship?.relationshipType)
function onChangeRelationshipType(evt) { function updateRelationshipType(fromType) {
if (evt.detail === RelationshipTypes.ONE_TO_MANY) { if (fromType === RelationshipTypes.MANY_TO_MANY) {
relationship.through = null toRelationship.relationshipType = RelationshipTypes.MANY_TO_MANY
} else {
toRelationship.relationshipType = RelationshipTypes.MANY_TO_ONE
} }
} }
function buildRelationships() {
// if any to many only need to check from
const manyToMany = fromRelationship.relationshipType === RelationshipTypes.MANY_TO_MANY
// main is simply used to know this is the side the user configured it from
const id = uuid()
let relateFrom = {
...fromRelationship,
type: "link",
main: true,
_id: id,
}
let relateTo = {
...toRelationship,
type: "link",
_id: id,
}
// [0] is because we don't support composite keys for relationships right now
if (manyToMany) {
relateFrom = {
...relateFrom,
through: through._id,
fieldName: toTable.primary[0],
}
relateTo = {
...relateTo,
through: through._id,
fieldName: fromTable.primary[0],
}
} else {
relateFrom = {
...relateFrom,
foreignKey: relateFrom.fieldName,
fieldName: fromTable.primary[0],
}
relateTo = {
...relateTo,
relationshipType: RelationshipTypes.ONE_TO_MANY,
foreignKey: relateFrom.fieldName,
fieldName: fromTable.primary[0],
}
}
fromRelationship = relateFrom
toRelationship = relateTo
}
// save the relationship on to the datasource // save the relationship on to the datasource
async function saveRelationship() { async function saveRelationship() {
const manyToMany = relationship.relationshipType === RelationshipTypes.MANY_TO_MANY buildRelationships()
// source of relationship // source of relationship
datasource.entities[from.name].schema[relationship.name] = { datasource.entities[fromTable.name].schema[fromRelationship.name] = fromRelationship
type: "link",
foreignKey: relationship.fieldName,
...relationship
}
// save other side of relationship in the other schema // save other side of relationship in the other schema
datasource.entities[to.name].schema[relationship.name] = { datasource.entities[toTable.name].schema[toRelationship.name] = toRelationship
name: relationship.name,
type: "link",
relationshipType: manyToMany ? RelationshipTypes.MANY_TO_MANY : RelationshipTypes.MANY_TO_ONE,
tableId: from._id,
fieldName: relationship.fieldName,
foreignKey: relationship.fieldName
}
// If relationship has been renamed // If relationship has been renamed
if (originalName !== relationship.name) { if (originalFromName !== fromRelationship.name) {
delete datasource.entities[from.name].schema[originalName] delete datasource.entities[fromTable.name].schema[originalFromName]
delete datasource.entities[to.name].schema[originalName] }
if (originalToName !== toRelationship.name) {
delete datasource.entities[toTable.name].schema[originalToName]
} }
console.log({
from: datasource.entities[from.name].schema[relationship.name],
to: datasource.entities[to.name].schema[relationship.name],
})
await save() await save()
await tables.fetch() await tables.fetch()
} }
async function deleteRelationship() { async function deleteRelationship() {
delete datasource.entities[from.name].schema[relationship.name] delete datasource.entities[fromTable.name].schema[fromRelationship.name]
delete datasource.entities[to.name].schema[relationship.name] delete datasource.entities[toTable.name].schema[toRelationship.name]
await save() await save()
await tables.fetch() await tables.fetch()
close() close()
@ -87,57 +129,54 @@
onConfirm={saveRelationship} onConfirm={saveRelationship}
disabled={!valid} disabled={!valid}
> >
<Input label="Relationship Name" bind:value={relationship.name} /> <div class="relationship-names">
<div class="left-name">
<Input label="From name" bind:value={fromRelationship.name} />
</div>
<div class="right-name">
<Input label="To name" bind:value={toRelationship.name} />
</div>
</div>
<div class="table-selector"> <div class="table-selector">
<Select <Select
label="Relationship" label="Relationship"
options={relationshipTypes} options={relationshipTypes}
bind:value={relationship.relationshipType} bind:value={fromRelationship.relationshipType}
/> />
<Select <Select
label="From" label="From"
options={tableOptions} options={tableOptions}
bind:value={relationship.source} bind:value={toRelationship.tableId}
/> />
<Select <Select
label={"Has many"} label={"Has many"}
options={tableOptions} options={tableOptions}
bind:value={relationship.tableId} bind:value={fromRelationship.tableId}
/> />
{#if relationship?.relationshipType === RelationshipTypes.MANY_TO_MANY} {#if fromRelationship?.relationshipType === RelationshipTypes.MANY_TO_MANY}
<Select <Select
label={"Through"} label={"Through"}
options={tableOptions} options={tableOptions}
bind:value={relationship.through} bind:value={fromRelationship.through}
/> />
{:else if toTable}
<Select <Select
label={"Key"} label={`Foreign Key (${toTable?.name})`}
options={Object.keys(through.schema || {})} options={Object.keys(toTable?.schema)}
bind:value={relationship.fieldName} bind:value={fromRelationship.fieldName}
/>
{/if}
{#if relationship?.relationshipType === RelationshipTypes.ONE_TO_MANY && to}
<Select
label={`Foreign Key (${to.name})`}
options={Object.keys(to.schema)}
bind:value={relationship.fieldName}
/> />
{/if} {/if}
</div> </div>
<div slot="footer"> <div slot="footer">
{#if originalName !== null} {#if originalFromName !== null}
<Button warning text on:click={deleteRelationship}>Delete</Button> <Button warning text on:click={deleteRelationship}>Delete</Button>
{/if} {/if}
</div> </div>
</ModalContent> </ModalContent>
<style> <style>
@ -146,4 +185,14 @@
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr);
grid-gap: var(--spacing-xl); grid-gap: var(--spacing-xl);
} }
.relationship-names {
display: grid;
grid-gap: var(--spacing-xl);
}
.left-name {
grid-column: 1;
}
.right-name {
grid-column: 2;
}
</style> </style>

View File

@ -2,31 +2,71 @@
import { goto, beforeUrlChange } from "@roxi/routify" import { goto, beforeUrlChange } from "@roxi/routify"
import { Button, Heading, Body, Divider, Layout, Modal } from "@budibase/bbui" import { Button, Heading, Body, Divider, Layout, Modal } from "@budibase/bbui"
import { datasources, integrations, queries, tables } from "stores/backend" import { datasources, integrations, queries, tables } from "stores/backend"
import { RelationshipTypes } from "constants/backend"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte" import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
import CreateEditRelationship from "./CreateEditRelationship/CreateEditRelationship.svelte" import CreateEditRelationship from "./CreateEditRelationship/CreateEditRelationship.svelte"
import DisplayColumnModal from "./modals/EditDisplayColumnsModal.svelte"
import ICONS from "components/backend/DatasourceNavigator/icons" import ICONS from "components/backend/DatasourceNavigator/icons"
import { capitalise } from "helpers" import { capitalise } from "helpers"
let unsaved = false let unsaved = false
let relationshipModal let relationshipModal
let selectedRelationship let displayColumnModal
let selectedFromRelationship, selectedToRelationship
$: datasource = $datasources.list.find(ds => ds._id === $datasources.selected) $: datasource = $datasources.list.find(ds => ds._id === $datasources.selected)
$: integration = datasource && $integrations[datasource.source] $: integration = datasource && $integrations[datasource.source]
$: plusTables = datasource?.plus ? Object.values(datasource.entities || {}) : [] $: plusTables = datasource?.plus
? Object.values(datasource.entities || {})
: []
$: relationships = getRelationships(plusTables)
function buildRelationshipDisplayString(fromTable, toTable) { function getRelationships(tables) {
let displayString = fromTable.name if (!tables || !Array.isArray(tables)) {
const toTableName = toTable.tableId?.split("_").pop() return {}
}
displayString += `→ ${toTableName} (${toTable.relationshipType})` let pairs = {}
for (let table of tables) {
if (toTable.through) { for (let column of Object.values(table.schema)) {
// TODO: Through stuff if (column.type !== "link") {
continue
}
console.log(`table - ${table.name} - ${column.name} - id: ${column._id} - ${column.main}`)
// these relationships have an id to pair them to each other
// one has a main for the from side
const key = column.main ? "from" : "to"
pairs[column._id] = {
...pairs[column._id],
[key]: column,
}
}
}
return pairs
} }
function buildRelationshipDisplayString(fromCol, toCol) {
function getTableName(tableId) {
if (!tableId || typeof tableId !== "string") {
return null
}
return plusTables.find(table => table._id === tableId)?.name || "Unknown"
}
if (!toCol || !fromCol) {
return "Cannot build name"
}
const fromTableName = getTableName(toCol.tableId)
const toTableName = getTableName(fromCol.tableId)
const throughTableName = getTableName(fromCol.through)
console.log(throughTableName)
let displayFrom = `${fromTableName} (${fromCol.name})`
let displayTo = `${toTableName} (${toCol.name})`
let displayString
if (throughTableName) {
displayString = `${displayFrom} through ${throughTableName} → ${displayTo}`
} else {
displayString = `${displayFrom} → ${displayTo}`
}
return displayString return displayString
} }
@ -66,11 +106,16 @@
unsaved = true unsaved = true
} }
function openRelationshipModal(relationship) { function openRelationshipModal(fromRelationship, toRelationship) {
selectedRelationship = relationship || {} selectedFromRelationship = fromRelationship || {}
selectedToRelationship = toRelationship || {}
relationshipModal.show() relationshipModal.show()
} }
function openDisplayColumnModal() {
displayColumnModal.show()
}
$beforeUrlChange(() => { $beforeUrlChange(() => {
if (unsaved) { if (unsaved) {
notifications.error( notifications.error(
@ -83,7 +128,18 @@
</script> </script>
<Modal bind:this={relationshipModal}> <Modal bind:this={relationshipModal}>
<CreateEditRelationship {datasource} save={saveDatasource} close={relationshipModal.hide} {plusTables} relationship={selectedRelationship} /> <CreateEditRelationship
{datasource}
save={saveDatasource}
close={relationshipModal.hide}
{plusTables}
fromRelationship={selectedFromRelationship}
toRelationship={selectedToRelationship}
/>
</Modal>
<Modal bind:this={displayColumnModal}>
<DisplayColumnModal {datasource} {plusTables} save={saveDatasource} />
</Modal> </Modal>
{#if datasource && integration} {#if datasource && integration}
@ -119,10 +175,17 @@
<Divider /> <Divider />
<div class="query-header"> <div class="query-header">
<Heading size="S">Tables</Heading> <Heading size="S">Tables</Heading>
<div class="table-buttons">
{#if plusTables && plusTables.length !== 0}
<Button primary on:click={openDisplayColumnModal}>
Update display columns
</Button>
{/if}
<Button primary on:click={updateDatasourceSchema} <Button primary on:click={updateDatasourceSchema}
>Fetch Tables From Database</Button >Fetch Tables From Database</Button
> >
</div> </div>
</div>
<Body> <Body>
This datasource can determine tables automatically. Budibase can fetch This datasource can determine tables automatically. Budibase can fetch
your tables directly from the database and you can use them without your tables directly from the database and you can use them without
@ -130,10 +193,7 @@
</Body> </Body>
<div class="query-list"> <div class="query-list">
{#each plusTables as table} {#each plusTables as table}
<div <div class="query-list-item" on:click={() => onClickTable(table)}>
class="query-list-item"
on:click={() => onClickTable(table)}
>
<p class="query-name">{table.name}</p> <p class="query-name">{table.name}</p>
<p>Primary Key: {table.primary}</p> <p>Primary Key: {table.primary}</p>
<p></p> <p></p>
@ -144,27 +204,32 @@
<Divider /> <Divider />
<div class="query-header"> <div class="query-header">
<Heading size="S">Relationships</Heading> <Heading size="S">Relationships</Heading>
<Button primary on:click={() => openRelationshipModal()}>Create Relationship</Button> <Button primary on:click={() => openRelationshipModal()}
>Create Relationship</Button
>
</div> </div>
<Body> <Body>
Tell budibase how your tables are related to get even more smart features. Tell budibase how your tables are related to get even more smart
features.
</Body> </Body>
<div class="query-list"> <div class="query-list">
{#each plusTables as table} {#each Object.values(relationships) as relationship}
{#each Object.keys(table.schema) as column}
{#if table.schema[column].type === "link" && table.schema[column].relationshipType !== RelationshipTypes.MANY_TO_ONE}
<div <div
class="query-list-item" class="query-list-item"
on:click={() => openRelationshipModal(table.schema[column])}> on:click={() =>
<p class="query-name">{table.schema[column].name}</p> openRelationshipModal(relationship.from, relationship.to)}
<p>{buildRelationshipDisplayString(table, table.schema[column])}</p> >
<p>
{buildRelationshipDisplayString(
relationship.from,
relationship.to
)}
</p>
<p class="query-name">{relationship.from?.name} to {relationship.to?.name}</p>
<p></p> <p></p>
</div> </div>
{/if}
{/each}
{/each} {/each}
</div> </div>
{/if} {/if}
<Divider /> <Divider />
<div class="query-header"> <div class="query-header">
@ -252,4 +317,10 @@
text-overflow: ellipsis; text-overflow: ellipsis;
font-size: var(--font-size-s); font-size: var(--font-size-s);
} }
.table-buttons {
display: grid;
grid-gap: var(--spacing-l);
grid-template-columns:1fr 1fr;
}
</style> </style>

View File

@ -0,0 +1,38 @@
<script>
import { ModalContent, Select, Body } from "@budibase/bbui"
import { tables } from "stores/backend"
export let datasource
export let plusTables
export let save
async function saveDisplayColumns() {
// be explicit about copying over
for (let table of plusTables) {
datasource.entities[table.name].primaryDisplay = table.primaryDisplay
}
save()
await tables.fetch()
}
function getColumnOptions(table) {
if (!table || !table.schema) {
return []
}
return Object.entries(table.schema).filter(field => field[1].type !== "link").map(([fieldName]) => fieldName)
}
</script>
<ModalContent
title="Edit display columns"
confirmText="Save"
onConfirm={saveDisplayColumns}
>
<Body>Select the columns that will be shown when displaying relationships.</Body>
{#each plusTables as table}
<Select
label={table.name}
options={getColumnOptions(table)}
bind:value={table.primaryDisplay}
/>
{/each}
</ModalContent>

View File

@ -37,6 +37,6 @@ export interface BudibaseAppMetadata {
name: string name: string
url: string url: string
instance: { _id: string } instance: { _id: string }
updatedAt: Date, updatedAt: Date
createdAt: Date createdAt: Date
} }

View File

@ -135,10 +135,7 @@ module PostgresModule {
* Fetches the tables from the postgres table and assigns them to the datasource. * Fetches the tables from the postgres table and assigns them to the datasource.
* @param {*} datasourceId - datasourceId to fetch * @param {*} datasourceId - datasourceId to fetch
*/ */
async buildSchema( async buildSchema(datasourceId: string, entities: Record<string, Table>) {
datasourceId: string,
entities: Record<string, Table>
) {
let tableKeys: { [key: string]: string[] } = {} let tableKeys: { [key: string]: string[] } = {}
try { try {
const primaryKeysResponse = await this.client.query( const primaryKeysResponse = await this.client.query(