budibase/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte

212 lines
5.6 KiB
Svelte

<script lang="ts">
import { JsonFormatter } from "@budibase/frontend-core"
import { Icon, ProgressCircle, notifications } from "@budibase/bbui"
import { Helpers } from "@budibase/bbui"
import { fade } from "svelte/transition"
import { UserScriptError } from "@budibase/string-templates"
import type { Log } from "@budibase/string-templates"
import type { JSONValue } from "@budibase/types"
// this can be essentially any primitive response from the JS function
export let expressionResult: JSONValue | undefined = undefined
export let expressionError: string | undefined = undefined
export let expressionLogs: Log[] = []
export let evaluating = false
export let expression: string | null = null
$: error = expressionError != null
$: empty = expression == null || expression?.trim() === ""
$: success = !error && !empty
$: highlightedResult = highlight(expressionResult)
$: highlightedLogs = expressionLogs.map(l => ({
log: l.log.map(part => highlight(part)).join(", "),
line: l.line,
type: l.type,
}))
const formatError = (err: any) => {
if (err.code === UserScriptError.code) {
return err.userScriptError.toString()
}
return err.toString()
}
// json can be any primitive type
const highlight = (json?: JSONValue | null) => {
if (json == null) {
return ""
}
// Attempt to parse and then stringify, in case this is valid result
try {
json = JSON.stringify(JSON.parse(json as any), null, 2)
} catch (err) {
// couldn't parse/stringify, just treat it as the raw input
}
return JsonFormatter.format(json, {
keyColor: "#e06c75",
numberColor: "#e5c07b",
stringColor: "#98c379",
trueColor: "#d19a66",
falseColor: "#d19a66",
nullColor: "#c678dd",
})
}
const copy = () => {
let clipboardVal = expressionResult
if (typeof clipboardVal === "object") {
clipboardVal = JSON.stringify(clipboardVal, null, 2)
}
Helpers.copyToClipboard(clipboardVal)
notifications.success("Value copied to clipboard")
}
</script>
<div class="evaluation-side-panel">
<div class="header" class:success class:error>
<div class="header-content">
{#if error}
<Icon name="Alert" color="var(--error-content)" />
<div>Error</div>
{#if evaluating}
<div transition:fade|local={{ duration: 130 }}>
<ProgressCircle size="S" />
</div>
{/if}
<span />
<Icon name="Copy" size="S" hoverable on:click={copy} />
{:else}
<div>Preview</div>
{#if evaluating}
<div transition:fade|local={{ duration: 130 }}>
<ProgressCircle size="S" />
</div>
{/if}
<span />
{#if !empty}
<Icon name="Copy" newStyles size="S" hoverable on:click={copy} />
{/if}
{/if}
</div>
</div>
<div class="body">
{#if empty}
Your expression will be evaluated here
{:else if error}
<div class="error-msg">
{formatError(expressionError)}
</div>
{:else}
<div class="output-lines">
{#each highlightedLogs as logLine}
<div
class="line"
class:error-log={logLine.type === "error"}
class:warn-log={logLine.type === "warn"}
>
<div class="icon-log">
{#if logLine.type === "error"}
<Icon
size="XS"
name="CloseCircle"
color="var(--error-content)"
/>
{:else if logLine.type === "warn"}
<Icon size="XS" name="Alert" color="var(--warning-content)" />
{/if}
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
<span>{@html logLine.log}</span>
</div>
{#if logLine.line}
<span style="color: var(--blue); overflow-wrap: normal;"
>:{logLine.line}</span
>
{/if}
</div>
{/each}
<div class="line">
<div>
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
{@html highlightedResult}
</div>
</div>
</div>
{/if}
</div>
</div>
<style>
.evaluation-side-panel {
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
border-left: var(--border-light);
}
.header {
padding: var(--spacing-m) var(--spacing-l);
flex: 0 0 auto;
position: relative;
border-bottom: var(--border-light);
}
.header-content {
height: var(--spectrum-alias-item-height-m);
display: flex;
align-items: center;
z-index: 2;
position: relative;
gap: var(--spacing-m);
}
.header-content span {
flex: 1 1 auto;
}
.header.error::before {
content: "";
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1;
position: absolute;
}
.header.error::before {
background: var(--error-bg);
}
.error-msg {
padding-top: var(--spacing-m);
}
.body {
flex: 1 1 auto;
font-family: var(--font-mono);
margin: 0 var(--spacing-m);
font-size: 12px;
overflow-y: auto;
overflow-x: hidden;
word-wrap: anywhere;
height: 0;
}
.output-lines {
display: flex;
flex-direction: column;
}
.line {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: end;
padding: var(--spacing-m) 0;
word-wrap: anywhere;
}
.line:not(:first-of-type) {
border-top: var(--border-light);
}
.icon-log {
display: flex;
gap: var(--spacing-s);
align-items: start;
}
</style>