feat(frontend): AG chart (#2972)

* fix(frontend): Fix decision tree (#2928)

* fix(frontend): wip

* fix(frontend): wip

* fix(frontend): decision tree history

* fix(frontend): fix wording

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): ag charts

* feat(frontend): remove todo

* feat(frontend): remove todo

* feat(frontend): revert

* feat(frontend): fix converstion

* feat(frontend): ag charts ee

* feat(frontend): fix build

* feat(frontend): fix convertion to json

* feat(frontend): fix naming

* feat(frontend): fix id collision

* feat(frontend): fix initial load

* feat(frontend): fix initial load
This commit is contained in:
Faton Ramadani
2024-01-10 17:52:22 +01:00
committed by GitHub
parent a44d42fa77
commit 0de6dbced7
21 changed files with 1033 additions and 216 deletions
+15
View File
@@ -14,6 +14,8 @@
"@popperjs/core": "^2.11.6",
"@redocly/json-to-json-schema": "^0.0.1",
"@tanstack/svelte-table": "^8.9.9",
"ag-charts-community": "^9.0.1",
"ag-charts-enterprise": "^9.0.1",
"ag-grid-community": "^31.0.0",
"ag-grid-enterprise": "^31.0.0",
"ansi_up": "^5.2.1",
@@ -1687,6 +1689,19 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/ag-charts-community": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/ag-charts-community/-/ag-charts-community-9.0.1.tgz",
"integrity": "sha512-FZy1tAWO10TzRheoM622/vJWRqldAYH3VGreQ8qirt7FB/ctIR4kjY9GPZywDJnVcb/wET3nrgwq2vIiFKhKBw=="
},
"node_modules/ag-charts-enterprise": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/ag-charts-enterprise/-/ag-charts-enterprise-9.0.1.tgz",
"integrity": "sha512-b8Lz6wgs/CSL1lz0PUnNod016Z5n7vus1mTM9GXhxJ3uyEYP0klwFyvgtK0Cjgc0YeFyhoVDmb38dMKIvHnUyg==",
"dependencies": {
"ag-charts-community": "9.0.1"
}
},
"node_modules/ag-grid-community": {
"version": "31.0.0",
"resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-31.0.0.tgz",
+2
View File
@@ -97,6 +97,8 @@
"@popperjs/core": "^2.11.6",
"@redocly/json-to-json-schema": "^0.0.1",
"@tanstack/svelte-table": "^8.9.9",
"ag-charts-community": "^9.0.1",
"ag-charts-enterprise": "^9.0.1",
"ag-grid-community": "^31.0.0",
"ag-grid-enterprise": "^31.0.0",
"ansi_up": "^5.2.1",
@@ -16,7 +16,7 @@
export let componentInput: AppInput | undefined
export let configuration: RichConfigurations
export let initializing: boolean | undefined = undefined
export let customCss: ComponentCustomCSS<'piechartcomponent'> | undefined = undefined
export let customCss: ComponentCustomCSS<'chartjscomponent'> | undefined = undefined
export let render: boolean
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
@@ -22,7 +22,7 @@
export let componentInput: AppInput | undefined
export let configuration: RichConfigurations
export let initializing: boolean | undefined = undefined
export let customCss: ComponentCustomCSS<'piechartcomponent'> | undefined = undefined
export let customCss: ComponentCustomCSS<'chartjscomponent'> | undefined = undefined
export let render: boolean
export let datasets: RichConfiguration | undefined
export let xData: RichConfiguration | undefined
@@ -0,0 +1,300 @@
<script lang="ts">
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
import type { AppInput } from '../../../inputType'
import type {
AppViewerContext,
ComponentCustomCSS,
RichConfiguration,
RichConfigurations
} from '../../../types'
import { initCss } from '../../../utils'
import { getContext, onMount } from 'svelte'
import { initConfig, initOutput } from '../../../editor/appUtils'
import { components } from '../../../editor/component'
import ResolveConfig from '../../helpers/ResolveConfig.svelte'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../../helpers/ResolveStyle.svelte'
import type { AgChartOptions, AgChartInstance } from 'ag-charts-community'
export let id: string
export let componentInput: AppInput | undefined
export let configuration: RichConfigurations
export let initializing: boolean | undefined = undefined
export let customCss: ComponentCustomCSS<'agchartscomponent'> | undefined = undefined
export let render: boolean
export let datasets: RichConfiguration | undefined
export let xData: RichConfiguration | undefined
export let license: string | undefined = undefined
export let ee: boolean = false
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
type Dataset = {
value: RichConfiguration
name: string
type: 'bar' | 'line' | 'scatter' | 'area' | 'range-bar'
}
let resolvedDatasets: Dataset[]
let resolvedXData: number[] = []
const outputs = initOutput($worldStore, id, {
result: undefined as
| {
data: any[]
series: any[]
}
| undefined,
loading: false
})
let result: undefined = undefined
const resolvedConfig = initConfig(
components['agchartscomponent'].initialData.configuration,
configuration
)
let css = initCss($app.css?.agchartscomponent, customCss)
let chartInstance: AgChartInstance | undefined = undefined
function updateChart() {
if (!chartInstance) {
return
}
let data = [] as any[]
for (let i = 0; i < resolvedXData.length; i++) {
const o = {
x: resolvedXData[i]
}
for (let j = 0; j < resolvedDatasets.length; j++) {
if (!resolvedDatasetsValues[j]) {
continue
}
if (resolvedDatasetsValues[j].type === 'range-bar') {
o[`y-${j}-low`] = resolvedDatasetsValues[j].value[i]?.[0]
o[`y-${j}-high`] = resolvedDatasetsValues[j].value[i]?.[1]
} else {
o[`y-${j}`] = resolvedDatasetsValues[j].value[i]
}
}
data.push(o)
}
const options = {
container: document.getElementById(`agchart-${id}`) as HTMLElement,
data: data,
series:
(resolvedDatasets?.map((d, index) => {
const type = resolvedDatasetsValues[index].type
if (type === 'range-bar') {
return {
type: type,
xKey: 'x',
yLowKey: `y-${index}-low`,
yHighKey: `y-${index}-high`,
yName: d.name
}
} else {
return {
type: type,
xKey: 'x',
yKey: `y-${index}`,
yName: d.name
}
}
}) as any[]) ?? []
}
outputs.result.set({
data: options.data,
series: options.series
})
AgChartsInstance?.update(chartInstance, options)
}
$: resolvedDatasetsValues = resolvedDatasets?.map((d) => {
const config = initConfig(
{
value: {
type: 'oneOf',
selected: 'bar',
labels: {
bar: 'Bar',
scatter: 'Scatter',
line: 'Line',
area: 'Area',
['range-bar']: 'Range Bar'
},
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]
]
}
}
}
}
},
{
value: d.value
}
)
return {
type: d.value['selected'],
// @ts-ignore
value: config.value?.['configuration']?.[d.value['selected']].value
}
})
$: resolvedXData && resolvedDatasets && resolvedDatasetsValues && chartInstance && updateChart()
$: result && updateChartByResult()
function updateChartByResult() {
if (!result || !chartInstance) {
return
}
const options = {
container: document.getElementById(`agchart-${id}`) as HTMLElement,
data: result?.['data'],
series: result?.['series']
}
outputs.result.set({
data: result?.['data'],
series: result?.['series']
})
AgChartsInstance?.update(chartInstance, options)
}
let AgChartsInstance: any | undefined = undefined
async function loadLibrary() {
if (ee) {
const enterprise = await import('ag-charts-enterprise')
AgChartsInstance = enterprise.AgCharts
AgChartsInstance.setLicenseKey(license)
} else {
const community = await import('ag-charts-community')
AgChartsInstance = community.AgCharts
}
}
onMount(() => {
loadLibrary().then(() => {
try {
// Chart Options
const options: AgChartOptions = {
container: document.getElementById(`agchart-${id}`) as HTMLElement,
data: [],
series: []
}
chartInstance = AgChartsInstance?.create(options)
} catch (error) {
console.error(error)
}
})
})
</script>
{#if datasets}
<ResolveConfig
{id}
key={'datasets'}
bind:resolvedConfig={resolvedDatasets}
configuration={datasets}
/>
{/if}
{#if xData}
<ResolveConfig {id} key={'xData'} bind:resolvedConfig={resolvedXData} configuration={xData} />
{/if}
{#if resolvedDatasets}
{#each resolvedDatasets as resolvedDataset, index (resolvedDataset.name + index)}
<ResolveConfig
{id}
key={'datasets' + index}
extraKey={resolvedDataset.name}
bind:resolvedConfig={resolvedDatasetsValues[index]}
configuration={resolvedDataset.value}
/>
{/each}
{/if}
{#each Object.keys(components['agchartscomponent'].initialData.configuration) as key (key)}
<ResolveConfig
{id}
{key}
bind:resolvedConfig={resolvedConfig[key]}
configuration={configuration[key]}
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.chartjscomponent}
/>
{/each}
<RunnableWrapper {outputs} {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
<div
class={twMerge('w-full h-full', css?.container?.class, 'wm-agchart')}
style={css?.container?.style ?? ''}
>
<div id={`agchart-${id}`} class="h-full w-full" />
</div>
</RunnableWrapper>
@@ -66,6 +66,7 @@
import AppStatCard from '../../components/display/AppStatCard.svelte'
import AppMenu from '../../components/display/AppMenu.svelte'
import AppDecisionTree from '../../components/layout/AppDecisionTree.svelte'
import AppAgCharts from '../../components/display/charts/AppAgCharts.svelte'
import AppDbExplorer from '../../components/display/dbtable/AppDbExplorer.svelte'
export let component: AppComponent
@@ -296,6 +297,30 @@
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'agchartscomponent'}
<AppAgCharts
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
datasets={component.datasets}
xData={component.xData}
{render}
/>
{:else if component.type === 'agchartscomponentee'}
<AppAgCharts
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
datasets={component.datasets}
xData={component.xData}
license={component.license}
ee={true}
{render}
/>
{:else if component.type === 'tablecomponent'}
<AppTable
configuration={component.configuration}
@@ -110,6 +110,17 @@ export type ChartJsComponentV2 = BaseComponent<'chartjscomponentv2'> & {
datasets: RichConfiguration | undefined
}
export type AgChartsComponent = BaseComponent<'agchartscomponent'> & {
xData: RichConfiguration | undefined
datasets: RichConfiguration | undefined
}
export type AgChartsComponentEe = BaseComponent<'agchartscomponentee'> & {
license: string
xData: RichConfiguration | undefined
datasets: RichConfiguration | undefined
}
export type ScatterChartComponent = BaseComponent<'scatterchartcomponent'>
export type TableComponent = BaseComponent<'tablecomponent'> & {
@@ -253,6 +264,8 @@ export type TypedComponent =
| StatisticCardComponent
| MenuComponent
| DecisionTreeComponent
| AgChartsComponent
| AgChartsComponentEe
export type AppComponent = BaseAppComponent & TypedComponent
@@ -630,6 +643,20 @@ const aggridcomponentconst = {
}
} as const
const agchartscomponentconst = {
name: 'AgCharts',
icon: BarChart4,
documentationLink: `${documentationBaseUrl}/agcharts`,
dims: '2:8-6:8' as AppComponentDimensions,
customCss: {
container: { class: '', style: '' }
},
initialData: {
configuration: {},
componentInput: undefined
}
} as const
export const components = {
displaycomponent: {
name: 'Rich Result',
@@ -1232,6 +1259,9 @@ export const components = {
}
}
},
agchartscomponent: agchartscomponentconst,
agchartscomponentee: { ...agchartscomponentconst, name: 'AgCharts EE' },
htmlcomponent: {
name: 'HTML',
icon: Code2,
@@ -77,7 +77,13 @@ const tables: ComponentSet = {
const charts: ComponentSet = {
title: 'Charts',
components: ['plotlycomponentv2', 'chartjscomponentv2', 'vegalitecomponent']
components: [
'plotlycomponentv2',
'chartjscomponentv2',
'vegalitecomponent',
'agchartscomponent',
'agchartscomponentee'
]
} as const
export const COMPONENT_SETS = [layout, tabs, buttons, inputs, tables, display, charts] as const
@@ -548,6 +548,11 @@ export const customisationByComponent: Customisation[] = [
selectors: [{ selector: '.wm-chartjs', comment: 'ChartJS', customCssKey: 'container' }],
variables: []
},
{
components: ['agchartscomponent'],
selectors: [{ selector: '.wm-agchart', comment: 'AgCharts', customCssKey: 'container' }],
variables: []
},
{
components: ['timeseriescomponent'],
selectors: [{ selector: '.wm-timeseries', comment: 'Time series', customCssKey: 'container' }],
@@ -668,6 +668,12 @@ export const quickStyleProperties: Record<
piechartcomponent: {
container: containerDefaultProps
},
agchartscomponent: {
container: containerDefaultProps
},
agchartscomponentee: {
container: containerDefaultProps
},
chartjscomponent: {
container: containerDefaultProps
},
@@ -0,0 +1,58 @@
<script lang="ts">
import type { RichConfiguration } from '../../types'
import InputsSpecEditor from './InputsSpecEditor.svelte'
import PanelSection from './common/PanelSection.svelte'
export let datasets: RichConfiguration | undefined = undefined
export let xData: RichConfiguration | undefined = undefined
export let id: string
</script>
<PanelSection
title={`AG Chart configuration`}
tooltip="The configuration is divided into two parts: X-axis data and an array of datasets. Each dataset hold the data for the Y-axis and the configuration for the plot (type, name, etc)."
>
<div class="w-full flex flex-col gap-4">
{#if xData}
<InputsSpecEditor
key={`X-axis data`}
bind:componentInput={xData}
{id}
userInputEnabled={false}
shouldCapitalize={true}
resourceOnly={false}
fieldType={xData?.['fieldType']}
subFieldType={xData?.['subFieldType']}
format={xData?.['format']}
selectOptions={xData?.['selectOptions']}
tooltip={xData?.['tooltip']}
fileUpload={xData?.['fileUpload']}
placeholder={xData?.['placeholder']}
customTitle={xData?.['customTitle']}
displayType={false}
/>
{/if}
{#if datasets}
<InputsSpecEditor
key={`Dataset`}
bind:componentInput={datasets}
{id}
userInputEnabled={false}
shouldCapitalize={true}
resourceOnly={false}
fieldType={datasets?.['fieldType']}
subFieldType={datasets?.['subFieldType']}
format={datasets?.['format']}
selectOptions={datasets?.['selectOptions']}
tooltip="For each dataset, you can specify the data for the Y-axis and the configuration for the plot (type, color, etc). If you want to have an eval for every data point, you can switch to JSON mode."
fileUpload={datasets?.['fileUpload']}
placeholder={datasets?.['placeholder']}
customTitle={datasets?.['customTitle']}
displayType={false}
shouldFormatExpression={true}
allowTypeChange={false}
/>
{/if}
</div>
</PanelSection>
@@ -26,7 +26,8 @@
if (subFieldType === 'boolean') {
value.push(false)
} else if (subFieldType === 'number') {
value.push(0)
value.push(1)
value = value
} else if (subFieldType === 'object') {
value.push({})
} else if (subFieldType === 'labeledresource' || subFieldType === 'labeledselect') {
@@ -74,6 +75,69 @@
},
name: 'New dataset'
})
} else if (subFieldType === 'ag-chart') {
value.push({
value: {
type: 'oneOf',
selected: 'bar',
labels: {
bar: 'Bar',
scatter: 'Scatter',
line: 'Line',
area: 'Area',
'range-bar': 'Range Bar'
},
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]
]
}
}
}
},
name: 'New dataset'
})
} else if (subFieldType === 'number-tuple') {
value.push([0, 5])
}
} else {
value.push('')
@@ -38,6 +38,7 @@
import ComponentPanelDataSource from './ComponentPanelDataSource.svelte'
import MenuItems from './MenuItems.svelte'
import DecisionTreeGraphEditor from './DecisionTreeGraphEditor.svelte'
import GridAgChartsLicenseKe from './GridAgChartsLicenseKe.svelte'
export let componentSettings: { item: GridItem; parent: string | undefined } | undefined =
undefined
@@ -326,6 +327,8 @@
/>
{:else if componentSettings.item.data.type === 'aggridcomponentee'}
<GridAgGridLicenseKey bind:license={componentSettings.item.data.license} />
{:else if componentSettings.item.data.type === 'agchartscomponentee'}
<GridAgChartsLicenseKe bind:license={componentSettings.item.data.license} />
{:else if componentSettings.item.data.type === 'steppercomponent'}
<GridTab
bind:tabs={componentSettings.item.data.tabs}
@@ -5,8 +5,9 @@
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import PlotlyRichEditor from './PlotlyRichEditor.svelte'
import ChartJSRichEditor from './ChartJSRichEditor.svelte'
import { onMount } from 'svelte'
import type { RichConfiguration } from '../../types'
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'
@@ -18,7 +19,10 @@
onMount(() => {
if (
(component.type === 'plotlycomponentv2' || component.type === 'chartjscomponentv2') &&
(component.type === 'plotlycomponentv2' ||
component.type === 'chartjscomponentv2' ||
component.type === 'agchartscomponent' ||
component.type === 'agchartscomponentee') &&
component.componentInput === undefined &&
component.datasets === undefined
) {
@@ -29,6 +33,7 @@
}
})
const { worldStore } = getContext<AppViewerContext>('AppViewerContext')
interface Dataset {
value: any // Define more specific type if possible
name: string
@@ -82,9 +87,47 @@
} 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()
@@ -92,6 +135,81 @@
} 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'
},
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]
]
}
}
}
} as const,
name: 'Dataset 1',
type: 'bar'
}
]
}
}
@@ -146,8 +264,16 @@
}
}
function isAgChartsComponent(): boolean {
return component.type === 'agchartscomponent' || component.type === 'agchartscomponentee'
}
function convertToJson() {
if (component.type !== 'plotlycomponentv2') {
if (
component.type !== 'plotlycomponentv2' &&
component.type !== 'agchartscomponent' &&
component.type !== 'agchartscomponentee'
) {
return
}
@@ -227,9 +353,31 @@
\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'}
{#if component.type === 'plotlycomponentv2' || component.type === 'chartjscomponentv2' || component.type === 'agchartscomponent' || component.type === 'agchartscomponentee'}
<div class="p-2">
<ToggleButtonGroup
bind:selected
@@ -258,6 +406,12 @@
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}
@@ -0,0 +1,30 @@
<script lang="ts">
import Badge from '$lib/components/common/badge/Badge.svelte'
export let license: string
let valid = false
$: license && checkLicenseKey(license)
async function checkLicenseKey(key: string) {
try {
const { AgCharts } = await import('ag-charts-enterprise')
// @ts-ignore
valid = AgCharts.licenseKey
} catch (e) {
console.error(e)
}
}
</script>
<div class="p-2">
<span class="text-xs font-semibold">AgCharts EE License Key</span>
<input type="text" bind:value={license} placeholder="AgCharts Enterprise" />
{#if valid}
<Badge color="green">Valid</Badge>
{:else}
<Badge color="red">Invalid</Badge>
{/if}
</div>
@@ -13,6 +13,7 @@
export let id: string
export let resourceOnly: boolean
export let tooltip: string | undefined
export let disabledOptions: string[] = []
$: {
if (oneOf == undefined) {
@@ -63,7 +64,7 @@
}}
>
{#each Object.keys(inputSpecsConfiguration ?? {}) as choice}
{#if !getValueOfDeprecated(inputSpecsConfiguration[choice]) || oneOf.selected === choice}
{#if (!disabledOptions.includes(choice) && !getValueOfDeprecated(inputSpecsConfiguration[choice])) || oneOf.selected === choice}
<option value={choice}>{labels?.[choice] ?? choice}</option>
{/if}
{/each}
@@ -17,7 +17,9 @@
import TableColumnWizard from '$lib/components/wizards/TableColumnWizard.svelte'
import PlotlyWizard from '$lib/components/wizards/PlotlyWizard.svelte'
import ChartJSWizard from '$lib/components/wizards/ChartJSWizard.svelte'
import AgChartWizard from '$lib/components/wizards/AgChartWizard.svelte'
import DBExplorerWizard from '$lib/components/wizards/DBExplorerWizard.svelte'
import Label from '$lib/components/Label.svelte'
export let componentInput: StaticInput<any> | undefined
export let fieldType: InputType | undefined = undefined
@@ -31,80 +33,35 @@
$: componentInput && onchange?.()
</script>
{#if componentInput?.type === 'static'}
{#if fieldType === 'number' || fieldType === 'integer'}
<input on:keydown|stopPropagation type="number" bind:value={componentInput.value} />
{:else if fieldType === 'textarea'}
<textarea use:autosize on:keydown|stopPropagation bind:value={componentInput.value} />
{:else if fieldType === 'date'}
<input on:keydown|stopPropagation type="date" bind:value={componentInput.value} />
{:else if fieldType === 'boolean'}
<Toggle bind:checked={componentInput.value} size="xs" />
{:else if fieldType === 'select' && selectOptions}
<select on:keydown|stopPropagation bind:value={componentInput.value}>
{#each selectOptions ?? [] as option}
{#if typeof option == 'string'}
<option value={option}>
{option}
</option>
{:else}
<option value={option.value}>
{option.label}
</option>
{/if}
{/each}
</select>
{:else if fieldType === 'icon-select'}
<IconSelectInput bind:componentInput />
{:else if fieldType === 'tab-select'}
<TabSelectInput bind:componentInput />
{:else if fieldType === 'resource'}
<ResourcePicker
initialValue={componentInput.value?.split('$res:')?.[1] || ''}
on:change={(e) => {
let path = e.detail
if (componentInput) {
if (path) {
componentInput.value = `$res:${path}`
} else {
componentInput.value = undefined
}
}
}}
showSchemaExplorer
resourceType="postgresql"
/>
{:else if fieldType === 'labeledresource'}
{#if componentInput?.value && typeof componentInput?.value == 'object' && 'label' in componentInput?.value && (componentInput.value?.['value'] == undefined || typeof componentInput.value?.['value'] == 'string')}
<div class="flex flex-col gap-1 w-full">
<input
on:keydown|stopPropagation
placeholder="Label"
type="text"
bind:value={componentInput.value['label']}
/>
<ResourcePicker
initialValue={componentInput.value?.['value']?.split('$res:')?.[1] || ''}
on:change={(e) => {
let path = e.detail
if (componentInput) {
if (path) {
componentInput.value['value'] = `$res:${path}`
} else {
componentInput.value['value'] = undefined
}
}
}}
showSchemaExplorer
/>
</div>
{:else}
Inconsistent labeled resource object
{/if}
{:else if fieldType === 'color'}
<ColorInput bind:value={componentInput.value} />
{:else if fieldType === 'object' || fieldType == 'labeledselect'}
{#if format?.startsWith('resource-') && (componentInput.value == undefined || typeof componentInput.value == 'string')}
{#key subFieldType}
{#if componentInput?.type === 'static'}
{#if fieldType === 'number' || fieldType === 'integer'}
<input on:keydown|stopPropagation type="number" bind:value={componentInput.value} />
{:else if fieldType === 'textarea'}
<textarea use:autosize on:keydown|stopPropagation bind:value={componentInput.value} />
{:else if fieldType === 'date'}
<input on:keydown|stopPropagation type="date" bind:value={componentInput.value} />
{:else if fieldType === 'boolean'}
<Toggle bind:checked={componentInput.value} size="xs" />
{:else if fieldType === 'select' && selectOptions}
<select on:keydown|stopPropagation bind:value={componentInput.value}>
{#each selectOptions ?? [] as option}
{#if typeof option == 'string'}
<option value={option}>
{option}
</option>
{:else}
<option value={option.value}>
{option.label}
</option>
{/if}
{/each}
</select>
{:else if fieldType === 'icon-select'}
<IconSelectInput bind:componentInput />
{:else if fieldType === 'tab-select'}
<TabSelectInput bind:componentInput />
{:else if fieldType === 'resource'}
<ResourcePicker
initialValue={componentInput.value?.split('$res:')?.[1] || ''}
on:change={(e) => {
@@ -117,142 +74,230 @@
}
}
}}
resourceType={format && format?.split('-').length > 1
? format.substring('resource-'.length)
: undefined}
showSchemaExplorer
resourceType="postgresql"
/>
{:else if fieldType === 'labeledresource'}
{#if componentInput?.value && typeof componentInput?.value == 'object' && 'label' in componentInput?.value && (componentInput.value?.['value'] == undefined || typeof componentInput.value?.['value'] == 'string')}
<div class="flex flex-col gap-1 w-full">
<input
on:keydown|stopPropagation
placeholder="Label"
type="text"
bind:value={componentInput.value['label']}
/>
<ResourcePicker
initialValue={componentInput.value?.['value']?.split('$res:')?.[1] || ''}
on:change={(e) => {
let path = e.detail
if (componentInput) {
if (path) {
componentInput.value['value'] = `$res:${path}`
} else {
componentInput.value['value'] = undefined
}
}
}}
showSchemaExplorer
/>
</div>
{:else}
Inconsistent labeled resource object
{/if}
{:else if fieldType === 'color'}
<ColorInput bind:value={componentInput.value} />
{:else if fieldType === 'object' || fieldType == 'labeledselect'}
{#if format?.startsWith('resource-') && (componentInput.value == undefined || typeof componentInput.value == 'string')}
<ResourcePicker
initialValue={componentInput.value?.split('$res:')?.[1] || ''}
on:change={(e) => {
let path = e.detail
if (componentInput) {
if (path) {
componentInput.value = `$res:${path}`
} else {
componentInput.value = undefined
}
}
}}
resourceType={format && format?.split('-').length > 1
? format.substring('resource-'.length)
: undefined}
showSchemaExplorer
/>
{:else}
<div class="flex w-full flex-col">
<JsonEditor
small
bind:value={componentInput.value}
code={JSON.stringify(componentInput.value, null, 2)}
/>
</div>
{/if}
{:else if fieldType === 'array'}
<ArrayStaticInputEditor {subFieldType} bind:componentInput on:deleteArrayItem />
{:else if fieldType === 'schema'}
<div class="w-full">
<SchemaEditor bind:schema={componentInput.value} lightMode />
</div>
{:else if fieldType === 'ag-grid'}
<div class="flex flex-row rounded-md bg-surface items-center h-full">
<div class="relative w-full">
<input
class="text-xs px-2 border-y w-full flex flex-row items-center border-r rounded-r-md h-8"
bind:value={componentInput.value.field}
placeholder="Field"
/>
<div class="absolute top-1 right-1">
<AgGridWizard bind:value={componentInput.value}>
<svelte:fragment slot="trigger">
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
</AgGridWizard>
</div>
</div>
</div>
{:else if fieldType === 'db-explorer' && componentInput.value != undefined}
<div class="flex flex-row rounded-md bg-surface items-center h-full">
<div class="relative w-full">
<input
class="text-xs px-2 border-y w-full flex flex-row items-center border-r rounded-r-md h-8"
bind:value={componentInput.value.field}
placeholder="Field"
disabled
/>
<div class="absolute top-1 right-1">
<DBExplorerWizard bind:value={componentInput.value}>
<svelte:fragment slot="trigger">
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
</DBExplorerWizard>
</div>
</div>
</div>
{:else if fieldType === 'table-column'}
<div class="flex flex-row rounded-md bg-surface items-center h-full">
<div class="relative w-full">
<input
class="text-xs px-2 border-y w-full flex flex-row items-center border-r rounded-r-md h-8"
bind:value={componentInput.value.field}
placeholder="Field"
/>
<div class="absolute top-1 right-1">
<TableColumnWizard bind:column={componentInput.value}>
<svelte:fragment slot="trigger">
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
</TableColumnWizard>
</div>
</div>
</div>
{:else if fieldType === 'plotly'}
<div class="flex flex-row rounded-md bg-surface items-center h-full">
<div class="relative w-full">
<input
class="text-xs px-2 border-y w-full flex flex-row items-center border-r rounded-r-md h-8"
bind:value={componentInput.value.name}
placeholder="Dataset name"
/>
<div class="absolute top-1 right-1">
<PlotlyWizard bind:value={componentInput.value} on:remove>
<svelte:fragment slot="trigger">
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
</PlotlyWizard>
</div>
</div>
</div>
{:else if fieldType === 'chartjs'}
<div class="flex flex-row rounded-md bg-surface items-center h-full">
<div class="relative w-full">
<input
class="text-xs px-2 border-y w-full flex flex-row items-center border-r rounded-r-md h-8"
bind:value={componentInput.value.name}
placeholder="Dataset name"
/>
<div class="absolute top-1 right-1">
<ChartJSWizard bind:value={componentInput.value} on:remove>
<svelte:fragment slot="trigger">
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
</ChartJSWizard>
</div>
</div>
</div>
{:else if fieldType === 'ag-chart'}
<div class="flex flex-row rounded-md bg-surface items-center h-full">
<div class="relative w-full">
<input
class="text-xs px-2 border-y w-full flex flex-row items-center border-r rounded-r-md h-8"
bind:value={componentInput.value.name}
placeholder="Dataset name"
/>
<div class="absolute top-1 right-1">
<AgChartWizard bind:value={componentInput.value} on:remove>
<svelte:fragment slot="trigger">
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
</AgChartWizard>
</div>
</div>
</div>
{:else if fieldType === 'number-tuple'}
<div class="flex flex-row rounded-md bg-surface items-center h-full">
<div class="relative w-full flex flex-row gap-2">
<Label label="Y Low">
<input
class="text-xs px-2 border-y w-full flex flex-row items-center border-r rounded-r-md h-8"
bind:value={componentInput.value[0]}
type="number"
/>
</Label>
<Label label="Y High">
<input
class="text-xs px-2 border-y w-full flex flex-row items-center border-r rounded-r-md h-8"
bind:value={componentInput.value[1]}
type="number"
/>
</Label>
</div>
</div>
{:else}
<div class="flex w-full flex-col">
<JsonEditor
small
<div class="flex gap-1 relative w-full">
<textarea
rows="1"
use:autosize
on:keydown|stopPropagation
placeholder={placeholder ?? 'Static value'}
bind:value={componentInput.value}
code={JSON.stringify(componentInput.value, null, 2)}
class="!pr-12"
/>
</div>
{/if}
{:else if fieldType === 'array'}
<ArrayStaticInputEditor {subFieldType} bind:componentInput on:deleteArrayItem />
{:else if fieldType === 'schema'}
<div class="w-full">
<SchemaEditor bind:schema={componentInput.value} lightMode />
</div>
{:else if fieldType === 'ag-grid'}
<div class="flex flex-row rounded-md bg-surface items-center h-full">
<div class="relative w-full">
<input
class="text-xs px-2 border-y w-full flex flex-row items-center border-r rounded-r-md h-8"
bind:value={componentInput.value.field}
placeholder="Field"
/>
<div class="absolute top-1 right-1">
<AgGridWizard bind:value={componentInput.value}>
<svelte:fragment slot="trigger">
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
</AgGridWizard>
</div>
</div>
</div>
{:else if fieldType === 'db-explorer' && componentInput.value != undefined}
<div class="flex flex-row rounded-md bg-surface items-center h-full">
<div class="relative w-full">
<input
class="text-xs px-2 border-y w-full flex flex-row items-center border-r rounded-r-md h-8"
bind:value={componentInput.value.field}
placeholder="Field"
disabled
/>
<div class="absolute top-1 right-1">
<DBExplorerWizard bind:value={componentInput.value}>
<svelte:fragment slot="trigger">
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
</DBExplorerWizard>
</div>
</div>
</div>
{:else if fieldType === 'table-column'}
<div class="flex flex-row rounded-md bg-surface items-center h-full">
<div class="relative w-full">
<input
class="text-xs px-2 border-y w-full flex flex-row items-center border-r rounded-r-md h-8"
bind:value={componentInput.value.field}
placeholder="Field"
/>
<div class="absolute top-1 right-1">
<TableColumnWizard bind:column={componentInput.value}>
<svelte:fragment slot="trigger">
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
</TableColumnWizard>
</div>
</div>
</div>
{:else if fieldType === 'plotly'}
<div class="flex flex-row rounded-md bg-surface items-center h-full">
<div class="relative w-full">
<input
class="text-xs px-2 border-y w-full flex flex-row items-center border-r rounded-r-md h-8"
bind:value={componentInput.value.name}
placeholder="Dataset name"
/>
<div class="absolute top-1 right-1">
<PlotlyWizard bind:value={componentInput.value} on:remove>
<svelte:fragment slot="trigger">
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
</PlotlyWizard>
</div>
</div>
</div>
{:else if fieldType === 'chartjs'}
<div class="flex flex-row rounded-md bg-surface items-center h-full">
<div class="relative w-full">
<input
class="text-xs px-2 border-y w-full flex flex-row items-center border-r rounded-r-md h-8"
bind:value={componentInput.value.name}
placeholder="Dataset name"
/>
<div class="absolute top-1 right-1">
<ChartJSWizard bind:value={componentInput.value} on:remove>
<svelte:fragment slot="trigger">
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
</ChartJSWizard>
</div>
</div>
</div>
{:else}
<div class="flex gap-1 relative w-full">
<textarea
rows="1"
use:autosize
on:keydown|stopPropagation
placeholder={placeholder ?? 'Static value'}
bind:value={componentInput.value}
class="!pr-12"
/>
</div>
{/if}
{/if}
{/key}
@@ -27,9 +27,11 @@ export type InputType =
| 'plotly'
| 'chartjs'
| 'DecisionTreeNode'
| 'ag-chart'
| 'resource'
| 'db-explorer'
| 'db-table'
| 'number-tuple'
// Connection to an output of another component
// defined by the id of the component and the path of the output
@@ -201,7 +203,9 @@ export type AppInput =
| AppInputSpec<'array', object[], 'plotly'>
| AppInputSpec<'array', object[], 'chartjs'>
| AppInputSpec<'array', DecisionTreeNode, 'DecisionTreeNode'>
| AppInputSpec<'array', object[], 'ag-chart'>
| AppInputSpec<'resource', string>
| AppInputSpec<'array', object[], 'number-tuple'>
export type RowAppInput = Extract<AppInput, { type: 'row' }>
export type StaticAppInput = Extract<AppInput, { type: 'static' }>
@@ -0,0 +1,68 @@
<script lang="ts">
import { createEventDispatcher, getContext } from 'svelte'
import { Popup } from '../common'
import Label from '../Label.svelte'
import { offset, flip, shift } from 'svelte-floating-ui/dom'
import Button from '../common/button/Button.svelte'
import OneOfInputSpecsEditor from '../apps/editor/settingsPanel/OneOfInputSpecsEditor.svelte'
import type { AppViewerContext, GridItem, RichConfiguration } from '../apps/types'
import { findGridItem } from '../apps/editor/appUtils'
const { selectedComponent, app } = getContext<AppViewerContext>('AppViewerContext')
type Dataset = {
value: RichConfiguration
name: string
type: 'bar' | 'scatter' | 'line' | 'area' | 'range-bar'
}
let component: GridItem | undefined = undefined
$: if (component === undefined && $selectedComponent && $app) {
component = findGridItem($app, $selectedComponent[0])
}
$: isEE = component?.data.type === 'agchartscomponentee'
export let value: Dataset | undefined = undefined
const dispatch = createEventDispatcher()
function removeDataset() {
dispatch('remove')
}
</script>
<Popup
floatingConfig={{
strategy: 'fixed',
placement: 'left-end',
middleware: [offset(8), flip(), shift()]
}}
containerClasses="border rounded-lg shadow-lg bg-surface p-4"
>
<svelte:fragment slot="button">
<slot name="trigger" />
</svelte:fragment>
{#if value}
<div class="flex flex-col w-96 p-2 gap-4">
<Label label="Name">
<input type="text" bind:value={value.name} />
</Label>
<OneOfInputSpecsEditor
key={'Data'}
bind:oneOf={value.value}
id={$selectedComponent?.[0] ?? ''}
shouldCapitalize={true}
resourceOnly={false}
inputSpecsConfiguration={value.value?.['configuration']}
labels={value.value?.['labels']}
tooltip={value.value?.['tooltip']}
disabledOptions={isEE ? [] : ['range-bar']}
/>
<Button color="red" size="xs" on:click={removeDataset}>Remove dataset</Button>
</div>
{/if}
</Popup>
+1 -1
View File
@@ -1,7 +1,7 @@
import { ScriptService, type MainArgSignature, FlowService, Script } from '$lib/gen'
import { get, writable } from 'svelte/store'
import type { Schema, SchemaProperty, SupportedLanguage } from './common.js'
import { emptySchema, sendUserToast, sortObject } from './utils.js'
import { emptySchema, sortObject } from './utils.js'
import { tick } from 'svelte'
import init, {
parse_deno,
+1
View File
@@ -629,6 +629,7 @@ const config = {
{
display: 'block',
fontSize: theme('fontSize.sm'),
boxShadow: theme('boxShadow.sm'),
width: '100%',
padding: `${theme('spacing.1')} ${theme('spacing.2')}`,
border: `1px solid ${theme('colors.gray.300')}`,