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", "label": "Rows",
"key": "rows" "key": "rows"
}, },
{ {
"label": "Extra Info", "label": "Extra Info",
"key": "info" "key": "info"
}, },

View File

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

View File

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

View File

@ -3,34 +3,40 @@
import { Layout, Heading, Tabs, Tab, Icon } from "@budibase/bbui" import { Layout, Heading, Tabs, Tab, Icon } from "@budibase/bbui"
import DevToolsStatsTab from "./DevToolsStatsTab.svelte" import DevToolsStatsTab from "./DevToolsStatsTab.svelte"
import DevToolsComponentTab from "./DevToolsComponentTab.svelte" import DevToolsComponentTab from "./DevToolsComponentTab.svelte"
import { devToolsStore } from "../../stores" import { devToolsStore } from "stores"
const context = getContext("context") const context = getContext("context")
</script> </script>
<div class="devtools" class:mobile={$context.device.mobile}> <div
<Layout noPadding gap="XS"> class="devtools"
<div class="header"> class:hidden={!$devToolsStore.visible}
<Heading size="XS">Budibase DevTools</Heading> class:mobile={$context.device.mobile}
<Icon >
hoverable {#if $devToolsStore.visible}
name="Close" <Layout noPadding gap="XS">
on:click={() => devToolsStore.actions.setVisible(false)} <div class="header">
/> <Heading size="XS">Budibase DevTools</Heading>
</div> <Icon
<Tabs selected="Application"> hoverable
<Tab title="Application"> name="Close"
<div class="tab-content"> on:click={() => devToolsStore.actions.setVisible(false)}
<DevToolsStatsTab /> />
</div> </div>
</Tab> <Tabs selected="Application">
<Tab title="Components"> <Tab title="Application">
<div class="tab-content"> <div class="tab-content">
<DevToolsComponentTab /> <DevToolsStatsTab />
</div> </div>
</Tab> </Tab>
</Tabs> <Tab title="Components">
</Layout> <div class="tab-content">
<DevToolsComponentTab />
</div>
</Tab>
</Tabs>
</Layout>
{/if}
</div> </div>
<style> <style>
@ -39,6 +45,11 @@
flex: 0 0 320px; flex: 0 0 320px;
border-left: 1px solid var(--spectrum-global-color-gray-300); border-left: 1px solid var(--spectrum-global-color-gray-300);
overflow: auto; overflow: auto;
transition: margin-right 300ms ease;
margin-right: 0;
}
.devtools.hidden {
margin-right: -320px;
} }
.devtools.mobile { .devtools.mobile {
display: none; 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> <script>
import { import { Body, Layout, Heading, Button, Tabs, Tab } from "@budibase/bbui"
Body,
Layout,
Heading,
Button,
TextArea,
Tabs,
Tab,
Toggle,
} from "@budibase/bbui"
import { builderStore, devToolsStore, componentStore } from "stores" import { builderStore, devToolsStore, componentStore } from "stores"
import DevToolsStat from "./DevToolsStat.svelte" import DevToolsStat from "./DevToolsStat.svelte"
import { getSettingsDefinition } from "utils/componentProps.js" import DevToolsComponentSettingsTab from "./DevToolsComponentSettingsTab.svelte"
import DevToolsComponentContextTab from "./DevToolsComponentContextTab.svelte"
let showEnrichedSettings = true
$: selectedInstance = $componentStore.selectedComponentInstance
$: settingsDefinition = getSettingsDefinition(
$componentStore.selectedComponentDefinition
)
$: rawSettings = selectedInstance?.getRawSettings()
$: settings = selectedInstance?.getSettings()
$: { $: {
if (!selectedInstance) { // Reset selection store if we can't find a matching instance
if (!$componentStore.selectedComponentInstance) {
builderStore.actions.selectComponent(null) builderStore.actions.selectComponent(null)
} }
} }
@ -46,17 +30,19 @@
</div> </div>
</Layout> </Layout>
{:else} {:else}
<Layout noPadding> <Layout noPadding gap="S">
<Heading size="XS">
{$componentStore.selectedComponent?._instanceName}
</Heading>
<Layout noPadding gap="XS"> <Layout noPadding gap="XS">
<DevToolsStat
label="Component"
value={$componentStore.selectedComponent?._instanceName}
/>
<DevToolsStat <DevToolsStat
label="Type" label="Type"
value={$componentStore.selectedComponentDefinition?.name} value={$componentStore.selectedComponentDefinition?.name}
/> />
<DevToolsStat label="ID" value={$componentStore.selectedComponent?._id} /> <DevToolsStat
label="Component ID"
value={$componentStore.selectedComponent?._id}
/>
</Layout> </Layout>
<div class="buttons"> <div class="buttons">
<Button <Button
@ -79,37 +65,12 @@
<Tabs selected="Settings"> <Tabs selected="Settings">
<Tab title="Settings"> <Tab title="Settings">
<div class="tab-content"> <div class="tab-content">
<Layout noPadding gap="S"> <DevToolsComponentSettingsTab />
<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>
</div> </div>
</Tab> </Tab>
<Tab title="Context"> <Tab title="Bindings">
<div class="tab-content"> <div class="tab-content">
<TextArea <DevToolsComponentContextTab />
readonly
label="Data context"
value={JSON.stringify(
selectedInstance?.getDataContext(),
null,
2
)}
/>
</div> </div>
</Tab> </Tab>
</Tabs> </Tabs>

View File

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

View File

@ -1,12 +1,39 @@
<script> <script>
import { Helpers } from "@budibase/bbui"
import { notificationStore } from "stores"
export let label export let label
export let value 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> </script>
<div class="stat"> <div class="stat">
<div class="stat-label">{label}</div> <div class="stat-label" title={prettyLabel}>{prettyLabel}</div>
<div class="stat-value" title={value} on:click={() => console.log(value)}> <div
{value == null ? " " : value} class="stat-value"
class:copyable={canCopy}
class:empty
title={prettyValue}
on:click={canCopy ? copyValue : null}
>
{prettyValue}
</div> </div>
</div> </div>
@ -23,7 +50,7 @@
color: var(--spectrum-global-color-gray-600); color: var(--spectrum-global-color-gray-600);
text-transform: uppercase; text-transform: uppercase;
flex: 0 0 auto; flex: 0 0 auto;
max-width: 140px; width: 120px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -35,5 +62,14 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; 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> </style>

View File

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

View File

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