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> |
|||
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) { |
|||
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) { |
|||
$goto(`../rest/${$params.query}`) |
|||
} |
|||
</script> |
|||
|
|||
<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 { |
|||
create?(query: any): Promise<any[]> |
|||
read?(query: any): Promise<any[]> |
|||
update?(query: any): Promise<any[]> |
|||
delete?(query: any): Promise<any[]> |
|||
create?(query: any): Promise<any[] | any> |
|||
read?(query: any): Promise<any[] | any> |
|||
update?(query: any): Promise<any[] | 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