Files
windmill/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanelDataSource.svelte
T

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>