212 lines
5.6 KiB
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>
|