From 8d7e50df2147235e3ce1e4a0b89827177132dc5a Mon Sep 17 00:00:00 2001 From: namnguyen Date: Tue, 18 Jun 2024 15:23:26 +0700 Subject: [PATCH 1/7] add title for Metric.tsx --- frontend/taipy-gui/src/components/Taipy/Metric.tsx | 4 ++++ taipy/gui/_renderers/factory.py | 1 + taipy/gui/viselements.json | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/frontend/taipy-gui/src/components/Taipy/Metric.tsx b/frontend/taipy-gui/src/components/Taipy/Metric.tsx index 41217b7316..818612432e 100644 --- a/frontend/taipy-gui/src/components/Taipy/Metric.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Metric.tsx @@ -25,6 +25,7 @@ import { darkThemeTemplate } from "../../themes/darkThemeTemplate"; const Plot = lazy(() => import("react-plotly.js")); interface MetricProps extends TaipyBaseProps, TaipyHoverProps { + title?: string type?: string min?: number max?: number @@ -72,6 +73,9 @@ const Metric = (props: MetricProps) => { (delta !== undefined) && mode.push("delta"); return [ { + title: { + text: props.title + }, domain: {x: [0, 1], y: [0, 1]}, value: value, type: "indicator", diff --git a/taipy/gui/_renderers/factory.py b/taipy/gui/_renderers/factory.py index 76edb4c25f..09a3762165 100644 --- a/taipy/gui/_renderers/factory.py +++ b/taipy/gui/_renderers/factory.py @@ -360,6 +360,7 @@ class _Factory: .set_attributes( [ ("id",), + ("title",), ("active", PropertyType.dynamic_boolean, True), ("layout", PropertyType.dynamic_dict), ("type", PropertyType.string, "circular"), diff --git a/taipy/gui/viselements.json b/taipy/gui/viselements.json index 5a93f2f3d9..a125500a00 100644 --- a/taipy/gui/viselements.json +++ b/taipy/gui/viselements.json @@ -1116,6 +1116,11 @@ "shared" ], "properties": [ + { + "name": "title", + "type": "str", + "doc": "The title of the metric." + }, { "name": "value", "default_property": true, From aadf04a44294aba2e1891ddf0cd5e8c77abcea83 Mon Sep 17 00:00:00 2001 From: trgiangdo Date: Tue, 18 Jun 2024 16:09:16 +0700 Subject: [PATCH 2/7] fix: put the _write_default_data() inside the _Reloader context --- taipy/core/data/csv.py | 4 +++- taipy/core/data/excel.py | 4 +++- taipy/core/data/json.py | 5 +++-- taipy/core/data/parquet.py | 4 +++- taipy/core/data/pickle.py | 4 +++- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/taipy/core/data/csv.py b/taipy/core/data/csv.py index 3918a988fd..e5d5435c88 100644 --- a/taipy/core/data/csv.py +++ b/taipy/core/data/csv.py @@ -18,6 +18,7 @@ from taipy.config.common.scope import Scope +from .._entity._reload import _Reloader from .._version._version_manager_factory import _VersionManagerFactory from ..job.job_id import JobId from ._file_datanode_mixin import _FileDataNodeMixin @@ -116,7 +117,8 @@ def __init__( **properties, ) - self._write_default_data(default_value) + with _Reloader(): + self._write_default_data(default_value) self._TAIPY_PROPERTIES.update( { diff --git a/taipy/core/data/excel.py b/taipy/core/data/excel.py index 08d453aa97..92f201df6a 100644 --- a/taipy/core/data/excel.py +++ b/taipy/core/data/excel.py @@ -18,6 +18,7 @@ from taipy.config.common.scope import Scope +from .._entity._reload import _Reloader from .._version._version_manager_factory import _VersionManagerFactory from ..exceptions.exceptions import ExposedTypeLengthMismatch, NonExistingExcelSheet, SheetNameLengthMismatch from ..job.job_id import JobId @@ -118,7 +119,8 @@ def __init__( **properties, ) - self._write_default_data(default_value) + with _Reloader(): + self._write_default_data(default_value) self._TAIPY_PROPERTIES.update( { diff --git a/taipy/core/data/json.py b/taipy/core/data/json.py index e1f131c702..f361bc9e60 100644 --- a/taipy/core/data/json.py +++ b/taipy/core/data/json.py @@ -18,7 +18,7 @@ from taipy.config.common.scope import Scope -from .._entity._reload import _self_reload +from .._entity._reload import _Reloader, _self_reload from .._version._version_manager_factory import _VersionManagerFactory from ._file_datanode_mixin import _FileDataNodeMixin from .data_node import DataNode @@ -112,7 +112,8 @@ def __init__( self._decoder = self._properties.get(self._DECODER_KEY, _DefaultJSONDecoder) self._encoder = self._properties.get(self._ENCODER_KEY, _DefaultJSONEncoder) - self._write_default_data(default_value) + with _Reloader(): + self._write_default_data(default_value) self._TAIPY_PROPERTIES.update( { diff --git a/taipy/core/data/parquet.py b/taipy/core/data/parquet.py index 9583f0df07..6e968c5966 100644 --- a/taipy/core/data/parquet.py +++ b/taipy/core/data/parquet.py @@ -18,6 +18,7 @@ from taipy.config.common.scope import Scope +from .._entity._reload import _Reloader from .._version._version_manager_factory import _VersionManagerFactory from ..exceptions.exceptions import UnknownCompressionAlgorithm, UnknownParquetEngine from ..job.job_id import JobId @@ -153,7 +154,8 @@ def __init__( **properties, ) - self._write_default_data(default_value) + with _Reloader(): + self._write_default_data(default_value) if not self._last_edit_date and (isfile(self._path) or isdir(self._path)): self._last_edit_date = datetime.now() diff --git a/taipy/core/data/pickle.py b/taipy/core/data/pickle.py index ad6d51534b..7c7693ff68 100644 --- a/taipy/core/data/pickle.py +++ b/taipy/core/data/pickle.py @@ -15,6 +15,7 @@ from taipy.config.common.scope import Scope +from .._entity._reload import _Reloader from .._version._version_manager_factory import _VersionManagerFactory from ._file_datanode_mixin import _FileDataNodeMixin from .data_node import DataNode @@ -98,7 +99,8 @@ def __init__( **properties, ) - self._write_default_data(default_value) + with _Reloader(): + self._write_default_data(default_value) self._TAIPY_PROPERTIES.update( { From f84824de952ad1dcbbcf143903666c8b7ed14823 Mon Sep 17 00:00:00 2001 From: namnguyen Date: Wed, 19 Jun 2024 05:43:50 +0700 Subject: [PATCH 3/7] fix hover_text + Data & Partial --- .../taipy-gui/src/components/Taipy/Metric.tsx | 39 ++++++++++--------- taipy/gui/_renderers/factory.py | 1 + taipy/gui/viselements.json | 16 +++++--- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/frontend/taipy-gui/src/components/Taipy/Metric.tsx b/frontend/taipy-gui/src/components/Taipy/Metric.tsx index 818612432e..6f8c64eef4 100644 --- a/frontend/taipy-gui/src/components/Taipy/Metric.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Metric.tsx @@ -12,7 +12,7 @@ */ import React, {CSSProperties, lazy, Suspense, useMemo} from 'react'; -import {Data} from "plotly.js"; +import {Data, Delta, Layout} from "plotly.js"; import Box from "@mui/material/Box"; import Skeleton from "@mui/material/Skeleton"; import Tooltip from "@mui/material/Tooltip"; @@ -20,7 +20,7 @@ import {useTheme} from "@mui/material"; import {useClassNames, useDynamicJsonProperty, useDynamicProperty} from "../../utils/hooks"; import {extractPrefix, extractSuffix, sprintfToD3Converter} from "../../utils/formatConversion"; import {TaipyBaseProps, TaipyHoverProps} from "./utils"; -import { darkThemeTemplate } from "../../themes/darkThemeTemplate"; +import {darkThemeTemplate} from "../../themes/darkThemeTemplate"; const Plot = lazy(() => import("react-plotly.js")); @@ -73,9 +73,6 @@ const Metric = (props: MetricProps) => { (delta !== undefined) && mode.push("delta"); return [ { - title: { - text: props.title - }, domain: {x: [0, 1], y: [0, 1]}, value: value, type: "indicator", @@ -90,7 +87,7 @@ const Metric = (props: MetricProps) => { prefix: extractPrefix(props.deltaFormat), suffix: extractSuffix(props.deltaFormat), valueformat: sprintfToD3Converter(props.deltaFormat) - }, + } as Partial, gauge: { axis: { range: [ @@ -106,7 +103,7 @@ const Metric = (props: MetricProps) => { } }, } - ]; + ] as Data[]; }, [ props.format, props.deltaFormat, @@ -148,26 +145,32 @@ const Metric = (props: MetricProps) => { layout.template = template; } - return layout + return { + title: props.title || layout.title, + ...layout + } as Partial }, [ + props.title, props.template, props.template_Dark_, props.template_Light_, theme.palette.mode, - baseLayout + baseLayout, ]) return ( - }> - - +
+ }> + + +
); @@ -175,7 +178,7 @@ const Metric = (props: MetricProps) => { export default Metric; -const { colorscale, colorway, font} = darkThemeTemplate.layout; +const {colorscale, colorway, font} = darkThemeTemplate.layout; const darkTemplate = { layout: { colorscale, diff --git a/taipy/gui/_renderers/factory.py b/taipy/gui/_renderers/factory.py index 09a3762165..9a4c8aca78 100644 --- a/taipy/gui/_renderers/factory.py +++ b/taipy/gui/_renderers/factory.py @@ -373,6 +373,7 @@ class _Factory: ("show_value", PropertyType.boolean, True), ("format", PropertyType.string), ("delta_format", PropertyType.string), + ("hover_text", PropertyType.dynamic_string), ("template", PropertyType.dict), ("template[dark]", PropertyType.dict), ("template[light]", PropertyType.dict), diff --git a/taipy/gui/viselements.json b/taipy/gui/viselements.json index a125500a00..5495ffa9c7 100644 --- a/taipy/gui/viselements.json +++ b/taipy/gui/viselements.json @@ -1116,11 +1116,6 @@ "shared" ], "properties": [ - { - "name": "title", - "type": "str", - "doc": "The title of the metric." - }, { "name": "value", "default_property": true, @@ -1133,6 +1128,12 @@ "type": "str", "doc": "The type of the gauge.
Possible values are:\n
    \n
  • \"none\"
  • \n
  • \"circular\"
  • \n
  • \"linear\"
." }, + { + "name": "title", + "default_value": "None", + "type": "str", + "doc": "The title of the metric." + }, { "name": "min", "type": "int|float", @@ -1197,6 +1198,11 @@ "name": "template[light]", "type": "dict", "doc": "The Plotly layout template applied over the base template when theme is not dark." + }, + { + "name": "hover_text", + "type": "dynamic(str)", + "doc": "The information that is displayed when the user hovers over this element." } ] } From 82c8f7f3c46bf62450b153af7defd2bfe37a3a04 Mon Sep 17 00:00:00 2001 From: namnguyen Date: Wed, 19 Jun 2024 13:51:37 +0700 Subject: [PATCH 4/7] wrap tooltip's children with div --- .../taipy-gui/src/components/Taipy/Chart.tsx | 275 +++++++++--------- 1 file changed, 139 insertions(+), 136 deletions(-) diff --git a/frontend/taipy-gui/src/components/Taipy/Chart.tsx b/frontend/taipy-gui/src/components/Taipy/Chart.tsx index 3a0c697be4..ecf338854e 100644 --- a/frontend/taipy-gui/src/components/Taipy/Chart.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Chart.tsx @@ -11,7 +11,7 @@ * specific language governing permissions and limitations under the License. */ -import React, { CSSProperties, useCallback, useEffect, useMemo, useRef, useState, lazy, Suspense } from "react"; +import React, {CSSProperties, useCallback, useEffect, useMemo, useRef, useState, lazy, Suspense} from "react"; import { Config, Data, @@ -26,15 +26,15 @@ import { import Skeleton from "@mui/material/Skeleton"; import Box from "@mui/material/Box"; import Tooltip from "@mui/material/Tooltip"; -import { useTheme } from "@mui/material"; +import {useTheme} from "@mui/material"; -import { getArrayValue, getUpdateVar, TaipyActiveProps, TaipyChangeProps } from "./utils"; +import {getArrayValue, getUpdateVar, TaipyActiveProps, TaipyChangeProps} from "./utils"; import { createRequestChartUpdateAction, createSendActionNameAction, createSendUpdateAction, } from "../../context/taipyReducers"; -import { ColumnDesc } from "./tableUtils"; +import {ColumnDesc} from "./tableUtils"; import { useClassNames, useDispatch, @@ -43,7 +43,7 @@ import { useDynamicProperty, useModule, } from "../../utils/hooks"; -import { darkThemeTemplate } from "../../themes/darkThemeTemplate"; +import {darkThemeTemplate} from "../../themes/darkThemeTemplate"; const Plot = lazy(() => import("react-plotly.js")); @@ -91,7 +91,7 @@ interface ChartConfig { export type TraceValueType = Record; -const defaultStyle = { position: "relative", display: "inline-block" }; +const defaultStyle = {position: "relative", display: "inline-block"}; const indexedData = /^(\d+)\/(.*)/; @@ -105,7 +105,7 @@ const getColNameFromIndexed = (colName: string): string => { return colName; }; -const getValue = ( +const getValue = ( values: TraceValueType | undefined, arr: T[], idx: number, @@ -150,21 +150,21 @@ const getDecimatorsPayload = ( ) => { return decimators ? { - width: plotDiv?.clientWidth, - height: plotDiv?.clientHeight, - decimators: decimators.map((d, i) => - d - ? { - decimator: d, - xAxis: getAxis(traces, i, columns, 0), - yAxis: getAxis(traces, i, columns, 1), - zAxis: getAxis(traces, i, columns, 2), - chartMode: modes[i], - } - : undefined - ), - relayoutData: relayoutData, - } + width: plotDiv?.clientWidth, + height: plotDiv?.clientHeight, + decimators: decimators.map((d, i) => + d + ? { + decimator: d, + xAxis: getAxis(traces, i, columns, 0), + yAxis: getAxis(traces, i, columns, 1), + zAxis: getAxis(traces, i, columns, 2), + chartMode: modes[i], + } + : undefined + ), + relayoutData: relayoutData, + } : undefined; }; @@ -177,6 +177,7 @@ const isOnClick = (types: string[]) => (types?.length ? types.every((t) => t === interface WithpointNumbers { pointNumbers: number[]; } + const getPlotIndex = (pt: PlotDatum) => pt.pointIndex === undefined ? pt.pointNumber === undefined @@ -223,7 +224,7 @@ const TaipyPlotlyButtons: ModeBarButtonAny[] = [ if (!div) { return; } - const { height } = gd.dataset; + const {height} = gd.dataset; if (!height) { gd.setAttribute("data-height", getComputedStyle(div).height); } @@ -341,7 +342,7 @@ const Chart = (props: ChartProp) => { useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars); const layout = useMemo(() => { - const layout = { ...baseLayout }; + const layout = {...baseLayout}; let template = undefined; try { const tpl = props.template && JSON.parse(props.template); @@ -351,7 +352,7 @@ const Chart = (props: ChartProp) => { ? JSON.parse(props.template_Dark_) : darkThemeTemplate : props.template_Light_ && JSON.parse(props.template_Light_); - template = tpl ? (tplTheme ? { ...tpl, ...tplTheme } : tpl) : tplTheme ? tplTheme : undefined; + template = tpl ? (tplTheme ? {...tpl, ...tplTheme} : tpl) : tplTheme ? tplTheme : undefined; } catch (e) { console.info(`Error while parsing Chart.template\n${(e as Error).message || e}`); } @@ -401,11 +402,11 @@ const Chart = (props: ChartProp) => { const style = useMemo( () => height === undefined - ? ({ ...defaultStyle, width: width } as CSSProperties) - : ({ ...defaultStyle, width: width, height: height } as CSSProperties), + ? ({...defaultStyle, width: width} as CSSProperties) + : ({...defaultStyle, width: width, height: height} as CSSProperties), [width, height] ); - const skelStyle = useMemo(() => ({ ...style, minHeight: "7em" }), [style]); + const skelStyle = useMemo(() => ({...style, minHeight: "7em"}), [style]); const dataPl = useMemo(() => { if (props.figure) { @@ -417,83 +418,83 @@ const Chart = (props: ChartProp) => { const datum = data[dataKey]; lastDataPl.current = datum ? config.traces.map((trace, idx) => { - const ret = { - ...getArrayValue(config.options, idx, {}), - type: config.types[idx], - mode: config.modes[idx], - name: - getArrayValue(config.names, idx) || - (config.columns[trace[1]] ? getColNameFromIndexed(config.columns[trace[1]].dfid) : undefined), - } as Record; - ret.marker = { ...getArrayValue(config.markers, idx, ret.marker || {}) }; - if (Object.keys(ret.marker as object).length) { - MARKER_TO_COL.forEach((prop) => { - const val = (ret.marker as Record)[prop]; - if (typeof val === "string") { - const arr = getValueFromCol(datum, val as string); - if (arr.length) { - (ret.marker as Record)[prop] = arr; - } - } - }); - } else { - delete ret.marker; - } - const xs = getValue(datum, trace, 0) || []; - const ys = getValue(datum, trace, 1) || []; - const addIndex = getArrayValue(config.addIndex, idx, true) && !ys.length; - const baseX = addIndex ? Array.from(Array(xs.length).keys()) : xs; - const baseY = addIndex ? xs : ys; - const axisNames = config.axisNames.length > idx ? config.axisNames[idx] : ([] as string[]); - if (baseX.length) { - if (axisNames.length > 0) { - ret[axisNames[0]] = baseX; - } else { - ret.x = baseX; - } - } - if (baseY.length) { - if (axisNames.length > 1) { - ret[axisNames[1]] = baseY; - } else { - ret.y = baseY; - } - } - const baseZ = getValue(datum, trace, 2, true); - if (baseZ) { - if (axisNames.length > 2) { - ret[axisNames[2]] = baseZ; - } else { - ret.z = baseZ; - } - } - // Hack for treemap charts: create a fallback 'parents' column if needed - // This works ONLY because 'parents' is the third named axis - // (see __CHART_AXIS in gui/utils/chart_config_builder.py) - else if (config.types[idx] === "treemap" && Array.isArray(ret.labels)) { - ret.parents = Array(ret.labels.length).fill(""); - } - // Other axis - for (let i = 3; i < axisNames.length; i++) { - ret[axisNames[i]] = getValue(datum, trace, i, true); - } - ret.text = getValue(datum, config.texts, idx, true); - ret.xaxis = config.xaxis[idx]; - ret.yaxis = config.yaxis[idx]; - ret.hovertext = getValue(datum, config.labels, idx, true); - const selPoints = getArrayValue(selected, idx, []); - if (selPoints?.length) { - ret.selectedpoints = selPoints; - } - ret.orientation = getArrayValue(config.orientations, idx); - ret.line = getArrayValue(config.lines, idx); - ret.textposition = getArrayValue(config.textAnchors, idx); - const selectedMarker = getArrayValue(config.selectedMarkers, idx); - if (selectedMarker) { - ret.selected = { marker: selectedMarker }; - } - return ret as Data; - }) + const ret = { + ...getArrayValue(config.options, idx, {}), + type: config.types[idx], + mode: config.modes[idx], + name: + getArrayValue(config.names, idx) || + (config.columns[trace[1]] ? getColNameFromIndexed(config.columns[trace[1]].dfid) : undefined), + } as Record; + ret.marker = {...getArrayValue(config.markers, idx, ret.marker || {})}; + if (Object.keys(ret.marker as object).length) { + MARKER_TO_COL.forEach((prop) => { + const val = (ret.marker as Record)[prop]; + if (typeof val === "string") { + const arr = getValueFromCol(datum, val as string); + if (arr.length) { + (ret.marker as Record)[prop] = arr; + } + } + }); + } else { + delete ret.marker; + } + const xs = getValue(datum, trace, 0) || []; + const ys = getValue(datum, trace, 1) || []; + const addIndex = getArrayValue(config.addIndex, idx, true) && !ys.length; + const baseX = addIndex ? Array.from(Array(xs.length).keys()) : xs; + const baseY = addIndex ? xs : ys; + const axisNames = config.axisNames.length > idx ? config.axisNames[idx] : ([] as string[]); + if (baseX.length) { + if (axisNames.length > 0) { + ret[axisNames[0]] = baseX; + } else { + ret.x = baseX; + } + } + if (baseY.length) { + if (axisNames.length > 1) { + ret[axisNames[1]] = baseY; + } else { + ret.y = baseY; + } + } + const baseZ = getValue(datum, trace, 2, true); + if (baseZ) { + if (axisNames.length > 2) { + ret[axisNames[2]] = baseZ; + } else { + ret.z = baseZ; + } + } + // Hack for treemap charts: create a fallback 'parents' column if needed + // This works ONLY because 'parents' is the third named axis + // (see __CHART_AXIS in gui/utils/chart_config_builder.py) + else if (config.types[idx] === "treemap" && Array.isArray(ret.labels)) { + ret.parents = Array(ret.labels.length).fill(""); + } + // Other axis + for (let i = 3; i < axisNames.length; i++) { + ret[axisNames[i]] = getValue(datum, trace, i, true); + } + ret.text = getValue(datum, config.texts, idx, true); + ret.xaxis = config.xaxis[idx]; + ret.yaxis = config.yaxis[idx]; + ret.hovertext = getValue(datum, config.labels, idx, true); + const selPoints = getArrayValue(selected, idx, []); + if (selPoints?.length) { + ret.selectedpoints = selPoints; + } + ret.orientation = getArrayValue(config.orientations, idx); + ret.line = getArrayValue(config.lines, idx); + ret.textposition = getArrayValue(config.textAnchors, idx); + const selectedMarker = getArrayValue(config.selectedMarkers, idx); + if (selectedMarker) { + ret.selected = {marker: selectedMarker}; + } + return ret as Data; + }) : []; return lastDataPl.current; }, [props.figure, selected, data, config, dataKey]); @@ -522,7 +523,7 @@ const Chart = (props: ChartProp) => { const onRelayout = useCallback( (eventData: PlotRelayoutEvent) => { - onRangeChange && dispatch(createSendActionNameAction(id, module, { action: onRangeChange, ...eventData })); + onRangeChange && dispatch(createSendActionNameAction(id, module, {action: onRangeChange, ...eventData})); if (config.decimators && !config.types.includes("scatter3d")) { const backCols = Object.values(config.columns).map((col) => col.dfid); const eventDataKey = Object.entries(eventData) @@ -577,8 +578,8 @@ const Chart = (props: ChartProp) => { ? props.figure ? index : data[dataKey].tp_index - ? (data[dataKey].tp_index[index] as number) - : index + ? (data[dataKey].tp_index[index] as number) + : index : 0, [data, dataKey, props.figure] ); @@ -616,34 +617,36 @@ const Chart = (props: ChartProp) => { return render ? ( - }> - {Array.isArray(props.figure) && props.figure.length && props.figure[0].data !== undefined ? ( - - ) : ( - - )} - +
+ }> + {Array.isArray(props.figure) && props.figure.length && props.figure[0].data !== undefined ? ( + + ) : ( + + )} + +
) : null; From d01ca1f143d2a65c9a4febe0365e221e88143210 Mon Sep 17 00:00:00 2001 From: namnguyen Date: Wed, 19 Jun 2024 14:18:40 +0700 Subject: [PATCH 5/7] move Tooltip outside the Box --- .../taipy-gui/src/components/Taipy/Chart.tsx | 66 +++++++++---------- .../taipy-gui/src/components/Taipy/Metric.tsx | 26 ++++---- 2 files changed, 44 insertions(+), 48 deletions(-) diff --git a/frontend/taipy-gui/src/components/Taipy/Chart.tsx b/frontend/taipy-gui/src/components/Taipy/Chart.tsx index ecf338854e..ef590e23e7 100644 --- a/frontend/taipy-gui/src/components/Taipy/Chart.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Chart.tsx @@ -615,40 +615,38 @@ const Chart = (props: ChartProp) => { ); return render ? ( - - -
- }> - {Array.isArray(props.figure) && props.figure.length && props.figure[0].data !== undefined ? ( - - ) : ( - - )} - -
-
-
+ + + }> + {Array.isArray(props.figure) && props.figure.length && props.figure[0].data !== undefined ? ( + + ) : ( + + )} + + + ) : null; }; diff --git a/frontend/taipy-gui/src/components/Taipy/Metric.tsx b/frontend/taipy-gui/src/components/Taipy/Metric.tsx index 6f8c64eef4..f049082965 100644 --- a/frontend/taipy-gui/src/components/Taipy/Metric.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Metric.tsx @@ -159,20 +159,18 @@ const Metric = (props: MetricProps) => { ]) return ( - - -
- }> - - -
-
-
+ + + }> + + + + ); } From cc67c5bd4fc565e2b576f065e3e68d945a5ed060 Mon Sep 17 00:00:00 2001 From: namnguyen Date: Wed, 19 Jun 2024 16:25:23 +0700 Subject: [PATCH 6/7] update title --- frontend/taipy-gui/src/components/Taipy/Metric.tsx | 11 ++++++----- taipy/gui/viselements.json | 5 ----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/frontend/taipy-gui/src/components/Taipy/Metric.tsx b/frontend/taipy-gui/src/components/Taipy/Metric.tsx index f049082965..2436b537be 100644 --- a/frontend/taipy-gui/src/components/Taipy/Metric.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Metric.tsx @@ -50,7 +50,7 @@ interface MetricProps extends TaipyBaseProps, TaipyHoverProps { template_Light_?: string; } -const emptyLayout = {} as Record>; +const emptyLayout = {} as Partial; const defaultStyle = {position: "relative", display: "inline-block"}; const Metric = (props: MetricProps) => { @@ -145,10 +145,11 @@ const Metric = (props: MetricProps) => { layout.template = template; } - return { - title: props.title || layout.title, - ...layout - } as Partial + if (props.title) { + layout.title = props.title; + } + + return layout as Partial; }, [ props.title, props.template, diff --git a/taipy/gui/viselements.json b/taipy/gui/viselements.json index 5495ffa9c7..27f3cec165 100644 --- a/taipy/gui/viselements.json +++ b/taipy/gui/viselements.json @@ -1198,11 +1198,6 @@ "name": "template[light]", "type": "dict", "doc": "The Plotly layout template applied over the base template when theme is not dark." - }, - { - "name": "hover_text", - "type": "dynamic(str)", - "doc": "The information that is displayed when the user hovers over this element." } ] } From 5a801cdf6c824801c41835362feaad24021f80d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fred=20Lef=C3=A9v=C3=A8re-Laoide?= <90181748+FredLL-Avaiga@users.noreply.github.com> Date: Thu, 20 Jun 2024 00:04:18 +0200 Subject: [PATCH 7/7] Submission status update (#1434) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Submission status update resolves #1419 * catch date format exception * add tests --------- Co-authored-by: Fred Lefévère-Laoide --- frontend/taipy-gui/src/utils/index.spec.ts | 79 ++++++++++++++++------ frontend/taipy-gui/src/utils/index.ts | 22 +++--- frontend/taipy/src/ScenarioViewer.tsx | 9 ++- frontend/taipy/src/StatusChip.tsx | 3 +- taipy/gui/_renderers/builder.py | 1 + taipy/gui/_renderers/factory.py | 27 -------- taipy/gui_core/_context.py | 40 ++++++----- 7 files changed, 99 insertions(+), 82 deletions(-) diff --git a/frontend/taipy-gui/src/utils/index.spec.ts b/frontend/taipy-gui/src/utils/index.spec.ts index 3de158f612..a5a07a51e1 100644 --- a/frontend/taipy-gui/src/utils/index.spec.ts +++ b/frontend/taipy-gui/src/utils/index.spec.ts @@ -14,7 +14,7 @@ import "@testing-library/jest-dom"; import { FormatConfig } from "../context/taipyReducers"; -import { getNumberString } from "./index"; +import { getNumberString, getDateTimeString } from "./index"; let myWarn: jest.Mock; @@ -23,49 +23,88 @@ beforeEach(() => { console.warn = myWarn; }) -const getFormatConfig = (numberFormat?: string): FormatConfig => ({timeZone: "", date: "", dateTime: "", number: numberFormat || "", forceTZ: false}) +const getNumberFormatConfig = (numberFormat?: string): FormatConfig => ({timeZone: "", date: "", dateTime: "", number: numberFormat || "", forceTZ: false}) +const getDateFormatConfig = (dateFormat?: string): FormatConfig => ({timeZone: "", date: dateFormat || "", dateTime: dateFormat || "", number: "", forceTZ: false}) describe("getNumberString", () => { it("returns straight", async () => { - expect(getNumberString(1, undefined, getFormatConfig())).toBe("1"); + expect(getNumberString(1, undefined, getNumberFormatConfig())).toBe("1"); }); it("returns formatted", async () => { - expect(getNumberString(1, "%.1f", getFormatConfig())).toBe("1.0"); + expect(getNumberString(1, "%.1f", getNumberFormatConfig())).toBe("1.0"); }); it("returns formatted float", async () => { - expect(getNumberString(1.0, "%.0f", getFormatConfig())).toBe("1"); + expect(getNumberString(1.0, "%.0f", getNumberFormatConfig())).toBe("1"); }); it("returns default formatted", async () => { - expect(getNumberString(1, "", getFormatConfig("%.1f"))).toBe("1.0"); + expect(getNumberString(1, "", getNumberFormatConfig("%.1f"))).toBe("1.0"); }); it("returns for non variable format", async () => { - expect(getNumberString(1, "toto", getFormatConfig())).toBe("toto"); + expect(getNumberString(1, "toto", getNumberFormatConfig())).toBe("toto"); }); it("returns formatted over default", async () => { - expect(getNumberString(1, "%.2f", getFormatConfig("%.1f"))).toBe("1.00"); + expect(getNumberString(1, "%.2f", getNumberFormatConfig("%.1f"))).toBe("1.00"); }); it("returns for string", async () => { - expect(getNumberString("null" as unknown as number, "", getFormatConfig("%.1f"))).toBe("null"); - expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] expecting number but found string") + expect(getNumberString("null" as unknown as number, "", getNumberFormatConfig("%.1f"))).toBe("null"); + expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] expecting number but found string") }); it("returns for object", async () => { - expect(getNumberString({t: 1} as unknown as number, "", getFormatConfig("%.1f"))).toBe(""); - expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] expecting number but found object") + expect(getNumberString({t: 1} as unknown as number, "", getNumberFormatConfig("%.1f"))).toBe(""); + expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] expecting number but found object") }); it("returns for bad format", async () => { - expect(getNumberString(1, "%.f", getFormatConfig())).toBe("1"); - expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] unexpected placeholder") + expect(getNumberString(1, "%.f", getNumberFormatConfig())).toBe("1"); + expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] unexpected placeholder") }); it("returns for null", async () => { - expect(getNumberString(null as unknown as number, "%2.f", getFormatConfig("%.1f"))).toBe(""); - expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] unexpected placeholder") + expect(getNumberString(null as unknown as number, "%2.f", getNumberFormatConfig("%.1f"))).toBe(""); + expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] unexpected placeholder") }); it("returns for undefined", async () => { - expect(getNumberString(undefined as unknown as number, "%2.f", getFormatConfig("%.1f"))).toBe(""); - expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] unexpected placeholder") + expect(getNumberString(undefined as unknown as number, "%2.f", getNumberFormatConfig("%.1f"))).toBe(""); + expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] unexpected placeholder") }); it("returns for NaN", async () => { - expect(getNumberString(NaN, "%2.f", getFormatConfig("%.1f"))).toBe("NaN"); - expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] unexpected placeholder") + expect(getNumberString(NaN, "%2.f", getNumberFormatConfig("%.1f"))).toBe("NaN"); + expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] unexpected placeholder") + }); +}); + +describe("getDateTimeString", () => { + it("returns straight", async () => { + expect(getDateTimeString("2024-10-05", undefined, getDateFormatConfig())).toContain("05 2024"); + }); + it("returns formatted", async () => { + expect(getDateTimeString("2024-10-05", "dd-MM-yy", getDateFormatConfig())).toBe("05-10-24"); + }); + it("returns default formatted", async () => { + expect(getDateTimeString("2024-10-05", "", getDateFormatConfig("dd-MM-yy"))).toBe("05-10-24"); + }); + it("returns formatted over default", async () => { + expect(getDateTimeString("2024-10-05", "dd-MM-yy", getNumberFormatConfig("yy-MM-dd"))).toBe("05-10-24"); + }); + it("returns for string", async () => { + expect(getDateTimeString("null" as unknown as string, "", getDateFormatConfig("dd-MM-yy"))).toBe("Invalid Date"); + expect(myWarn).toHaveBeenCalledWith("Invalid date format:", "Invalid time value") + }); + it("returns for object", async () => { + expect(getDateTimeString({t: 1} as unknown as string, "", getDateFormatConfig("dd-MM-yy"))).toBe("Invalid Date"); + expect(myWarn).toHaveBeenCalledWith("Invalid date format:", "Invalid time value") + }); + it("returns for bad format", async () => { + expect(getDateTimeString("2024-10-05", "D", getDateFormatConfig())).toContain("05 2024"); + expect(myWarn).toHaveBeenCalled() + expect(myWarn.mock.lastCall).toHaveLength(2) + expect(myWarn.mock.lastCall[0]).toBe("Invalid date format:") + expect(myWarn.mock.lastCall[1]).toContain("Use `d` instead of `D`") + }); + it("returns for null", async () => { + expect(getDateTimeString(null as unknown as string, "dd-MM-yy", getDateFormatConfig())).toBe("null"); + expect(myWarn).toHaveBeenCalledWith("Invalid date format:", "Invalid time value") + }); + it("returns for undefined", async () => { + expect(getDateTimeString(undefined as unknown as string, "dd-MM-yy", getDateFormatConfig())).toBe("null"); + expect(myWarn).toHaveBeenCalledWith("Invalid date format:", "Invalid time value") }); }); diff --git a/frontend/taipy-gui/src/utils/index.ts b/frontend/taipy-gui/src/utils/index.ts index aff939a8c1..cd010638c5 100644 --- a/frontend/taipy-gui/src/utils/index.ts +++ b/frontend/taipy-gui/src/utils/index.ts @@ -105,14 +105,20 @@ export const getDateTimeString = ( tz?: string, withTime: boolean = true ): string => { - if (withTime) { - return formatInTimeZone( - getDateTime(value) || "", - formatConf.forceTZ || !tz ? formatConf.timeZone : tz, - datetimeformat || formatConf.dateTime - ); + const dateVal = getDateTime(value); + try { + if (withTime) { + return formatInTimeZone( + dateVal || "", + formatConf.forceTZ || !tz ? formatConf.timeZone : tz, + datetimeformat || formatConf.dateTime + ); + } + return format(dateVal || 0, datetimeformat || formatConf.date); + } catch (e) { + console.warn("Invalid date format:", (e as Error).message || e); + return `${dateVal}`; } - return format(getDateTime(value) || 0, datetimeformat || formatConf.date); }; export const getNumberString = (value: number, numberformat: string | undefined, formatConf: FormatConfig): string => { @@ -121,7 +127,7 @@ export const getNumberString = (value: number, numberformat: string | undefined, ? sprintf(numberformat || formatConf.number, value) : value.toLocaleString(); } catch (e) { - console.warn("getNumberString: " + (e as Error).message || e); + console.warn("Invalid number format:", (e as Error).message || e); return ( (typeof value === "number" && value.toLocaleString()) || (typeof value === "string" && (value as string)) || diff --git a/frontend/taipy/src/ScenarioViewer.tsx b/frontend/taipy/src/ScenarioViewer.tsx index e36854070f..1daea5fd38 100644 --- a/frontend/taipy/src/ScenarioViewer.tsx +++ b/frontend/taipy/src/ScenarioViewer.tsx @@ -366,7 +366,6 @@ const ScenarioViewer = (props: ScenarioViewerProps) => { } } setValid(!!sc); - // setSubmissionStatus(0); setScenario((oldSc) => (oldSc === sc ? oldSc : sc ? (deepEqual(oldSc, sc) ? oldSc : sc) : invalidScenario)); }, [props.scenario, props.defaultScenario]); @@ -579,7 +578,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => { const addSequenceHandler = useCallback(() => setSequences((seq) => [...seq, ["", [], "", true]]), []); // Submission status - const [submissionStatus, setSubmissionStatus] = useState(0); + const [submissionStatus, setSubmissionStatus] = useState(-1); // on scenario change useEffect(() => { @@ -594,10 +593,10 @@ const ScenarioViewer = (props: ScenarioViewerProps) => { useEffect(() => { const ids = props.coreChanged?.scenario; if (typeof ids === "string" ? ids === scId : Array.isArray(ids) ? ids.includes(scId) : ids) { - props.updateVarName && dispatch(createRequestUpdateAction(id, module, [props.updateVarName], true)); - if (props.coreChanged?.submission !== undefined) { + if (typeof props.coreChanged?.submission === "number") { setSubmissionStatus(props.coreChanged?.submission as number); } + props.updateVarName && dispatch(createRequestUpdateAction(id, module, [props.updateVarName], true)); } }, [props.coreChanged, props.updateVarName, id, module, dispatch, scId]); @@ -629,7 +628,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => { sx={ChipSx} /> ) : null} - {submissionStatus ? : null} + {submissionStatus > -1 ? : null} {showSubmit ? ( diff --git a/frontend/taipy/src/StatusChip.tsx b/frontend/taipy/src/StatusChip.tsx index 96810abd9e..3ca25773f5 100644 --- a/frontend/taipy/src/StatusChip.tsx +++ b/frontend/taipy/src/StatusChip.tsx @@ -3,7 +3,8 @@ import { SxProps, Theme } from "@mui/material"; import Chip from "@mui/material/Chip"; export enum Status { - SUBMITTED = 1, + SUBMITTED = 0, + UNDEFINED = 1, BLOCKED = 2, PENDING = 3, RUNNING = 4, diff --git a/taipy/gui/_renderers/builder.py b/taipy/gui/_renderers/builder.py index 0cde9bcff1..0c254589d9 100644 --- a/taipy/gui/_renderers/builder.py +++ b/taipy/gui/_renderers/builder.py @@ -946,6 +946,7 @@ def set_attributes(self, attributes: t.List[tuple]): # noqa: C901 attributes (list(tuple)): The list of attributes as (property name, property type, default value). """ + attributes.append(("id",)) # Every element should have an id attribute for attr in attributes: if not isinstance(attr, tuple): attr = (attr,) diff --git a/taipy/gui/_renderers/factory.py b/taipy/gui/_renderers/factory.py index 21fedb15fe..4c8fb40d7e 100644 --- a/taipy/gui/_renderers/factory.py +++ b/taipy/gui/_renderers/factory.py @@ -78,7 +78,6 @@ class _Factory: .set_value_and_default(with_update=False) .set_attributes( [ - ("id",), ("on_action", PropertyType.function), ("active", PropertyType.dynamic_boolean, True), ("hover_text", PropertyType.dynamic_string), @@ -90,7 +89,6 @@ class _Factory: .set_value_and_default(with_update=True, with_default=False, var_type=PropertyType.data) .set_attributes( [ - ("id",), ("on_action", PropertyType.function), ("active", PropertyType.dynamic_boolean, True), ("hover_text", PropertyType.dynamic_string), @@ -107,7 +105,6 @@ class _Factory: .set_value_and_default(with_default=False, var_type=PropertyType.data) .set_attributes( [ - ("id",), ("title",), ("width", PropertyType.string_or_number), ("height", PropertyType.string_or_number), @@ -140,7 +137,6 @@ class _Factory: .set_attributes( [ ("with_time", PropertyType.boolean), - ("id",), ("active", PropertyType.dynamic_boolean, True), ("editable", PropertyType.dynamic_boolean, True), ("hover_text", PropertyType.dynamic_string), @@ -160,7 +156,6 @@ class _Factory: .set_attributes( [ ("with_time", PropertyType.boolean), - ("id",), ("active", PropertyType.dynamic_boolean, True), ("editable", PropertyType.dynamic_boolean, True), ("hover_text", PropertyType.dynamic_string), @@ -181,7 +176,6 @@ class _Factory: ._set_partial() # partial should be set before page .set_attributes( [ - ("id",), ("page",), ("title",), ("on_action", PropertyType.function), @@ -201,7 +195,6 @@ class _Factory: ._set_partial() # partial should be set before page .set_attributes( [ - ("id",), ("page",), ("expanded", PropertyType.dynamic_boolean, True, True, False), ("hover_text", PropertyType.dynamic_string), @@ -218,7 +211,6 @@ class _Factory: ._set_content("content", image=False) .set_attributes( [ - ("id",), ("on_action", PropertyType.function), ("active", PropertyType.dynamic_boolean, True), ("render", PropertyType.dynamic_boolean, True), @@ -238,7 +230,6 @@ class _Factory: ._set_file_content() .set_attributes( [ - ("id",), ("on_action", PropertyType.function), ("active", PropertyType.dynamic_boolean, True), ("multiple", PropertyType.boolean, False), @@ -258,7 +249,6 @@ class _Factory: ._set_content("content") .set_attributes( [ - ("id",), ("on_action", PropertyType.function), ("active", PropertyType.dynamic_boolean, True), ("width",), @@ -275,7 +265,6 @@ class _Factory: .set_value_and_default(with_update=False, native_type=True) .set_attributes( [ - ("id",), ("min", PropertyType.number), ("max", PropertyType.number), ("value", PropertyType.dynamic_number), @@ -296,7 +285,6 @@ class _Factory: ._set_propagate() .set_attributes( [ - ("id",), ("active", PropertyType.dynamic_boolean, True), ("hover_text", PropertyType.dynamic_string), ("on_change", PropertyType.function), @@ -314,7 +302,6 @@ class _Factory: .set_value_and_default(with_default=False) .set_attributes( [ - ("id",), ("columns[mobile]",), ("gap",), ] @@ -325,7 +312,6 @@ class _Factory: .set_value_and_default(default_val="Log-in") .set_attributes( [ - ("id",), ("message", PropertyType.dynamic_string), ("on_action", PropertyType.function, "on_login"), ] @@ -338,7 +324,6 @@ class _Factory: ) .set_attributes( [ - ("id",), ("active", PropertyType.dynamic_boolean, True), ("label",), ("width",), @@ -359,7 +344,6 @@ class _Factory: .set_value_and_default(var_type=PropertyType.dynamic_number, native_type=True) .set_attributes( [ - ("id",), ("title",), ("active", PropertyType.dynamic_boolean, True), ("layout", PropertyType.dynamic_dict), @@ -383,7 +367,6 @@ class _Factory: gui=gui, control_type=control_type, element_name="NavBar", attributes=attrs, default_value=None ).set_attributes( [ - ("id",), ("active", PropertyType.dynamic_boolean, True), ("hover_text", PropertyType.dynamic_string), ("lov", PropertyType.single_lov), @@ -401,7 +384,6 @@ class _Factory: ._set_propagate() .set_attributes( [ - ("id",), ("active", PropertyType.dynamic_boolean, True), ("hover_text", PropertyType.dynamic_string), ("on_change", PropertyType.function), @@ -417,7 +399,6 @@ class _Factory: ._set_partial() # partial should be set before page .set_attributes( [ - ("id",), ("page",), ("anchor", PropertyType.string, "left"), ("on_close", PropertyType.function), @@ -436,7 +417,6 @@ class _Factory: ._set_partial() # partial should be set before page .set_attributes( [ - ("id",), ("page", PropertyType.dynamic_string), ("render", PropertyType.dynamic_boolean, True), ("height", PropertyType.dynamic_string), @@ -454,7 +434,6 @@ class _Factory: ("filter", PropertyType.boolean), ("height", PropertyType.string_or_number), ("hover_text", PropertyType.dynamic_string), - ("id",), ("value_by_id", PropertyType.boolean), ("multiple", PropertyType.boolean), ("width", PropertyType.string_or_number), @@ -478,7 +457,6 @@ class _Factory: ("active", PropertyType.dynamic_boolean, True), ("height",), ("hover_text", PropertyType.dynamic_string), - ("id",), ("value_by_id", PropertyType.boolean), ("max", PropertyType.number, 100), ("min", PropertyType.number, 0), @@ -503,7 +481,6 @@ class _Factory: .set_value_and_default(with_update=False) .set_attributes( [ - ("id",), ("without_close", PropertyType.boolean, False), ("hover_text", PropertyType.dynamic_string), ] @@ -524,7 +501,6 @@ class _Factory: ("auto_loading", PropertyType.boolean), ("width", PropertyType.string_or_number, "100%"), ("height", PropertyType.string_or_number, "80vh"), - ("id",), ("active", PropertyType.dynamic_boolean, True), ("editable", PropertyType.dynamic_boolean, True), ("on_edit", PropertyType.function), @@ -552,7 +528,6 @@ class _Factory: .set_attributes( [ ("format",), - ("id",), ("hover_text", PropertyType.dynamic_string), ("raw", PropertyType.boolean, False), ("mode",), @@ -566,7 +541,6 @@ class _Factory: [ ("active", PropertyType.dynamic_boolean, True), ("hover_text", PropertyType.dynamic_string), - ("id",), ("label",), ("value_by_id", PropertyType.boolean), ("unselected_value", PropertyType.string, ""), @@ -592,7 +566,6 @@ class _Factory: ("filter", PropertyType.boolean), ("hover_text", PropertyType.dynamic_string), ("height", PropertyType.string_or_number), - ("id",), ("value_by_id", PropertyType.boolean), ("multiple", PropertyType.boolean), ("width", PropertyType.string_or_number), diff --git a/taipy/gui_core/_context.py b/taipy/gui_core/_context.py index 0242d60442..17ebc79a7b 100644 --- a/taipy/gui_core/_context.py +++ b/taipy/gui_core/_context.py @@ -119,10 +119,7 @@ def process_event(self, event: Event): else None ) if sequence and hasattr(sequence, "parent_ids") and sequence.parent_ids: # type: ignore - self.gui._broadcast( - _GuiCoreContext._CORE_CHANGED_NAME, - {"scenario": list(sequence.parent_ids)}, # type: ignore - ) + self.broadcast_core_changed({"scenario": list(sequence.parent_ids)}) except Exception as e: _warn(f"Access to sequence {event.entity_id} failed", e) elif event.entity_type == EventEntityType.JOB: @@ -133,25 +130,26 @@ def process_event(self, event: Event): elif event.entity_type == EventEntityType.DATA_NODE: with self.lock: self.data_nodes_by_owner = None - self.gui._broadcast( - _GuiCoreContext._CORE_CHANGED_NAME, - {"datanode": event.entity_id if event.operation != EventOperation.DELETION else True}, + self.broadcast_core_changed( + {"datanode": event.entity_id if event.operation != EventOperation.DELETION else True} ) + def broadcast_core_changed(self, payload: t.Dict[str, t.Any], client_id: t.Optional[str] = None): + self.gui._broadcast(_GuiCoreContext._CORE_CHANGED_NAME, payload, client_id) + def scenario_refresh(self, scenario_id: t.Optional[str]): with self.lock: self.scenario_by_cycle = None self.data_nodes_by_owner = None - self.gui._broadcast( - _GuiCoreContext._CORE_CHANGED_NAME, - {"scenario": scenario_id or True}, - ) + self.broadcast_core_changed({"scenario": scenario_id or True}) def submission_status_callback(self, submission_id: t.Optional[str] = None, event: t.Optional[Event] = None): if not submission_id or not is_readable(t.cast(SubmissionId, submission_id)): return submission = None new_status = None + payload: t.Optional[t.Dict[str, t.Any]] = None + client_id: t.Optional[str] = None try: last_status = self.client_submission.get(submission_id) if not last_status: @@ -161,6 +159,7 @@ def submission_status_callback(self, submission_id: t.Optional[str] = None, even if not submission or not submission.entity_id: return + payload = {} new_status = t.cast(SubmissionStatus, submission.submission_status) client_id = submission.properties.get("client_id") @@ -176,7 +175,7 @@ def submission_status_callback(self, submission_id: t.Optional[str] = None, even if job.is_pending() else None ) - self.gui._broadcast(_GuiCoreContext._CORE_CHANGED_NAME, {"tasks": running_tasks}, client_id) + payload.update(tasks=running_tasks) if last_status != new_status: # callback @@ -210,15 +209,14 @@ def submission_status_callback(self, submission_id: t.Optional[str] = None, even _warn(f"Submission ({submission_id}) is not available", e) finally: - entity_id = submission.entity_id if submission else None - self.gui._broadcast( - _GuiCoreContext._CORE_CHANGED_NAME, - { - "jobs": True, - "scenario": entity_id or False, - "submission": new_status.value if new_status else None, - }, - ) + if payload is not None: + payload.update(jobs=True) + entity_id = submission.entity_id if submission else None + if entity_id: + payload.update(scenario=entity_id) + if new_status: + payload.update(submission=new_status.value) + self.broadcast_core_changed(payload, client_id) def no_change_adapter(self, entity: t.List): return entity