mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 16:03:27 +00:00
466 lines
11 KiB
Svelte
466 lines
11 KiB
Svelte
<script lang="ts">
|
|
import type { AppComponent } from '../component'
|
|
|
|
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
|
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
|
import PlotlyRichEditor from './PlotlyRichEditor.svelte'
|
|
import ChartJSRichEditor from './ChartJSRichEditor.svelte'
|
|
import AGChartRichEditor from './AGChartRichEditor.svelte'
|
|
import { getContext, onMount } from 'svelte'
|
|
import type { AppViewerContext, RichConfiguration } from '../../types'
|
|
import type { InputConnectionEval } from '../../inputType'
|
|
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
|
|
|
|
export let component: AppComponent
|
|
let convertToUIEditorCallback: (() => void) | undefined = undefined
|
|
|
|
let selected = 'ui-editor'
|
|
let renderCount = 0
|
|
|
|
onMount(() => {
|
|
if (
|
|
(component.type === 'plotlycomponentv2' ||
|
|
component.type === 'chartjscomponentv2' ||
|
|
component.type === 'agchartscomponent' ||
|
|
component.type === 'agchartscomponentee') &&
|
|
component.componentInput === undefined &&
|
|
component.datasets === undefined
|
|
) {
|
|
setUpUIEditor()
|
|
renderCount++
|
|
} else if (component.componentInput !== undefined) {
|
|
selected = 'json'
|
|
}
|
|
})
|
|
|
|
const { worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
|
interface Dataset {
|
|
value: any // Define more specific type if possible
|
|
name: string
|
|
aggregation_method: string
|
|
type: string
|
|
tooltip: string
|
|
color: string
|
|
}
|
|
|
|
function resolveConfiguration(
|
|
x: RichConfiguration | undefined,
|
|
connections: InputConnectionEval[]
|
|
): string | undefined {
|
|
if (!x) return undefined
|
|
if (x.type === 'evalv2') {
|
|
connections.push(...x.connections)
|
|
return x.expr
|
|
}
|
|
if (x.type === 'static') return `[${x.value}]`
|
|
return undefined
|
|
}
|
|
|
|
function handleSelected(selected: string) {
|
|
if (component.type === 'plotlycomponentv2') {
|
|
if (selected === 'ui-editor') {
|
|
convertToUIEditorCallback = () => {
|
|
component.componentInput = undefined
|
|
setUpUIEditor()
|
|
}
|
|
|
|
setTimeout(() => {
|
|
const activeElement = document.activeElement as HTMLElement
|
|
activeElement?.blur()
|
|
document.body.focus()
|
|
})
|
|
} else if (selected === 'json') {
|
|
convertToJson()
|
|
}
|
|
} else if (component.type === 'chartjscomponentv2') {
|
|
if (selected === 'ui-editor') {
|
|
convertToUIEditorCallback = () => {
|
|
component.componentInput = undefined
|
|
setUpUIEditor()
|
|
}
|
|
|
|
setTimeout(() => {
|
|
const activeElement = document.activeElement as HTMLElement
|
|
activeElement?.blur()
|
|
document.body.focus()
|
|
})
|
|
} else if (selected === 'json') {
|
|
convertChartJSToJson()
|
|
}
|
|
} else if (isAgChartsComponent()) {
|
|
if (selected === 'ui-editor') {
|
|
convertToUIEditorCallback = () => {
|
|
component.componentInput = undefined
|
|
setUpUIEditor()
|
|
}
|
|
|
|
setTimeout(() => {
|
|
const activeElement = document.activeElement as HTMLElement
|
|
activeElement?.blur()
|
|
document.body.focus()
|
|
})
|
|
} else if (selected === 'json') {
|
|
convertAgChartToJson()
|
|
}
|
|
}
|
|
}
|
|
|
|
function convertAgChartToJson() {
|
|
if (component.type !== 'agchartscomponent' && component.type !== 'agchartscomponentee') {
|
|
return
|
|
}
|
|
|
|
const connections: InputConnectionEval[] = []
|
|
|
|
if (component.datasets === undefined || component.datasets.type !== 'static') return
|
|
|
|
const datasetsAsString = datasetToAgChartJson()
|
|
|
|
component.componentInput = {
|
|
type: 'evalv2',
|
|
fieldType: 'object',
|
|
noStatic: true,
|
|
expr: datasetsAsString,
|
|
connections: connections.filter(Boolean)
|
|
}
|
|
|
|
component.datasets = undefined
|
|
component.xData = undefined
|
|
}
|
|
|
|
function setUpUIEditor() {
|
|
if (component.type === 'plotlycomponentv2') {
|
|
component.datasets = createPlotlyComponentDataset()
|
|
component.xData = createXData()
|
|
} else if (component.type === 'chartjscomponentv2') {
|
|
component.datasets = createChartjsComponentDataset()
|
|
component.xData = createXData()
|
|
} else if (component.type === 'agchartscomponent' || component.type === 'agchartscomponentee') {
|
|
component.datasets = createAgChartsComponentDataset()
|
|
component.xData = createXData()
|
|
}
|
|
}
|
|
|
|
function createAgChartsComponentDataset(): RichConfiguration {
|
|
return {
|
|
type: 'static',
|
|
fieldType: 'array',
|
|
subFieldType: 'ag-chart',
|
|
|
|
value: [
|
|
{
|
|
value: {
|
|
type: 'oneOf',
|
|
selected: 'bar',
|
|
labels: {
|
|
bar: 'Bar',
|
|
scatter: 'Scatter',
|
|
line: 'Line',
|
|
area: 'Area',
|
|
['range-bar']: 'Range Bar',
|
|
['range-area']: 'Range Area'
|
|
},
|
|
configuration: {
|
|
bar: {
|
|
value: {
|
|
type: 'static',
|
|
fieldType: 'array',
|
|
subFieldType: 'number',
|
|
value: [25, 25, 50]
|
|
}
|
|
},
|
|
scatter: {
|
|
value: {
|
|
type: 'static',
|
|
fieldType: 'array',
|
|
subFieldType: 'number',
|
|
value: [25, 25, 50]
|
|
}
|
|
},
|
|
line: {
|
|
value: {
|
|
type: 'static',
|
|
fieldType: 'array',
|
|
subFieldType: 'number',
|
|
value: [25, 25, 50]
|
|
}
|
|
},
|
|
area: {
|
|
value: {
|
|
type: 'static',
|
|
fieldType: 'array',
|
|
subFieldType: 'number',
|
|
value: [25, 25, 50]
|
|
}
|
|
},
|
|
['range-bar']: {
|
|
value: {
|
|
type: 'static',
|
|
fieldType: 'array',
|
|
subFieldType: 'number-tuple',
|
|
value: [
|
|
[10, 15],
|
|
[20, 25],
|
|
[18, 27]
|
|
]
|
|
}
|
|
},
|
|
['range-area']: {
|
|
value: {
|
|
type: 'static',
|
|
fieldType: 'array',
|
|
subFieldType: 'number-tuple',
|
|
value: [
|
|
[10, 15],
|
|
[20, 25],
|
|
[18, 27]
|
|
]
|
|
}
|
|
}
|
|
}
|
|
} as const,
|
|
name: 'Dataset 1',
|
|
type: 'bar'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
function createChartjsComponentDataset(): RichConfiguration {
|
|
return {
|
|
type: 'static',
|
|
fieldType: 'array',
|
|
subFieldType: 'chartjs',
|
|
value: [
|
|
{
|
|
value: {
|
|
type: 'static',
|
|
fieldType: 'array',
|
|
subFieldType: 'number',
|
|
value: [25, 25, 50]
|
|
},
|
|
name: 'Dataset 1'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
function createPlotlyComponentDataset(): RichConfiguration {
|
|
return {
|
|
type: 'static',
|
|
fieldType: 'array',
|
|
subFieldType: 'plotly',
|
|
value: [
|
|
{
|
|
value: {
|
|
type: 'static',
|
|
fieldType: 'array',
|
|
subFieldType: 'number',
|
|
value: [1, 2, 3, 4]
|
|
},
|
|
name: 'Dataset 1',
|
|
aggregation_method: 'sum',
|
|
type: 'bar',
|
|
tooltip: '',
|
|
color: '#C8A2C8'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
function createXData(): RichConfiguration {
|
|
return {
|
|
type: 'evalv2',
|
|
fieldType: 'array',
|
|
expr: '[1, 2, 3, 4]',
|
|
connections: []
|
|
}
|
|
}
|
|
|
|
function isAgChartsComponent(): boolean {
|
|
return component.type === 'agchartscomponent' || component.type === 'agchartscomponentee'
|
|
}
|
|
|
|
function convertToJson() {
|
|
if (
|
|
component.type !== 'plotlycomponentv2' &&
|
|
component.type !== 'agchartscomponent' &&
|
|
component.type !== 'agchartscomponentee'
|
|
) {
|
|
return
|
|
}
|
|
|
|
const connections: InputConnectionEval[] = []
|
|
const xDataResolved = resolveConfiguration(component.xData, connections)
|
|
|
|
if (component.datasets === undefined || component.datasets.type !== 'static') return
|
|
|
|
const datasets = component.datasets?.value
|
|
|
|
const datasetsAsString =
|
|
datasets
|
|
.map((rawDataset: Dataset) => datasetToJson(rawDataset, xDataResolved, connections))
|
|
.join(',\n') || ''
|
|
|
|
component.componentInput = {
|
|
type: 'evalv2',
|
|
fieldType: 'object',
|
|
noStatic: true,
|
|
expr: `[\n${datasetsAsString}\n]`,
|
|
connections: connections.filter(Boolean)
|
|
}
|
|
|
|
component.datasets = undefined
|
|
component.xData = undefined
|
|
}
|
|
|
|
function convertChartJSToJson() {
|
|
if (component.type !== 'chartjscomponentv2') {
|
|
return
|
|
}
|
|
|
|
const connections: InputConnectionEval[] = []
|
|
const xDataResolved = resolveConfiguration(component.xData, connections)
|
|
|
|
if (component.datasets === undefined || component.datasets.type !== 'static') return
|
|
|
|
const datasets = component.datasets?.value
|
|
|
|
component.componentInput = {
|
|
type: 'evalv2',
|
|
fieldType: 'object',
|
|
noStatic: true,
|
|
expr: `({
|
|
\t"labels": ${xDataResolved},
|
|
\t"datasets": [\n${
|
|
datasets
|
|
.map(
|
|
(rawDataset: Dataset) => `\t\t{
|
|
\t\t\tdata:${resolveConfiguration(rawDataset.value, connections)},
|
|
\t\t\tlabel: "${rawDataset.name}"
|
|
\t\t}`
|
|
)
|
|
.join(',\n') || ''
|
|
}\n\t]
|
|
})`,
|
|
connections: connections.filter(Boolean)
|
|
}
|
|
|
|
component.datasets = undefined
|
|
component.xData = undefined
|
|
}
|
|
|
|
function datasetToJson(
|
|
dataset: Dataset,
|
|
xDataExpr: string | undefined,
|
|
connections: InputConnectionEval[]
|
|
): string {
|
|
return `\t{
|
|
\t\t"type": "${dataset.type}",
|
|
\t\t"x": ${xDataExpr},
|
|
\t\t"y": ${resolveConfiguration(dataset.value, connections)},
|
|
\t\t"text": "${dataset.tooltip || ''}",
|
|
\t\t"aggregation_method": "${dataset.aggregation_method}",
|
|
\t\t"marker": {
|
|
\t\t\t"color": "${dataset.color}"
|
|
\t\t}
|
|
\t}`
|
|
}
|
|
|
|
function datasetToAgChartJson(): string {
|
|
const outputs = $worldStore.outputsById[component.id]
|
|
const result = outputs?.result.peak()
|
|
|
|
if (!result.data && !result.series) {
|
|
throw new Error('Invalid result')
|
|
}
|
|
|
|
return (
|
|
'(' +
|
|
JSON.stringify(
|
|
{
|
|
data: result.data,
|
|
series: result.series
|
|
},
|
|
null,
|
|
'\t'
|
|
) +
|
|
')'
|
|
)
|
|
}
|
|
</script>
|
|
|
|
{#if component.type === 'plotlycomponentv2' || component.type === 'chartjscomponentv2' || component.type === 'agchartscomponent' || component.type === 'agchartscomponentee'}
|
|
<div class="p-2">
|
|
<ToggleButtonGroup
|
|
bind:selected
|
|
on:selected={() => {
|
|
handleSelected(selected)
|
|
}}
|
|
>
|
|
<ToggleButton
|
|
value="ui-editor"
|
|
label="UI Editor"
|
|
tooltip="Use the UI editor to quickly create a plotly chart."
|
|
/>
|
|
<ToggleButton
|
|
value="json"
|
|
label="JSON"
|
|
tooltip="Switch to JSON mode for complete customization of Plotly settings."
|
|
/>
|
|
</ToggleButtonGroup>
|
|
</div>
|
|
|
|
{#if selected === 'ui-editor'}
|
|
{#key renderCount}
|
|
{#if component.type === 'plotlycomponentv2'}
|
|
<PlotlyRichEditor
|
|
id={component.id}
|
|
bind:datasets={component.datasets}
|
|
bind:xData={component.xData}
|
|
/>
|
|
{:else if isAgChartsComponent()}
|
|
<AGChartRichEditor
|
|
id={component.id}
|
|
bind:datasets={component.datasets}
|
|
bind:xData={component.xData}
|
|
/>
|
|
{:else if component.type === 'chartjscomponentv2'}
|
|
<ChartJSRichEditor
|
|
id={component.id}
|
|
bind:datasets={component.datasets}
|
|
bind:xData={component.xData}
|
|
/>
|
|
{/if}
|
|
{/key}
|
|
{:else}
|
|
<slot />
|
|
{/if}
|
|
{:else}
|
|
<slot />
|
|
{/if}
|
|
|
|
<ConfirmationModal
|
|
open={Boolean(convertToUIEditorCallback)}
|
|
title="Convert to UI Editor"
|
|
confirmationText="Remove"
|
|
on:canceled={() => {
|
|
convertToUIEditorCallback = undefined
|
|
selected = 'json'
|
|
}}
|
|
on:confirmed={() => {
|
|
if (convertToUIEditorCallback) {
|
|
convertToUIEditorCallback()
|
|
}
|
|
convertToUIEditorCallback = undefined
|
|
selected = 'ui-editor'
|
|
}}
|
|
>
|
|
<div class="flex flex-col w-full space-y-4">
|
|
<span>
|
|
Are you sure you want to convert this component to the UI Editor? The UI Editor does not
|
|
support all the features of the JSON editor.
|
|
</span>
|
|
</div>
|
|
</ConfirmationModal>
|