mirror of https://github.com/Budibase/budibase.git
101 changed files with 3872 additions and 1612 deletions
@ -0,0 +1,36 @@ |
|||||
|
name: Budibase Release Helm Charts |
||||
|
|
||||
|
on: |
||||
|
workflow_dispatch: |
||||
|
|
||||
|
jobs: |
||||
|
release: |
||||
|
runs-on: ubuntu-latest |
||||
|
|
||||
|
steps: |
||||
|
- uses: actions/checkout@v2 |
||||
|
with: |
||||
|
fetch-depth: 0 |
||||
|
|
||||
|
- name: 'Get Previous tag' |
||||
|
id: previoustag |
||||
|
uses: "WyriHaximus/github-action-get-previous-tag@v1" |
||||
|
|
||||
|
- name: Install Helm |
||||
|
uses: azure/setup-helm@v1 |
||||
|
with: |
||||
|
version: v3.4.0 |
||||
|
|
||||
|
# - run: yarn release:helm |
||||
|
# env: |
||||
|
# BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }} |
||||
|
|
||||
|
- name: Configure Git |
||||
|
run: | |
||||
|
git config user.name "Budibase Helm Bot" |
||||
|
git config user.email "<>" |
||||
|
|
||||
|
- name: Run chart-releaser |
||||
|
uses: helm/chart-releaser-action@v1.2.1 |
||||
|
env: |
||||
|
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" |
||||
@ -0,0 +1,3 @@ |
|||||
|
apiVersion: v1 |
||||
|
entries: {} |
||||
|
generated: "2021-12-13T12:46:40.291206+01:00" |
||||
@ -0,0 +1,59 @@ |
|||||
|
<script> |
||||
|
import { createEventDispatcher } from "svelte" |
||||
|
|
||||
|
let dispatch = createEventDispatcher() |
||||
|
|
||||
|
export let type = "info" |
||||
|
export let icon = "Info" |
||||
|
export let size = "S" |
||||
|
export let extraButtonText |
||||
|
export let extraButtonAction |
||||
|
|
||||
|
let show = true |
||||
|
|
||||
|
function clear() { |
||||
|
show = false |
||||
|
dispatch("change") |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
{#if show} |
||||
|
<div class="spectrum-Toast spectrum-Toast--{type}"> |
||||
|
<svg |
||||
|
class="spectrum-Icon spectrum-Icon--size{size} spectrum-Toast-typeIcon" |
||||
|
focusable="false" |
||||
|
aria-hidden="true" |
||||
|
> |
||||
|
<use xlink:href="#spectrum-icon-18-{icon}" /> |
||||
|
</svg> |
||||
|
<div class="spectrum-Toast-body"> |
||||
|
<div class="spectrum-Toast-content"> |
||||
|
<slot /> |
||||
|
</div> |
||||
|
{#if extraButtonText && extraButtonAction} |
||||
|
<button |
||||
|
class="spectrum-Button spectrum-Button--sizeM spectrum-Button--overBackground spectrum-Button--quiet" |
||||
|
on:click={extraButtonAction} |
||||
|
> |
||||
|
<span class="spectrum-Button-label">{extraButtonText}</span> |
||||
|
</button> |
||||
|
{/if} |
||||
|
</div> |
||||
|
<div class="spectrum-Toast-buttons"> |
||||
|
<button |
||||
|
class="spectrum-ClearButton spectrum-ClearButton--overBackground spectrum-ClearButton--size{size}" |
||||
|
on:click={clear} |
||||
|
> |
||||
|
<div class="spectrum-ClearButton-fill"> |
||||
|
<svg |
||||
|
class="spectrum-ClearButton-icon spectrum-Icon spectrum-UIIcon-Cross100" |
||||
|
focusable="false" |
||||
|
aria-hidden="true" |
||||
|
> |
||||
|
<use xlink:href="#spectrum-css-icon-Cross100" /> |
||||
|
</svg> |
||||
|
</div> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
{/if} |
||||
|
After Width: | Height: | Size: 21 KiB |
@ -0,0 +1,44 @@ |
|||||
|
import { datasources, tables } from "../stores/backend" |
||||
|
import { IntegrationNames } from "../constants/backend" |
||||
|
import analytics, { Events } from "../analytics" |
||||
|
import { get } from "svelte/store" |
||||
|
import cloneDeep from "lodash/cloneDeepWith" |
||||
|
|
||||
|
function prepareData(config) { |
||||
|
let datasource = {} |
||||
|
let existingTypeCount = get(datasources).list.filter( |
||||
|
ds => ds.source === config.type |
||||
|
).length |
||||
|
|
||||
|
let baseName = IntegrationNames[config.type] |
||||
|
let name = |
||||
|
existingTypeCount === 0 ? baseName : `${baseName}-${existingTypeCount + 1}` |
||||
|
|
||||
|
datasource.type = "datasource" |
||||
|
datasource.source = config.type |
||||
|
datasource.config = config.config |
||||
|
datasource.name = name |
||||
|
datasource.plus = config.plus |
||||
|
|
||||
|
return datasource |
||||
|
} |
||||
|
|
||||
|
export async function saveDatasource(config) { |
||||
|
const datasource = prepareData(config) |
||||
|
// Create datasource
|
||||
|
const resp = await datasources.save(datasource, datasource.plus) |
||||
|
|
||||
|
// update the tables incase data source plus
|
||||
|
await tables.fetch() |
||||
|
await datasources.select(resp._id) |
||||
|
analytics.captureEvent(Events.DATASOURCE.CREATED, { |
||||
|
name: resp.name, |
||||
|
source: resp.source, |
||||
|
}) |
||||
|
return resp |
||||
|
} |
||||
|
|
||||
|
export async function createRestDatasource(integration) { |
||||
|
const config = cloneDeep(integration) |
||||
|
return saveDatasource(config) |
||||
|
} |
||||
@ -0,0 +1,221 @@ |
|||||
|
<script> |
||||
|
import { |
||||
|
Heading, |
||||
|
Body, |
||||
|
Divider, |
||||
|
InlineAlert, |
||||
|
Button, |
||||
|
notifications, |
||||
|
Modal, |
||||
|
Table, |
||||
|
} from "@budibase/bbui" |
||||
|
import { datasources, integrations, tables } from "stores/backend" |
||||
|
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte" |
||||
|
import CreateExternalTableModal from "./CreateExternalTableModal.svelte" |
||||
|
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte" |
||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte" |
||||
|
import { goto } from "@roxi/routify" |
||||
|
|
||||
|
export let datasource |
||||
|
export let save |
||||
|
|
||||
|
let tableSchema = { |
||||
|
name: {}, |
||||
|
primary: { displayName: "Primary Key" }, |
||||
|
} |
||||
|
let relationshipSchema = { |
||||
|
tables: {}, |
||||
|
columns: {}, |
||||
|
} |
||||
|
let relationshipModal |
||||
|
let createExternalTableModal |
||||
|
let selectedFromRelationship, selectedToRelationship |
||||
|
let confirmDialog |
||||
|
|
||||
|
$: integration = datasource && $integrations[datasource.source] |
||||
|
$: plusTables = datasource?.plus |
||||
|
? Object.values(datasource?.entities || {}) |
||||
|
: [] |
||||
|
$: relationships = getRelationships(plusTables) |
||||
|
$: schemaError = $datasources.schemaError |
||||
|
$: relationshipInfo = relationshipTableData(relationships) |
||||
|
|
||||
|
function getRelationships(tables) { |
||||
|
if (!tables || !Array.isArray(tables)) { |
||||
|
return {} |
||||
|
} |
||||
|
let pairs = {} |
||||
|
for (let table of tables) { |
||||
|
for (let column of Object.values(table.schema)) { |
||||
|
if (column.type !== "link") { |
||||
|
continue |
||||
|
} |
||||
|
// 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) |
||||
|
|
||||
|
let displayString |
||||
|
if (throughTableName) { |
||||
|
displayString = `${fromTableName} through ${throughTableName} → ${toTableName}` |
||||
|
} else { |
||||
|
displayString = `${fromTableName} → ${toTableName}` |
||||
|
} |
||||
|
return displayString |
||||
|
} |
||||
|
|
||||
|
async function updateDatasourceSchema() { |
||||
|
try { |
||||
|
await datasources.updateSchema(datasource) |
||||
|
notifications.success(`Datasource ${name} tables updated successfully.`) |
||||
|
await tables.fetch() |
||||
|
} catch (err) { |
||||
|
notifications.error(`Error updating datasource schema: ${err}`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function onClickTable(table) { |
||||
|
tables.select(table) |
||||
|
$goto(`../../table/${table._id}`) |
||||
|
} |
||||
|
|
||||
|
function openRelationshipModal(fromRelationship, toRelationship) { |
||||
|
selectedFromRelationship = fromRelationship || {} |
||||
|
selectedToRelationship = toRelationship || {} |
||||
|
relationshipModal.show() |
||||
|
} |
||||
|
|
||||
|
function createNewTable() { |
||||
|
createExternalTableModal.show() |
||||
|
} |
||||
|
|
||||
|
function relationshipTableData(relations) { |
||||
|
return Object.values(relations).map(relationship => ({ |
||||
|
tables: buildRelationshipDisplayString( |
||||
|
relationship.from, |
||||
|
relationship.to |
||||
|
), |
||||
|
columns: `${relationship.from?.name} to ${relationship.to?.name}`, |
||||
|
from: relationship.from, |
||||
|
to: relationship.to, |
||||
|
})) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<Modal bind:this={relationshipModal}> |
||||
|
<CreateEditRelationship |
||||
|
{datasource} |
||||
|
{save} |
||||
|
close={relationshipModal.hide} |
||||
|
{plusTables} |
||||
|
fromRelationship={selectedFromRelationship} |
||||
|
toRelationship={selectedToRelationship} |
||||
|
/> |
||||
|
</Modal> |
||||
|
|
||||
|
<Modal bind:this={createExternalTableModal}> |
||||
|
<CreateExternalTableModal {datasource} /> |
||||
|
</Modal> |
||||
|
|
||||
|
<ConfirmDialog |
||||
|
bind:this={confirmDialog} |
||||
|
okText="Fetch tables" |
||||
|
onOk={updateDatasourceSchema} |
||||
|
onCancel={() => confirmDialog.hide()} |
||||
|
warning={false} |
||||
|
title="Confirm table fetch" |
||||
|
> |
||||
|
<Body> |
||||
|
If you have fetched tables from this database before, this action may |
||||
|
overwrite any changes you made after your initial fetch. |
||||
|
</Body> |
||||
|
</ConfirmDialog> |
||||
|
|
||||
|
<Divider size="S" /> |
||||
|
<div class="query-header"> |
||||
|
<Heading size="S">Tables</Heading> |
||||
|
<div class="table-buttons"> |
||||
|
<Button secondary on:click={() => confirmDialog.show()}> |
||||
|
Fetch tables |
||||
|
</Button> |
||||
|
<Button cta icon="Add" on:click={createNewTable}>New table</Button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<Body> |
||||
|
This datasource can determine tables automatically. Budibase can fetch your |
||||
|
tables directly from the database and you can use them without having to write |
||||
|
any queries at all. |
||||
|
</Body> |
||||
|
{#if schemaError} |
||||
|
<InlineAlert |
||||
|
type="error" |
||||
|
header="Error fetching tables" |
||||
|
message={schemaError} |
||||
|
onConfirm={datasources.removeSchemaError} |
||||
|
/> |
||||
|
{/if} |
||||
|
<Table |
||||
|
on:click={({ detail }) => onClickTable(detail)} |
||||
|
schema={tableSchema} |
||||
|
data={Object.values(plusTables)} |
||||
|
allowEditColumns={false} |
||||
|
allowEditRows={false} |
||||
|
allowSelectRows={false} |
||||
|
customRenderers={[{ column: "primary", component: ArrayRenderer }]} |
||||
|
/> |
||||
|
{#if plusTables?.length !== 0} |
||||
|
<Divider size="S" /> |
||||
|
<div class="query-header"> |
||||
|
<Heading size="S">Relationships</Heading> |
||||
|
<Button primary on:click={openRelationshipModal}> |
||||
|
Define relationship |
||||
|
</Button> |
||||
|
</div> |
||||
|
<Body> |
||||
|
Tell budibase how your tables are related to get even more smart features. |
||||
|
</Body> |
||||
|
{/if} |
||||
|
<Table |
||||
|
on:click={({ detail }) => openRelationshipModal(detail.from, detail.to)} |
||||
|
schema={relationshipSchema} |
||||
|
data={relationshipInfo} |
||||
|
allowEditColumns={false} |
||||
|
allowEditRows={false} |
||||
|
allowSelectRows={false} |
||||
|
/> |
||||
|
|
||||
|
<style> |
||||
|
.query-header { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin: 0 0 var(--spacing-s) 0; |
||||
|
} |
||||
|
|
||||
|
.table-buttons { |
||||
|
display: flex; |
||||
|
gap: var(--spacing-m); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,58 @@ |
|||||
|
<script> |
||||
|
import { Divider, Heading, ActionButton, Badge, Body } from "@budibase/bbui" |
||||
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte" |
||||
|
import RestAuthenticationBuilder from "./auth/RestAuthenticationBuilder.svelte" |
||||
|
|
||||
|
export let datasource |
||||
|
|
||||
|
let addHeader |
||||
|
</script> |
||||
|
|
||||
|
<Divider size="S" /> |
||||
|
<div class="section-header"> |
||||
|
<div class="badge"> |
||||
|
<Heading size="S">Headers</Heading> |
||||
|
<Badge quiet grey>Optional</Badge> |
||||
|
</div> |
||||
|
</div> |
||||
|
<Body size="S"> |
||||
|
Headers enable you to provide additional information about the request, such |
||||
|
as format. |
||||
|
</Body> |
||||
|
<KeyValueBuilder |
||||
|
bind:this={addHeader} |
||||
|
bind:object={datasource.config.defaultHeaders} |
||||
|
on:change |
||||
|
noAddButton |
||||
|
/> |
||||
|
<div> |
||||
|
<ActionButton icon="Add" on:click={() => addHeader.addEntry()}> |
||||
|
Add header |
||||
|
</ActionButton> |
||||
|
</div> |
||||
|
|
||||
|
<Divider size="S" /> |
||||
|
<div class="section-header"> |
||||
|
<div class="badge"> |
||||
|
<Heading size="S">Authentication</Heading> |
||||
|
<Badge quiet grey>Optional</Badge> |
||||
|
</div> |
||||
|
</div> |
||||
|
<Body size="S"> |
||||
|
Create an authentication config that can be shared with queries. |
||||
|
</Body> |
||||
|
<RestAuthenticationBuilder bind:configs={datasource.config.authConfigs} /> |
||||
|
|
||||
|
<style> |
||||
|
.section-header { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.badge { |
||||
|
display: flex; |
||||
|
gap: var(--spacing-m); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,13 @@ |
|||||
|
<script> |
||||
|
import { AUTH_TYPE_LABELS } from "./authTypes" |
||||
|
|
||||
|
export let value |
||||
|
|
||||
|
const renderAuthType = value => { |
||||
|
return AUTH_TYPE_LABELS.filter(type => type.value === value).map( |
||||
|
type => type.label |
||||
|
) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
{renderAuthType(value)} |
||||
@ -0,0 +1,65 @@ |
|||||
|
<script> |
||||
|
import { Table, Modal, Layout, ActionButton } from "@budibase/bbui" |
||||
|
import AuthTypeRenderer from "./AuthTypeRenderer.svelte" |
||||
|
import RestAuthenticationModal from "./RestAuthenticationModal.svelte" |
||||
|
import { uuid } from "builderStore/uuid" |
||||
|
|
||||
|
export let configs = [] |
||||
|
|
||||
|
let currentConfig = null |
||||
|
let modal |
||||
|
|
||||
|
const schema = { |
||||
|
name: "", |
||||
|
type: "", |
||||
|
} |
||||
|
|
||||
|
const openConfigModal = config => { |
||||
|
currentConfig = config |
||||
|
modal.show() |
||||
|
} |
||||
|
|
||||
|
const onConfirm = config => { |
||||
|
if (currentConfig) { |
||||
|
configs = configs.map(c => { |
||||
|
// replace the current config with the new one |
||||
|
if (c._id === currentConfig._id) { |
||||
|
return config |
||||
|
} |
||||
|
return c |
||||
|
}) |
||||
|
} else { |
||||
|
config._id = uuid() |
||||
|
configs = [...configs, config] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const onRemove = () => { |
||||
|
configs = configs.filter(c => { |
||||
|
return c._id !== currentConfig._id |
||||
|
}) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<Modal bind:this={modal}> |
||||
|
<RestAuthenticationModal {configs} {currentConfig} {onConfirm} {onRemove} /> |
||||
|
</Modal> |
||||
|
|
||||
|
<Layout gap="S" noPadding> |
||||
|
{#if configs && configs.length > 0} |
||||
|
<Table |
||||
|
on:click={({ detail }) => openConfigModal(detail)} |
||||
|
{schema} |
||||
|
data={configs} |
||||
|
allowEditColumns={false} |
||||
|
allowEditRows={false} |
||||
|
allowSelectRows={false} |
||||
|
customRenderers={[{ column: "type", component: AuthTypeRenderer }]} |
||||
|
/> |
||||
|
{/if} |
||||
|
<div> |
||||
|
<ActionButton on:click={() => openConfigModal()} con="Add" |
||||
|
>Add authentication</ActionButton |
||||
|
> |
||||
|
</div> |
||||
|
</Layout> |
||||
@ -0,0 +1,218 @@ |
|||||
|
<script> |
||||
|
import { onMount } from "svelte" |
||||
|
import { ModalContent, Layout, Select, Body, Input } from "@budibase/bbui" |
||||
|
import { AUTH_TYPE_LABELS, AUTH_TYPES } from "./authTypes" |
||||
|
|
||||
|
export let configs |
||||
|
export let currentConfig |
||||
|
export let onConfirm |
||||
|
export let onRemove |
||||
|
|
||||
|
let form = { |
||||
|
basic: {}, |
||||
|
bearer: {}, |
||||
|
} |
||||
|
|
||||
|
let errors = { |
||||
|
basic: {}, |
||||
|
bearer: {}, |
||||
|
} |
||||
|
|
||||
|
let blurred = { |
||||
|
basic: {}, |
||||
|
bearer: {}, |
||||
|
} |
||||
|
|
||||
|
let hasErrors = false |
||||
|
let hasChanged = false |
||||
|
|
||||
|
onMount(() => { |
||||
|
if (currentConfig) { |
||||
|
deconstructConfig() |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
/** |
||||
|
* map the current config's data into the form by type |
||||
|
*/ |
||||
|
const deconstructConfig = () => { |
||||
|
form.name = currentConfig.name |
||||
|
form.type = currentConfig.type |
||||
|
|
||||
|
if (currentConfig.type === AUTH_TYPES.BASIC) { |
||||
|
form.basic = { |
||||
|
...currentConfig.config, |
||||
|
} |
||||
|
} else if (currentConfig.type === AUTH_TYPES.BEARER) { |
||||
|
form.bearer = { |
||||
|
...currentConfig.config, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* map the form into a new config to save by type |
||||
|
*/ |
||||
|
const constructConfig = () => { |
||||
|
const newConfig = { |
||||
|
name: form.name, |
||||
|
type: form.type, |
||||
|
} |
||||
|
|
||||
|
if (currentConfig) { |
||||
|
newConfig._id = currentConfig._id |
||||
|
} |
||||
|
|
||||
|
if (form.type === AUTH_TYPES.BASIC) { |
||||
|
newConfig.config = { |
||||
|
...form.basic, |
||||
|
} |
||||
|
} else if (form.type === AUTH_TYPES.BEARER) { |
||||
|
newConfig.config = { |
||||
|
...form.bearer, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return newConfig |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* compare the existing config with the new config to see if there are any changes |
||||
|
*/ |
||||
|
const checkChanged = () => { |
||||
|
if (currentConfig) { |
||||
|
hasChanged = |
||||
|
JSON.stringify(currentConfig) !== JSON.stringify(constructConfig()) |
||||
|
} else { |
||||
|
hasChanged = true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const checkErrors = () => { |
||||
|
hasErrors = false |
||||
|
|
||||
|
// NAME |
||||
|
const nameError = () => { |
||||
|
// Unique name |
||||
|
if (form.name) { |
||||
|
errors.name = |
||||
|
// check for duplicate excluding the current config |
||||
|
configs.find( |
||||
|
c => c.name === form.name && c.name !== currentConfig?.name |
||||
|
) !== undefined |
||||
|
? "Name must be unique" |
||||
|
: null |
||||
|
} |
||||
|
// Name required |
||||
|
else { |
||||
|
errors.name = "Name is required" |
||||
|
} |
||||
|
return !!errors.name |
||||
|
} |
||||
|
|
||||
|
// TYPE |
||||
|
const typeError = () => { |
||||
|
errors.type = form.type ? null : "Type is required" |
||||
|
return !!errors.type |
||||
|
} |
||||
|
|
||||
|
// BASIC AUTH |
||||
|
const basicAuthErrors = () => { |
||||
|
errors.basic.username = form.basic.username |
||||
|
? null |
||||
|
: "Username is required" |
||||
|
errors.basic.password = form.basic.password |
||||
|
? null |
||||
|
: "Password is required" |
||||
|
|
||||
|
return !!(errors.basic.username || errors.basic.password || commonError) |
||||
|
} |
||||
|
|
||||
|
// BEARER TOKEN |
||||
|
const bearerTokenErrors = () => { |
||||
|
errors.bearer.token = form.bearer.token ? null : "Token is required" |
||||
|
return !!(errors.bearer.token || commonError) |
||||
|
} |
||||
|
|
||||
|
const commonError = nameError() || typeError() |
||||
|
if (form.type === AUTH_TYPES.BASIC) { |
||||
|
hasErrors = basicAuthErrors() || commonError |
||||
|
} else if (form.type === AUTH_TYPES.BEARER) { |
||||
|
hasErrors = bearerTokenErrors() || commonError |
||||
|
} else { |
||||
|
hasErrors = !!commonError |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const onFieldChange = () => { |
||||
|
checkErrors() |
||||
|
checkChanged() |
||||
|
} |
||||
|
|
||||
|
const onConfirmInternal = () => { |
||||
|
onConfirm(constructConfig()) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<ModalContent |
||||
|
title={currentConfig ? "Update Authentication" : "Add Authentication"} |
||||
|
onConfirm={onConfirmInternal} |
||||
|
confirmText={currentConfig ? "Update" : "Add"} |
||||
|
disabled={hasErrors || !hasChanged} |
||||
|
cancelText={"Cancel"} |
||||
|
size="M" |
||||
|
showSecondaryButton={!!currentConfig} |
||||
|
secondaryButtonText={"Remove"} |
||||
|
secondaryAction={onRemove} |
||||
|
secondaryButtonWarning={true} |
||||
|
> |
||||
|
<Layout gap="S"> |
||||
|
<Body size="S"> |
||||
|
The authorization header will be automatically generated when you send the |
||||
|
request. |
||||
|
</Body> |
||||
|
<Input |
||||
|
label="Name" |
||||
|
bind:value={form.name} |
||||
|
on:change={onFieldChange} |
||||
|
on:blur={() => (blurred.name = true)} |
||||
|
error={blurred.name ? errors.name : null} |
||||
|
/> |
||||
|
<Select |
||||
|
label="Type" |
||||
|
bind:value={form.type} |
||||
|
on:change={onFieldChange} |
||||
|
options={AUTH_TYPE_LABELS} |
||||
|
on:blur={() => (blurred.type = true)} |
||||
|
error={blurred.type ? errors.type : null} |
||||
|
/> |
||||
|
{#if form.type === AUTH_TYPES.BASIC} |
||||
|
<Input |
||||
|
label="Username" |
||||
|
bind:value={form.basic.username} |
||||
|
on:change={onFieldChange} |
||||
|
on:blur={() => (blurred.basic.username = true)} |
||||
|
error={blurred.basic.username ? errors.basic.username : null} |
||||
|
/> |
||||
|
<Input |
||||
|
label="Password" |
||||
|
bind:value={form.basic.password} |
||||
|
on:change={onFieldChange} |
||||
|
on:blur={() => (blurred.basic.password = true)} |
||||
|
error={blurred.basic.password ? errors.basic.password : null} |
||||
|
/> |
||||
|
{/if} |
||||
|
{#if form.type === AUTH_TYPES.BEARER} |
||||
|
<Input |
||||
|
label="Token" |
||||
|
bind:value={form.bearer.token} |
||||
|
on:change={onFieldChange} |
||||
|
on:blur={() => (blurred.bearer.token = true)} |
||||
|
error={blurred.bearer.token ? errors.bearer.token : null} |
||||
|
/> |
||||
|
{/if} |
||||
|
</Layout> |
||||
|
</ModalContent> |
||||
|
|
||||
|
<style> |
||||
|
</style> |
||||
@ -0,0 +1,15 @@ |
|||||
|
export const AUTH_TYPES = { |
||||
|
BASIC: "basic", |
||||
|
BEARER: "bearer", |
||||
|
} |
||||
|
|
||||
|
export const AUTH_TYPE_LABELS = [ |
||||
|
{ |
||||
|
label: "Basic Auth", |
||||
|
value: AUTH_TYPES.BASIC, |
||||
|
}, |
||||
|
{ |
||||
|
label: "Bearer Token", |
||||
|
value: AUTH_TYPES.BEARER, |
||||
|
}, |
||||
|
] |
||||
@ -0,0 +1,63 @@ |
|||||
|
<script> |
||||
|
import { Heading, Icon, Input, Label, Body } from "@budibase/bbui" |
||||
|
import { createEventDispatcher } from "svelte" |
||||
|
|
||||
|
let dispatch = createEventDispatcher() |
||||
|
|
||||
|
export let defaultValue = "" |
||||
|
export let value |
||||
|
export let type = "label" |
||||
|
export let size = "M" |
||||
|
|
||||
|
let editing = false |
||||
|
|
||||
|
function setEditing(state) { |
||||
|
editing = state |
||||
|
if (editing) { |
||||
|
dispatch("change") |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div class="parent"> |
||||
|
{#if !editing} |
||||
|
{#if type === "heading"} |
||||
|
<Heading {size}>{value || defaultValue}</Heading> |
||||
|
{:else if type === "body"} |
||||
|
<Body {size}>{value || defaultValue}</Body> |
||||
|
{:else} |
||||
|
<Label {size}>{value || defaultValue}</Label> |
||||
|
{/if} |
||||
|
<div class="hide"> |
||||
|
<Icon name="Edit" hoverable size="S" on:click={() => setEditing(true)} /> |
||||
|
</div> |
||||
|
{:else} |
||||
|
<div class="input"> |
||||
|
<Input placeholder={defaultValue} bind:value on:change /> |
||||
|
</div> |
||||
|
<Icon |
||||
|
name="SaveFloppy" |
||||
|
hoverable |
||||
|
size="S" |
||||
|
on:click={() => setEditing(false)} |
||||
|
/> |
||||
|
{/if} |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.parent { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: var(--spacing-m); |
||||
|
} |
||||
|
.hide { |
||||
|
display: none; |
||||
|
margin-top: 5px; |
||||
|
} |
||||
|
.parent:hover .hide { |
||||
|
display: block; |
||||
|
} |
||||
|
.input { |
||||
|
flex: 1; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<script> |
||||
|
export let value |
||||
|
</script> |
||||
|
|
||||
|
{Array.isArray(value) ? value.join(", ") : value} |
||||
@ -0,0 +1,6 @@ |
|||||
|
<script> |
||||
|
import { capitalise } from "helpers" |
||||
|
export let value |
||||
|
</script> |
||||
|
|
||||
|
{capitalise(value)} |
||||
@ -0,0 +1,56 @@ |
|||||
|
<script> |
||||
|
import { Label, Select } from "@budibase/bbui" |
||||
|
import { permissions, roles } from "stores/backend" |
||||
|
import { onMount } from "svelte" |
||||
|
import { Roles } from "constants/backend" |
||||
|
|
||||
|
export let query |
||||
|
export let saveId |
||||
|
export let label |
||||
|
|
||||
|
$: updateRole(roleId, saveId) |
||||
|
|
||||
|
let roleId, loaded |
||||
|
|
||||
|
async function updateRole(role, id) { |
||||
|
roleId = role |
||||
|
const queryId = query?._id || id |
||||
|
if (roleId && queryId) { |
||||
|
for (let level of ["read", "write"]) { |
||||
|
await permissions.save({ |
||||
|
level, |
||||
|
role, |
||||
|
resource: queryId, |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onMount(async () => { |
||||
|
if (!query || !query._id) { |
||||
|
roleId = Roles.BASIC |
||||
|
loaded = true |
||||
|
return |
||||
|
} |
||||
|
try { |
||||
|
roleId = (await permissions.forResource(query._id))["read"] |
||||
|
} catch (err) { |
||||
|
roleId = Roles.BASIC |
||||
|
} |
||||
|
loaded = true |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
{#if loaded} |
||||
|
{#if label} |
||||
|
<Label>{label}</Label> |
||||
|
{/if} |
||||
|
<Select |
||||
|
value={roleId} |
||||
|
on:change={e => updateRole(e.detail)} |
||||
|
options={$roles} |
||||
|
getOptionLabel={x => x.name} |
||||
|
getOptionValue={x => x._id} |
||||
|
autoWidth |
||||
|
/> |
||||
|
{/if} |
||||
@ -0,0 +1,11 @@ |
|||||
|
<script> |
||||
|
import { TextArea } from "@budibase/bbui" |
||||
|
|
||||
|
export let data |
||||
|
export let height |
||||
|
export let minHeight = "120" |
||||
|
|
||||
|
$: string = JSON.stringify(data || {}, null, 2) |
||||
|
</script> |
||||
|
|
||||
|
<TextArea disabled value={string} {height} {minHeight} /> |
||||
@ -0,0 +1,127 @@ |
|||||
|
<script> |
||||
|
import { ModalContent, Modal, Icon, ColorPicker, Label } from "@budibase/bbui" |
||||
|
import { apps } from "stores/portal" |
||||
|
|
||||
|
export let app |
||||
|
let modal |
||||
|
$: selectedIcon = app?.icon?.name |
||||
|
$: selectedColor = app?.icon?.color |
||||
|
|
||||
|
let iconsList = [ |
||||
|
"Actions", |
||||
|
"ConversionFunnel", |
||||
|
"App", |
||||
|
"Briefcase", |
||||
|
"Money", |
||||
|
"ShoppingCart", |
||||
|
"Form", |
||||
|
"Help", |
||||
|
"Monitoring", |
||||
|
"Sandbox", |
||||
|
"Project", |
||||
|
"Organisations", |
||||
|
"Magnify", |
||||
|
"Launch", |
||||
|
"Car", |
||||
|
"Camera", |
||||
|
"Bug", |
||||
|
"Channel", |
||||
|
"Calculator", |
||||
|
"Calendar", |
||||
|
"GraphDonut", |
||||
|
"GraphBarHorizontal", |
||||
|
"Demographic", |
||||
|
"Apps", |
||||
|
] |
||||
|
export const show = () => { |
||||
|
modal.show() |
||||
|
} |
||||
|
export const hide = () => { |
||||
|
modal.hide() |
||||
|
} |
||||
|
|
||||
|
const onCancel = () => { |
||||
|
selectedIcon = "" |
||||
|
selectedColor = "" |
||||
|
hide() |
||||
|
} |
||||
|
|
||||
|
const changeColor = val => { |
||||
|
selectedColor = val |
||||
|
} |
||||
|
|
||||
|
const save = async () => { |
||||
|
await apps.update(app.instance._id, { |
||||
|
icon: { |
||||
|
name: selectedIcon, |
||||
|
color: selectedColor, |
||||
|
}, |
||||
|
}) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<Modal bind:this={modal} on:hide={onCancel}> |
||||
|
<ModalContent |
||||
|
title={"Edit Icon"} |
||||
|
confirmText={"Save"} |
||||
|
onConfirm={() => save()} |
||||
|
> |
||||
|
<div class="scrollable-icons"> |
||||
|
<div class="title-spacing"> |
||||
|
<Label>Select an Icon</Label> |
||||
|
</div> |
||||
|
<div class="grid"> |
||||
|
{#each iconsList as item} |
||||
|
<div |
||||
|
class="icon-item" |
||||
|
style="color: {item === selectedIcon ? selectedColor : ''}" |
||||
|
on:click={() => (selectedIcon = item)} |
||||
|
> |
||||
|
<Icon name={item} /> |
||||
|
</div> |
||||
|
{/each} |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="color-selection"> |
||||
|
<div> |
||||
|
<Label>Select a Color</Label> |
||||
|
</div> |
||||
|
<div class="color-selection-item"> |
||||
|
<ColorPicker |
||||
|
bind:value={selectedColor} |
||||
|
on:change={e => changeColor(e.detail)} |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ModalContent> |
||||
|
</Modal> |
||||
|
|
||||
|
<style> |
||||
|
.scrollable-icons { |
||||
|
overflow-y: auto; |
||||
|
height: 230px; |
||||
|
} |
||||
|
|
||||
|
.grid { |
||||
|
display: grid; |
||||
|
grid-gap: 20px; |
||||
|
grid-template-columns: 1fr 1fr 1fr 1fr 1fr; |
||||
|
} |
||||
|
|
||||
|
.color-selection { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.color-selection-item { |
||||
|
margin-left: 20px; |
||||
|
} |
||||
|
|
||||
|
.title-spacing { |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.icon-item { |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,121 @@ |
|||||
|
import { IntegrationTypes } from "constants/backend" |
||||
|
|
||||
|
export function schemaToFields(schema) { |
||||
|
const response = {} |
||||
|
if (schema && typeof schema === "object") { |
||||
|
for (let [field, value] of Object.entries(schema)) { |
||||
|
response[field] = value?.type || "string" |
||||
|
} |
||||
|
} |
||||
|
return response |
||||
|
} |
||||
|
|
||||
|
export function fieldsToSchema(fields) { |
||||
|
const response = {} |
||||
|
if (fields && typeof fields === "object") { |
||||
|
for (let [name, type] of Object.entries(fields)) { |
||||
|
response[name] = { name, type } |
||||
|
} |
||||
|
} |
||||
|
return response |
||||
|
} |
||||
|
|
||||
|
export function breakQueryString(qs) { |
||||
|
if (!qs) { |
||||
|
return {} |
||||
|
} |
||||
|
if (qs.includes("?")) { |
||||
|
qs = qs.split("?")[1] |
||||
|
} |
||||
|
const params = qs.split("&") |
||||
|
let paramObj = {} |
||||
|
for (let param of params) { |
||||
|
const [key, value] = param.split("=") |
||||
|
paramObj[key] = value |
||||
|
} |
||||
|
return paramObj |
||||
|
} |
||||
|
|
||||
|
export function buildQueryString(obj) { |
||||
|
let str = "" |
||||
|
if (obj) { |
||||
|
for (let [key, value] of Object.entries(obj)) { |
||||
|
if (!key || key === "") { |
||||
|
continue |
||||
|
} |
||||
|
if (str !== "") { |
||||
|
str += "&" |
||||
|
} |
||||
|
str += `${key}=${value || ""}` |
||||
|
} |
||||
|
} |
||||
|
return str |
||||
|
} |
||||
|
|
||||
|
export function keyValueToQueryParameters(obj) { |
||||
|
let array = [] |
||||
|
if (obj && typeof obj === "object") { |
||||
|
for (let [key, value] of Object.entries(obj)) { |
||||
|
array.push({ name: key, default: value }) |
||||
|
} |
||||
|
} |
||||
|
return array |
||||
|
} |
||||
|
|
||||
|
export function queryParametersToKeyValue(array) { |
||||
|
let obj = {} |
||||
|
if (Array.isArray(array)) { |
||||
|
for (let param of array) { |
||||
|
obj[param.name] = param.default |
||||
|
} |
||||
|
} |
||||
|
return obj |
||||
|
} |
||||
|
|
||||
|
export function customQueryIconText(datasource, query) { |
||||
|
if (datasource.source !== IntegrationTypes.REST) { |
||||
|
return |
||||
|
} |
||||
|
switch (query.queryVerb) { |
||||
|
case "create": |
||||
|
return "POST" |
||||
|
case "update": |
||||
|
return "PUT" |
||||
|
case "read": |
||||
|
return "GET" |
||||
|
case "delete": |
||||
|
return "DELETE" |
||||
|
case "patch": |
||||
|
return "PATCH" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function customQueryIconColor(datasource, query) { |
||||
|
if (datasource.source !== IntegrationTypes.REST) { |
||||
|
return |
||||
|
} |
||||
|
switch (query.queryVerb) { |
||||
|
case "create": |
||||
|
return "#dcc339" |
||||
|
case "update": |
||||
|
return "#5197ec" |
||||
|
case "read": |
||||
|
return "#53a761" |
||||
|
case "delete": |
||||
|
return "#ea7d82" |
||||
|
case "patch": |
||||
|
default: |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function flipHeaderState(headersActivity) { |
||||
|
if (!headersActivity) { |
||||
|
return {} |
||||
|
} |
||||
|
const enabled = {} |
||||
|
Object.entries(headersActivity).forEach(([key, value]) => { |
||||
|
enabled[key] = !value |
||||
|
}) |
||||
|
return enabled |
||||
|
} |
||||
@ -1,13 +1,23 @@ |
|||||
<script> |
<script> |
||||
import { params } from "@roxi/routify" |
import { params } from "@roxi/routify" |
||||
import { queries } from "stores/backend" |
import { queries, datasources } from "stores/backend" |
||||
|
import { IntegrationTypes } from "constants/backend" |
||||
|
import { goto } from "@roxi/routify" |
||||
|
|
||||
|
let datasourceId |
||||
if ($params.query) { |
if ($params.query) { |
||||
const query = $queries.list.find(q => q._id === $params.query) |
const query = $queries.list.find(q => q._id === $params.query) |
||||
if (query) { |
if (query) { |
||||
queries.select(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) { |
||||
|
$goto(`../rest/${$params.query}`) |
||||
|
} |
||||
</script> |
</script> |
||||
|
|
||||
<slot /> |
<slot /> |
||||
|
|||||
@ -0,0 +1,62 @@ |
|||||
|
<script> |
||||
|
import { Body } from "@budibase/bbui" |
||||
|
import { RawRestBodyTypes } from "constants/backend" |
||||
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte" |
||||
|
import CodeMirrorEditor, { |
||||
|
EditorModes, |
||||
|
} from "components/common/CodeMirrorEditor.svelte" |
||||
|
|
||||
|
const objectTypes = [RawRestBodyTypes.FORM, RawRestBodyTypes.ENCODED] |
||||
|
const textTypes = [RawRestBodyTypes.JSON, RawRestBodyTypes.TEXT] |
||||
|
|
||||
|
export let query |
||||
|
export let bodyType |
||||
|
|
||||
|
$: checkRequestBody(bodyType) |
||||
|
|
||||
|
function checkRequestBody(type) { |
||||
|
if (!bodyType || !query) { |
||||
|
return |
||||
|
} |
||||
|
const currentType = typeof query?.fields.requestBody |
||||
|
if (objectTypes.includes(type) && currentType !== "object") { |
||||
|
query.fields.requestBody = {} |
||||
|
} else if (textTypes.includes(type) && currentType !== "string") { |
||||
|
query.fields.requestBody = "" |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div class="margin"> |
||||
|
{#if bodyType === RawRestBodyTypes.NONE} |
||||
|
<div class="none"> |
||||
|
<Body size="S" weight="800">THE REQUEST DOES NOT HAVE A BODY</Body> |
||||
|
</div> |
||||
|
{:else if objectTypes.includes(bodyType)} |
||||
|
<KeyValueBuilder |
||||
|
bind:object={query.fields.requestBody} |
||||
|
name="param" |
||||
|
headings |
||||
|
/> |
||||
|
{:else if textTypes.includes(bodyType)} |
||||
|
<CodeMirrorEditor |
||||
|
height={200} |
||||
|
mode={bodyType === RawRestBodyTypes.JSON |
||||
|
? EditorModes.JSON |
||||
|
: EditorModes.Text} |
||||
|
value={query.fields.requestBody} |
||||
|
resize="vertical" |
||||
|
on:change={e => (query.fields.requestBody = e.detail)} |
||||
|
/> |
||||
|
{/if} |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.margin { |
||||
|
margin-top: var(--spacing-m); |
||||
|
} |
||||
|
.none { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,13 @@ |
|||||
|
<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 /> |
||||
@ -0,0 +1,460 @@ |
|||||
|
<script> |
||||
|
import { params } from "@roxi/routify" |
||||
|
import { datasources, integrations, queries, flags } from "stores/backend" |
||||
|
import { |
||||
|
Layout, |
||||
|
Input, |
||||
|
Select, |
||||
|
Tabs, |
||||
|
Tab, |
||||
|
Banner, |
||||
|
Divider, |
||||
|
Button, |
||||
|
Heading, |
||||
|
RadioGroup, |
||||
|
Label, |
||||
|
Body, |
||||
|
TextArea, |
||||
|
Table, |
||||
|
notifications, |
||||
|
} from "@budibase/bbui" |
||||
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte" |
||||
|
import EditableLabel from "components/common/inputs/EditableLabel.svelte" |
||||
|
import CodeMirrorEditor, { |
||||
|
EditorModes, |
||||
|
} from "components/common/CodeMirrorEditor.svelte" |
||||
|
import RestBodyInput from "../../_components/RestBodyInput.svelte" |
||||
|
import { capitalise } from "helpers" |
||||
|
import { onMount } from "svelte" |
||||
|
import { |
||||
|
fieldsToSchema, |
||||
|
schemaToFields, |
||||
|
breakQueryString, |
||||
|
buildQueryString, |
||||
|
keyValueToQueryParameters, |
||||
|
queryParametersToKeyValue, |
||||
|
flipHeaderState, |
||||
|
} from "helpers/data/utils" |
||||
|
import { |
||||
|
RestBodyTypes as bodyTypes, |
||||
|
SchemaTypeOptions, |
||||
|
} from "constants/backend" |
||||
|
import JSONPreview from "components/integration/JSONPreview.svelte" |
||||
|
import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte" |
||||
|
import Placeholder from "assets/bb-spaceship.svg" |
||||
|
import { cloneDeep } from "lodash/fp" |
||||
|
|
||||
|
let query, datasource |
||||
|
let breakQs = {}, |
||||
|
bindings = {} |
||||
|
let url = "" |
||||
|
let saveId |
||||
|
let response, schema, enabledHeaders |
||||
|
let datasourceType, integrationInfo, queryConfig, responseSuccess |
||||
|
let authConfigId |
||||
|
|
||||
|
$: datasourceType = datasource?.source |
||||
|
$: integrationInfo = $integrations[datasourceType] |
||||
|
$: queryConfig = integrationInfo?.query |
||||
|
$: url = buildUrl(url, breakQs) |
||||
|
$: checkQueryName(url) |
||||
|
$: responseSuccess = |
||||
|
response?.info?.code >= 200 && response?.info?.code <= 206 |
||||
|
$: authConfigs = buildAuthConfigs(datasource) |
||||
|
|
||||
|
function getSelectedQuery() { |
||||
|
return cloneDeep( |
||||
|
$queries.list.find(q => q._id === $queries.selected) || { |
||||
|
datasourceId: $params.selectedDatasource, |
||||
|
parameters: [], |
||||
|
fields: { |
||||
|
// only init the objects, everything else is optional strings |
||||
|
disabledHeaders: {}, |
||||
|
headers: {}, |
||||
|
}, |
||||
|
queryVerb: "read", |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function checkQueryName(inputUrl = null) { |
||||
|
if (query && (!query.name || query.flags.urlName)) { |
||||
|
query.flags.urlName = true |
||||
|
query.name = url || inputUrl |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function buildUrl(base, qsObj) { |
||||
|
if (!base) { |
||||
|
return base |
||||
|
} |
||||
|
const qs = buildQueryString(qsObj) |
||||
|
let newUrl = base |
||||
|
if (base.includes("?")) { |
||||
|
newUrl = base.split("?")[0] |
||||
|
} |
||||
|
return qs.length > 0 ? `${newUrl}?${qs}` : newUrl |
||||
|
} |
||||
|
|
||||
|
const buildAuthConfigs = datasource => { |
||||
|
if (datasource?.config?.authConfigs) { |
||||
|
return datasource.config.authConfigs.map(c => ({ |
||||
|
label: c.name, |
||||
|
value: c._id, |
||||
|
})) |
||||
|
} |
||||
|
return [] |
||||
|
} |
||||
|
|
||||
|
function learnMoreBanner() { |
||||
|
window.open("https://docs.budibase.com/building-apps/data/transformers") |
||||
|
} |
||||
|
|
||||
|
function buildQuery() { |
||||
|
const newQuery = { ...query } |
||||
|
const queryString = buildQueryString(breakQs) |
||||
|
newQuery.fields.path = url.split("?")[0] |
||||
|
newQuery.fields.queryString = queryString |
||||
|
newQuery.fields.authConfigId = authConfigId |
||||
|
newQuery.fields.disabledHeaders = flipHeaderState(enabledHeaders) |
||||
|
newQuery.schema = fieldsToSchema(schema) |
||||
|
newQuery.parameters = keyValueToQueryParameters(bindings) |
||||
|
return newQuery |
||||
|
} |
||||
|
|
||||
|
async function saveQuery() { |
||||
|
const toSave = buildQuery() |
||||
|
try { |
||||
|
const { _id } = await queries.save(toSave.datasourceId, toSave) |
||||
|
saveId = _id |
||||
|
query = getSelectedQuery() |
||||
|
notifications.success(`Request saved successfully.`) |
||||
|
} catch (err) { |
||||
|
notifications.error(`Error creating query. ${err.message}`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function runQuery() { |
||||
|
try { |
||||
|
response = await queries.preview(buildQuery(query)) |
||||
|
if (response.rows.length === 0) { |
||||
|
notifications.info("Request did not return any data.") |
||||
|
} else { |
||||
|
response.info = response.info || { code: 200 } |
||||
|
schema = response.schema |
||||
|
notifications.success("Request sent successfully.") |
||||
|
} |
||||
|
} catch (err) { |
||||
|
notifications.error(err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const getAuthConfigId = () => { |
||||
|
let id = query.fields.authConfigId |
||||
|
if (id) { |
||||
|
// find the matching config on the datasource |
||||
|
const matchedConfig = datasource?.config?.authConfigs?.filter( |
||||
|
c => c._id === id |
||||
|
)[0] |
||||
|
// clear the id if the config is not found (deleted) |
||||
|
// i.e. just show 'None' in the dropdown |
||||
|
if (!matchedConfig) { |
||||
|
id = undefined |
||||
|
} |
||||
|
} |
||||
|
return id |
||||
|
} |
||||
|
|
||||
|
onMount(async () => { |
||||
|
query = getSelectedQuery() |
||||
|
// clear any unsaved changes to the datasource |
||||
|
await datasources.init() |
||||
|
datasource = $datasources.list.find(ds => ds._id === query?.datasourceId) |
||||
|
const datasourceUrl = datasource?.config.url |
||||
|
const qs = query?.fields.queryString |
||||
|
breakQs = breakQueryString(qs) |
||||
|
if (datasourceUrl && !query.fields.path?.startsWith(datasourceUrl)) { |
||||
|
const path = query.fields.path |
||||
|
query.fields.path = `${datasource.config.url}/${path ? path : ""}` |
||||
|
} |
||||
|
url = buildUrl(query.fields.path, breakQs) |
||||
|
schema = schemaToFields(query.schema) |
||||
|
bindings = queryParametersToKeyValue(query.parameters) |
||||
|
authConfigId = getAuthConfigId() |
||||
|
if (!query.fields.disabledHeaders) { |
||||
|
query.fields.disabledHeaders = {} |
||||
|
} |
||||
|
// make sure the disabled headers are set (migration) |
||||
|
for (let header of Object.keys(query.fields.headers)) { |
||||
|
if (!query.fields.disabledHeaders[header]) { |
||||
|
query.fields.disabledHeaders[header] = false |
||||
|
} |
||||
|
} |
||||
|
enabledHeaders = flipHeaderState(query.fields.disabledHeaders) |
||||
|
if (query && !query.transformer) { |
||||
|
query.transformer = "return data" |
||||
|
} |
||||
|
if (query && !query.flags) { |
||||
|
query.flags = { |
||||
|
urlName: false, |
||||
|
} |
||||
|
} |
||||
|
if (query && !query.fields.bodyType) { |
||||
|
query.fields.bodyType = "none" |
||||
|
} |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
{#if query && queryConfig} |
||||
|
<div class="inner"> |
||||
|
<div class="top"> |
||||
|
<Layout gap="S"> |
||||
|
<div class="top-bar"> |
||||
|
<EditableLabel |
||||
|
type="heading" |
||||
|
bind:value={query.name} |
||||
|
defaultValue="Untitled" |
||||
|
on:change={() => (query.flags.urlName = false)} |
||||
|
/> |
||||
|
<div class="access"> |
||||
|
<Label>Access level</Label> |
||||
|
<AccessLevelSelect {query} {saveId} /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="url-block"> |
||||
|
<div class="verb"> |
||||
|
<Select |
||||
|
bind:value={query.queryVerb} |
||||
|
on:change={() => {}} |
||||
|
options={Object.keys(queryConfig)} |
||||
|
getOptionLabel={verb => |
||||
|
queryConfig[verb]?.displayName || capitalise(verb)} |
||||
|
/> |
||||
|
</div> |
||||
|
<div class="url"> |
||||
|
<Input bind:value={url} placeholder="http://www.api.com/endpoint" /> |
||||
|
</div> |
||||
|
<Button cta disabled={!url} on:click={runQuery}>Send</Button> |
||||
|
</div> |
||||
|
<Tabs selected="Bindings" quiet noPadding noHorizPadding> |
||||
|
<Tab title="Bindings"> |
||||
|
<KeyValueBuilder |
||||
|
bind:object={bindings} |
||||
|
tooltip="Set the name of the binding which can be used in Handlebars statements throughout your query" |
||||
|
name="binding" |
||||
|
headings |
||||
|
keyPlaceholder="Binding name" |
||||
|
valuePlaceholder="Default" |
||||
|
/> |
||||
|
</Tab> |
||||
|
<Tab title="Params"> |
||||
|
<KeyValueBuilder bind:object={breakQs} name="param" headings /> |
||||
|
</Tab> |
||||
|
<Tab title="Headers"> |
||||
|
<KeyValueBuilder |
||||
|
bind:object={query.fields.headers} |
||||
|
bind:activity={enabledHeaders} |
||||
|
toggle |
||||
|
name="header" |
||||
|
headings |
||||
|
/> |
||||
|
</Tab> |
||||
|
<Tab title="Body"> |
||||
|
<RadioGroup |
||||
|
bind:value={query.fields.bodyType} |
||||
|
options={bodyTypes} |
||||
|
direction="horizontal" |
||||
|
getOptionLabel={option => option.name} |
||||
|
getOptionValue={option => option.value} |
||||
|
/> |
||||
|
<RestBodyInput bind:bodyType={query.fields.bodyType} bind:query /> |
||||
|
</Tab> |
||||
|
<Tab title="Transformer"> |
||||
|
<Layout noPadding> |
||||
|
{#if !$flags.queryTransformerBanner} |
||||
|
<Banner |
||||
|
extraButtonText="Learn more" |
||||
|
extraButtonAction={learnMoreBanner} |
||||
|
on:change={() => |
||||
|
flags.updateFlag("queryTransformerBanner", true)} |
||||
|
> |
||||
|
Add a JavaScript function to transform the query result. |
||||
|
</Banner> |
||||
|
{/if} |
||||
|
<CodeMirrorEditor |
||||
|
height={200} |
||||
|
mode={EditorModes.JSON} |
||||
|
value={query.transformer} |
||||
|
resize="vertical" |
||||
|
on:change={e => (query.transformer = e.detail)} |
||||
|
/> |
||||
|
</Layout> |
||||
|
</Tab> |
||||
|
<div class="auth-container"> |
||||
|
<div /> |
||||
|
<!-- spacer --> |
||||
|
<div class="auth-select"> |
||||
|
<Select |
||||
|
label="Auth" |
||||
|
labelPosition="left" |
||||
|
placeholder="None" |
||||
|
bind:value={authConfigId} |
||||
|
options={authConfigs} |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</Tabs> |
||||
|
</Layout> |
||||
|
</div> |
||||
|
<div class="bottom"> |
||||
|
<Layout paddingY="S" gap="S"> |
||||
|
<Divider size="S" /> |
||||
|
{#if !response && Object.keys(schema).length === 0} |
||||
|
<Heading size="M">Response</Heading> |
||||
|
<div class="placeholder"> |
||||
|
<div class="placeholder-internal"> |
||||
|
<img alt="placeholder" src={Placeholder} /> |
||||
|
<Body size="XS" textAlign="center" |
||||
|
>{"enter a url in the textbox above and click send to get a response".toUpperCase()}</Body |
||||
|
> |
||||
|
</div> |
||||
|
</div> |
||||
|
{:else} |
||||
|
<Tabs |
||||
|
selected={!response ? "Schema" : "JSON"} |
||||
|
quiet |
||||
|
noPadding |
||||
|
noHorizPadding |
||||
|
> |
||||
|
{#if response} |
||||
|
<Tab title="JSON"> |
||||
|
<div> |
||||
|
<JSONPreview height="300" data={response.rows[0]} /> |
||||
|
</div> |
||||
|
</Tab> |
||||
|
{/if} |
||||
|
{#if schema || response} |
||||
|
<Tab title="Schema"> |
||||
|
<KeyValueBuilder |
||||
|
bind:object={schema} |
||||
|
name="schema" |
||||
|
headings |
||||
|
options={SchemaTypeOptions} |
||||
|
/> |
||||
|
</Tab> |
||||
|
{/if} |
||||
|
{#if response} |
||||
|
<Tab title="Raw"> |
||||
|
<TextArea disabled value={response.extra?.raw} height="300" /> |
||||
|
</Tab> |
||||
|
<Tab title="Headers"> |
||||
|
<KeyValueBuilder object={response.extra?.headers} readOnly /> |
||||
|
</Tab> |
||||
|
<Tab title="Preview"> |
||||
|
<div class="table"> |
||||
|
{#if response} |
||||
|
<Table |
||||
|
schema={response?.schema} |
||||
|
data={response?.rows} |
||||
|
allowEditColumns={false} |
||||
|
allowEditRows={false} |
||||
|
allowSelectRows={false} |
||||
|
/> |
||||
|
{/if} |
||||
|
</div> |
||||
|
</Tab> |
||||
|
<div class="stats"> |
||||
|
<Label size="L"> |
||||
|
Status: <span class={responseSuccess ? "green" : "red"} |
||||
|
>{response?.info.code}</span |
||||
|
> |
||||
|
</Label> |
||||
|
<Label size="L"> |
||||
|
Time: <span class={responseSuccess ? "green" : "red"} |
||||
|
>{response?.info.time}</span |
||||
|
> |
||||
|
</Label> |
||||
|
<Label size="L"> |
||||
|
Size: <span class={responseSuccess ? "green" : "red"} |
||||
|
>{response?.info.size}</span |
||||
|
> |
||||
|
</Label> |
||||
|
<Button disabled={!responseSuccess} cta on:click={saveQuery} |
||||
|
>Save query</Button |
||||
|
> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</Tabs> |
||||
|
{/if} |
||||
|
</Layout> |
||||
|
</div> |
||||
|
</div> |
||||
|
{/if} |
||||
|
|
||||
|
<style> |
||||
|
.inner { |
||||
|
width: 960px; |
||||
|
margin: 0 auto; |
||||
|
height: 100%; |
||||
|
} |
||||
|
.table { |
||||
|
width: 960px; |
||||
|
} |
||||
|
.url-block { |
||||
|
display: flex; |
||||
|
gap: var(--spacing-s); |
||||
|
} |
||||
|
.verb { |
||||
|
flex: 1; |
||||
|
} |
||||
|
.url { |
||||
|
flex: 4; |
||||
|
} |
||||
|
.top { |
||||
|
min-height: 50%; |
||||
|
} |
||||
|
.bottom { |
||||
|
padding-bottom: 50px; |
||||
|
} |
||||
|
.stats { |
||||
|
display: flex; |
||||
|
gap: var(--spacing-xl); |
||||
|
margin-left: auto !important; |
||||
|
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; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,37 @@ |
|||||
|
import { writable } from "svelte/store" |
||||
|
import api from "builderStore/api" |
||||
|
|
||||
|
export function createFlagsStore() { |
||||
|
const { subscribe, set } = writable({}) |
||||
|
|
||||
|
return { |
||||
|
subscribe, |
||||
|
fetch: async () => { |
||||
|
const { doc, response } = await getFlags() |
||||
|
set(doc) |
||||
|
return response |
||||
|
}, |
||||
|
updateFlag: async (flag, value) => { |
||||
|
const response = await api.post("/api/users/flags", { |
||||
|
flag, |
||||
|
value, |
||||
|
}) |
||||
|
if (response.status === 200) { |
||||
|
const { doc } = await getFlags() |
||||
|
set(doc) |
||||
|
} |
||||
|
return response |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function getFlags() { |
||||
|
const response = await api.get("/api/users/flags") |
||||
|
let doc = {} |
||||
|
if (response.status === 200) { |
||||
|
doc = await response.json() |
||||
|
} |
||||
|
return { doc, response } |
||||
|
} |
||||
|
|
||||
|
export const flags = createFlagsStore() |
||||
@ -0,0 +1,19 @@ |
|||||
|
import { writable } from "svelte/store" |
||||
|
import api from "builderStore/api" |
||||
|
|
||||
|
export function templatesStore() { |
||||
|
const { subscribe, set } = writable([]) |
||||
|
|
||||
|
async function load() { |
||||
|
const response = await api.get("/api/templates?type=app") |
||||
|
const json = await response.json() |
||||
|
set(json) |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
subscribe, |
||||
|
load, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const templates = templatesStore() |
||||
@ -1,6 +1,6 @@ |
|||||
export interface IntegrationBase { |
export interface IntegrationBase { |
||||
create?(query: any): Promise<any[]> |
create?(query: any): Promise<any[] | any> |
||||
read?(query: any): Promise<any[]> |
read?(query: any): Promise<any[] | any> |
||||
update?(query: any): Promise<any[]> |
update?(query: any): Promise<any[] | any> |
||||
delete?(query: any): Promise<any[]> |
delete?(query: any): Promise<any[] | any> |
||||
} |
} |
||||
|
|||||
File diff suppressed because it is too large
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue