Add full featured component settings and bindings devtools tabs

This commit is contained in:
Andrew Kingston 2022-02-24 21:48:54 +00:00
parent 969e6531d5
commit 755e12a24d
11 changed files with 276 additions and 99 deletions

View File

@ -3373,7 +3373,7 @@
"label": "Rows",
"key": "rows"
},
{
{
"label": "Extra Info",
"key": "info"
},

View File

@ -142,7 +142,7 @@
<PeekScreenDisplay />
</CustomThemeWrapper>
{#if $devToolsStore.visible && !$builderStore.inBuilder}
{#if $appStore.isDevApp && !$builderStore.inBuilder}
<DevTools />
{/if}
</div>

View File

@ -17,7 +17,7 @@
propsAreSame,
getSettingsDefinition,
} from "utils/componentProps"
import { builderStore, devToolsStore, componentStore } from "stores"
import { builderStore, devToolsStore, componentStore, appStore } from "stores"
import { Helpers } from "@budibase/bbui"
import Manifest from "manifest.json"
import { getActiveConditions, reduceConditionActions } from "utils/conditions"
@ -349,15 +349,25 @@
}
onMount(() => {
componentStore.actions.registerInstance(id, {
getSettings: () => cachedSettings,
getRawSettings: () => ({ ...staticSettings, ...dynamicSettings }),
getDataContext: () => get(context),
})
if (
$appStore.isDevApp &&
!componentStore.actions.isComponentRegistered(id)
) {
componentStore.actions.registerInstance(id, {
getSettings: () => cachedSettings,
getRawSettings: () => ({ ...staticSettings, ...dynamicSettings }),
getDataContext: () => get(context),
})
}
})
onDestroy(() => {
componentStore.actions.unregisterInstance(id)
if (
$appStore.isDevApp &&
componentStore.actions.isComponentRegistered(id)
) {
componentStore.actions.unregisterInstance(id)
}
})
</script>

View File

@ -3,34 +3,40 @@
import { Layout, Heading, Tabs, Tab, Icon } from "@budibase/bbui"
import DevToolsStatsTab from "./DevToolsStatsTab.svelte"
import DevToolsComponentTab from "./DevToolsComponentTab.svelte"
import { devToolsStore } from "../../stores"
import { devToolsStore } from "stores"
const context = getContext("context")
</script>
<div class="devtools" class:mobile={$context.device.mobile}>
<Layout noPadding gap="XS">
<div class="header">
<Heading size="XS">Budibase DevTools</Heading>
<Icon
hoverable
name="Close"
on:click={() => devToolsStore.actions.setVisible(false)}
/>
</div>
<Tabs selected="Application">
<Tab title="Application">
<div class="tab-content">
<DevToolsStatsTab />
</div>
</Tab>
<Tab title="Components">
<div class="tab-content">
<DevToolsComponentTab />
</div>
</Tab>
</Tabs>
</Layout>
<div
class="devtools"
class:hidden={!$devToolsStore.visible}
class:mobile={$context.device.mobile}
>
{#if $devToolsStore.visible}
<Layout noPadding gap="XS">
<div class="header">
<Heading size="XS">Budibase DevTools</Heading>
<Icon
hoverable
name="Close"
on:click={() => devToolsStore.actions.setVisible(false)}
/>
</div>
<Tabs selected="Application">
<Tab title="Application">
<div class="tab-content">
<DevToolsStatsTab />
</div>
</Tab>
<Tab title="Components">
<div class="tab-content">
<DevToolsComponentTab />
</div>
</Tab>
</Tabs>
</Layout>
{/if}
</div>
<style>
@ -39,6 +45,11 @@
flex: 0 0 320px;
border-left: 1px solid var(--spectrum-global-color-gray-300);
overflow: auto;
transition: margin-right 300ms ease;
margin-right: 0;
}
.devtools.hidden {
margin-right: -320px;
}
.devtools.mobile {
display: none;

View File

@ -0,0 +1,112 @@
<script>
import { Layout, Select, Body } from "@budibase/bbui"
import { componentStore } from "stores/index.js"
import DevToolsStat from "./DevToolsStat.svelte"
const ReadableBindingMap = {
user: "Current user",
state: "State",
url: "URL",
device: "Device",
}
let category
$: selectedInstance = $componentStore.selectedComponentInstance
$: context = selectedInstance?.getDataContext()
$: bindingCategories = getContextProviders(context)
$: bindings = Object.entries(context?.[category] || {})
const getContextProviders = context => {
const filteredContext = { ...context }
// Remove some keys from context
delete filteredContext.key
delete filteredContext.closestComponentId
delete filteredContext.user_RefreshDataSource
// Keep track of encountered IDs so we can find actions
let actions = []
let encounteredCategories = []
// Create readable bindings
let categories = []
Object.keys(filteredContext)
.sort()
.forEach(category => {
let isAction = false
for (let cat of encounteredCategories) {
if (category.startsWith(`${cat}_`)) {
isAction = true
break
}
}
if (isAction) {
actions.push(category)
return
}
// Mark category as encountered so we can find any matching actions
encounteredCategories.push(category)
// Map any static categories to pretty names
if (ReadableBindingMap[category]) {
categories.push({
label: ReadableBindingMap[category],
value: category,
})
} else {
const component = componentStore.actions.getComponentById(category)
if (component) {
categories.push({
label: component._instanceName,
value: category,
})
} else {
// Check if its a block
if (category.includes("-")) {
const split = category.split("-")
const potentialId = split[0]
const component =
componentStore.actions.getComponentById(potentialId)
if (component) {
categories.push({
label: `${component._instanceName} (${split[1]})`,
value: category,
})
return
}
}
// Otherwise we don't know
categories.push({
label: "Unknown - " + category,
value: category,
})
}
}
})
return categories
}
</script>
<Layout noPadding gap="S">
<Body size="S">
Choose a category to see the value of all its available bindings.
</Body>
<Select bind:value={category} label="Category" options={bindingCategories} />
{#if bindings?.length}
<Layout noPadding gap="XS">
{#each bindings as binding}
<DevToolsStat
copyable
label={binding[0]}
value={JSON.stringify(binding[1])}
/>
{/each}
</Layout>
{:else if category}
<Body size="XS">There aren't any bindings available in this category.</Body>
{/if}
</Layout>

View File

@ -0,0 +1,30 @@
<script>
import { Layout, Toggle } from "@budibase/bbui"
import DevToolsStat from "./DevToolsStat.svelte"
import { componentStore } from "stores/index.js"
import { getSettingsDefinition } from "utils/componentProps.js"
let showEnrichedSettings = true
$: selectedInstance = $componentStore.selectedComponentInstance
$: settingsDefinition = getSettingsDefinition(
$componentStore.selectedComponentDefinition
)
$: rawSettings = selectedInstance?.getRawSettings()
$: settings = selectedInstance?.getSettings()
</script>
<Layout noPadding gap="S">
<Toggle text="Show enriched settings" bind:value={showEnrichedSettings} />
<Layout noPadding gap="XS">
{#each settingsDefinition as setting}
<DevToolsStat
copyable
label={setting.label}
value={JSON.stringify(
(showEnrichedSettings ? settings : rawSettings)?.[setting.key]
)}
/>
{/each}
</Layout>
</Layout>

View File

@ -1,29 +1,13 @@
<script>
import {
Body,
Layout,
Heading,
Button,
TextArea,
Tabs,
Tab,
Toggle,
} from "@budibase/bbui"
import { Body, Layout, Heading, Button, Tabs, Tab } from "@budibase/bbui"
import { builderStore, devToolsStore, componentStore } from "stores"
import DevToolsStat from "./DevToolsStat.svelte"
import { getSettingsDefinition } from "utils/componentProps.js"
let showEnrichedSettings = true
$: selectedInstance = $componentStore.selectedComponentInstance
$: settingsDefinition = getSettingsDefinition(
$componentStore.selectedComponentDefinition
)
$: rawSettings = selectedInstance?.getRawSettings()
$: settings = selectedInstance?.getSettings()
import DevToolsComponentSettingsTab from "./DevToolsComponentSettingsTab.svelte"
import DevToolsComponentContextTab from "./DevToolsComponentContextTab.svelte"
$: {
if (!selectedInstance) {
// Reset selection store if we can't find a matching instance
if (!$componentStore.selectedComponentInstance) {
builderStore.actions.selectComponent(null)
}
}
@ -46,17 +30,19 @@
</div>
</Layout>
{:else}
<Layout noPadding>
<Layout noPadding gap="S">
<Heading size="XS">
{$componentStore.selectedComponent?._instanceName}
</Heading>
<Layout noPadding gap="XS">
<DevToolsStat
label="Component"
value={$componentStore.selectedComponent?._instanceName}
/>
<DevToolsStat
label="Type"
value={$componentStore.selectedComponentDefinition?.name}
/>
<DevToolsStat label="ID" value={$componentStore.selectedComponent?._id} />
<DevToolsStat
label="Component ID"
value={$componentStore.selectedComponent?._id}
/>
</Layout>
<div class="buttons">
<Button
@ -79,37 +65,12 @@
<Tabs selected="Settings">
<Tab title="Settings">
<div class="tab-content">
<Layout noPadding gap="S">
<Toggle
text="Show enriched settings"
bind:value={showEnrichedSettings}
/>
<Layout noPadding gap="XS">
{#each settingsDefinition as setting}
<DevToolsStat
label={setting.label}
value={JSON.stringify(
(showEnrichedSettings ? settings : rawSettings)?.[
setting.key
]
)}
/>
{/each}
</Layout>
</Layout>
<DevToolsComponentSettingsTab />
</div>
</Tab>
<Tab title="Context">
<Tab title="Bindings">
<div class="tab-content">
<TextArea
readonly
label="Data context"
value={JSON.stringify(
selectedInstance?.getDataContext(),
null,
2
)}
/>
<DevToolsComponentContextTab />
</div>
</Tab>
</Tabs>

View File

@ -44,9 +44,9 @@
quiet
overBackground
icon="Code"
on:click={() => devToolsStore.actions.setVisible(true)}
on:click={() => devToolsStore.actions.setVisible(!$devToolsStore.visible)}
>
Open DevTools
{$devToolsStore.visible ? "Close" : "Open"} DevTools
</Button>
{/if}
</div>

View File

@ -1,12 +1,39 @@
<script>
import { Helpers } from "@budibase/bbui"
import { notificationStore } from "stores"
export let label
export let value
export let copyable = false
$: prettyLabel = label == null ? "-" : label
$: prettyValue = value == null ? "-" : value
$: empty = value == null
$: canCopy = copyable && !empty
const copyValue = async () => {
try {
await Helpers.copyToClipboard(value)
notificationStore.actions.success("Copied to clipboard")
} catch (error) {
notificationStore.actions.error(
"Failed to copy to clipboard. Check the dev console for the value."
)
console.warn("Failed to copy the value", value)
}
}
</script>
<div class="stat">
<div class="stat-label">{label}</div>
<div class="stat-value" title={value} on:click={() => console.log(value)}>
{value == null ? " " : value}
<div class="stat-label" title={prettyLabel}>{prettyLabel}</div>
<div
class="stat-value"
class:copyable={canCopy}
class:empty
title={prettyValue}
on:click={canCopy ? copyValue : null}
>
{prettyValue}
</div>
</div>
@ -23,7 +50,7 @@
color: var(--spectrum-global-color-gray-600);
text-transform: uppercase;
flex: 0 0 auto;
max-width: 140px;
width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -35,5 +62,14 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color var(--spectrum-global-animation-duration-100, 130ms)
ease-in-out;
}
.stat-value.empty {
color: var(--spectrum-global-color-gray-500);
}
.stat-value.copyable:hover {
color: var(--spectrum-global-color-blue-600);
cursor: pointer;
}
</style>

View File

@ -1,6 +1,6 @@
<script>
import { Layout } from "@budibase/bbui"
import { authStore, appStore, screenStore } from "stores"
import { authStore, appStore, screenStore, componentStore } from "stores"
import DevToolsStat from "./DevToolsStat.svelte"
</script>
@ -21,6 +21,7 @@
label="Active screen"
value={$screenStore.activeScreen?.routing.route}
/>
<DevToolsStat label="Components" value={$componentStore.mountedComponents} />
<DevToolsStat label="User" value={$authStore.email} />
<DevToolsStat label="Role" value={$authStore.roleId} />
</Layout>

View File

@ -9,8 +9,8 @@ const createComponentStore = () => {
const store = writable({})
const derivedStore = derived(
[builderStore, devToolsStore, screenStore],
([$builderState, $devToolsState, $screenState]) => {
[store, builderStore, devToolsStore, screenStore],
([$store, $builderState, $devToolsState, $screenState]) => {
// Avoid any of this logic if we aren't in the builder preview
if (!$builderState.inBuilder && !$devToolsState.visible) {
return {}
@ -34,10 +34,12 @@ const createComponentStore = () => {
findComponentPathById(asset?.props, selectedComponentId) || []
return {
selectedComponentInstance: get(store)[selectedComponentId],
selectedComponentInstance: $store[selectedComponentId],
selectedComponent: component,
selectedComponentDefinition: definition,
selectedComponentPath: path?.map(component => component._id),
mountedComponents: Object.keys($store).length,
currentAsset: asset,
}
}
)
@ -56,9 +58,23 @@ const createComponentStore = () => {
})
}
const isComponentRegistered = id => {
return get(store)[id] != null
}
const getComponentById = id => {
const asset = get(derivedStore).currentAsset
return findComponentById(asset?.props, id)
}
return {
...derivedStore,
actions: { registerInstance, unregisterInstance },
actions: {
registerInstance,
unregisterInstance,
isComponentRegistered,
getComponentById,
},
}
}