diff --git a/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx b/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx index 961977d9ec..1c281e652c 100644 --- a/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx +++ b/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx @@ -30,6 +30,7 @@ import Tooltip from "@mui/material/Tooltip"; import AddIcon from "@mui/icons-material/Add"; import DataSaverOn from "@mui/icons-material/DataSaverOn"; import DataSaverOff from "@mui/icons-material/DataSaverOff"; +import Download from "@mui/icons-material/Download"; import { createRequestInfiniteTableUpdateAction, @@ -61,6 +62,7 @@ import { getTooltip, defaultColumns, OnRowClick, + DownloadAction, } from "./tableUtils"; import { useClassNames, @@ -181,6 +183,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => { onAction = "", size = DEFAULT_SIZE, userData, + downloadable = false, } = props; const [rows, setRows] = useState([]); const [rowCount, setRowCount] = useState(1000); // need something > 0 to bootstrap the infinite loader @@ -274,7 +277,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => { col.tooltip = props.tooltip; } }); - addDeleteColumn((active && (onAdd || onDelete) ? 1 : 0) + (active && filter ? 1 : 0), baseColumns); + addDeleteColumn((active && (onAdd || onDelete) ? 1 : 0) + (active && filter ? 1 : 0) + (active && downloadable ? 1 : 0), baseColumns); const colsOrder = Object.keys(baseColumns).sort(getsortByIndex(baseColumns)); const styTt = colsOrder.reduce>>((pv, col) => { if (baseColumns[col].style) { @@ -305,7 +308,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => { hNan, false, ]; - }, [active, editable, onAdd, onDelete, baseColumns, props.lineStyle, props.tooltip, props.nanValue, props.filter]); + }, [active, editable, onAdd, onDelete, baseColumns, props.lineStyle, props.tooltip, props.nanValue, props.filter, downloadable]); const boxBodySx = useMemo(() => ({ height: height }), [height]); @@ -405,6 +408,17 @@ const AutoLoadingTable = (props: TaipyTableProps) => { [visibleStartIndex, dispatch, updateVarName, onAdd, module, userData] ); + const onDownload = useCallback( + () => + dispatch( + createSendActionNameAction(updateVarName, module, { + action: DownloadAction, + user_data: userData, + }) + ), + [dispatch, updateVarName, module, userData] + ); + const isItemLoaded = useCallback((index: number) => index < rows.length && !!rows[index], [rows]); const onCellValidation: OnCellValidation = useCallback( @@ -550,6 +564,17 @@ const AutoLoadingTable = (props: TaipyTableProps) => { className={className} /> ) : null, + active && downloadable ? ( + + + + + + ) : null, ] ) : ( { width = "100%", size = DEFAULT_SIZE, userData, + downloadable = false, } = props; const pageSize = props.pageSize === undefined || props.pageSize < 1 ? 100 : Math.round(props.pageSize); const [value, setValue] = useState>({}); @@ -142,7 +145,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => { col.tooltip = props.tooltip; } }); - addDeleteColumn((active && (onAdd || onDelete) ? 1 : 0) + (active && filter ? 1 : 0), baseColumns); + addDeleteColumn((active && (onAdd || onDelete) ? 1 : 0) + (active && filter ? 1 : 0) + (active && downloadable ? 1 : 0), baseColumns); const colsOrder = Object.keys(baseColumns).sort(getsortByIndex(baseColumns)); const styTt = colsOrder.reduce>>((pv, col) => { if (baseColumns[col].style) { @@ -173,7 +176,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => { hNan, false, ]; - }, [active, editable, onAdd, onDelete, baseColumns, props.lineStyle, props.tooltip, props.nanValue, props.filter]); + }, [active, editable, onAdd, onDelete, baseColumns, props.lineStyle, props.tooltip, props.nanValue, props.filter, downloadable]); useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars); @@ -311,6 +314,17 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => { [startIndex, dispatch, updateVarName, onAdd, module, userData] ); + const onDownload = useCallback( + () => + dispatch( + createSendActionNameAction(updateVarName, module, { + action: DownloadAction, + user_data: userData, + }) + ), + [dispatch, updateVarName, module, userData] + ); + const tableContainerSx = useMemo(() => ({ maxHeight: height }), [height]); const pso = useMemo(() => { @@ -441,6 +455,17 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => { className={className} /> ) : null, + active && downloadable ? ( + + + + + + ) : null, ] ) : ( t.Dict[str, t.Dict[str, str]]: + def _serve_status(self, template: Path) -> t.Dict[str, t.Dict[str, str]]: base_json: t.Dict[str, t.Any] = {"user_status": str(self.__call_on_status() or "")} if self._get_config("extended_status", False): base_json.update( @@ -942,7 +945,7 @@ def __upload_files(self): suffix = f".part.{part}" complete = part == total - 1 if file: # and allowed_file(file.filename) - upload_path = pathlib.Path(self._get_config("upload_folder", tempfile.gettempdir())).resolve() + upload_path = Path(self._get_config("upload_folder", tempfile.gettempdir())).resolve() file_path = _get_non_existent_file_path(upload_path, secure_filename(file.filename)) file.save(str(upload_path / (file_path.name + suffix))) if complete: @@ -1320,6 +1323,31 @@ def _get_user_instance(self, class_name: str, class_type: type) -> t.Union[objec cls = self.__locals_context.get_default().get(class_name) return cls if isinstance(cls, class_type) else class_name + def __download_csv(self, state: State, var_name: str, payload: dict): + holder_name = t.cast(str, payload.get("var_name")) + ret = self._accessors._get_data( + self, + holder_name, + _getscopeattr(self, holder_name, None), + {"alldata": True, "csv": True}, + ) + if isinstance(ret, dict): + df = ret.get("df") + try: + fd, temp_path = mkstemp(".csv", var_name, text=True) + with os.fdopen(fd, "wt", newline="") as csv_file: + df.to_csv(csv_file, index=False) # type:ignore + self._download(temp_path, "data.csv", Gui.__DOWNLOAD_DELETE_ACTION) + except Exception as e: # pragma: no cover + if not self._call_on_exception("download_csv", e): + _warn("download_csv(): Exception raised", e) + + def __delete_csv(self, state: State, var_name: str, payload: dict): + try: + (Path(tempfile.gettempdir()) / t.cast(str, payload.get("args", [])[-1]).split("/")[-1]).unlink(True) + except Exception: + pass + def __on_action(self, id: t.Optional[str], payload: t.Any) -> None: if isinstance(payload, dict): action = payload.get("action") @@ -1327,7 +1355,15 @@ def __on_action(self, id: t.Optional[str], payload: t.Any) -> None: action = str(payload) payload = {"action": action} if action: - if self.__call_function_with_args(action_function=self._get_user_function(action), id=id, payload=payload): + action_fn: t.Union[t.Callable, str] + if Gui.__DOWNLOAD_ACTION == action: + action_fn = self.__download_csv + payload["var_name"] = id + elif Gui.__DOWNLOAD_DELETE_ACTION == action: + action_fn = self.__delete_csv + else: + action_fn = self._get_user_function(action) + if self.__call_function_with_args(action_function=action_fn, id=id, payload=payload): return else: # pragma: no cover _warn(f"on_action(): '{action}' is not a valid function.") @@ -1911,7 +1947,7 @@ def _download( else: _warn("download() on_action is invalid.") content_str = self._get_content("Gui.download", content, False) - self.__send_ws_download(content_str, str(name), str(on_action)) + self.__send_ws_download(content_str, str(name), str(on_action) if on_action is not None else "") def _notify( self, @@ -2133,7 +2169,7 @@ def _set_frame(self, frame: t.Optional[FrameType]): def _set_css_file(self, css_file: t.Optional[str] = None): if css_file is None: - script_file = pathlib.Path(self.__frame.f_code.co_filename or ".").resolve() + script_file = Path(self.__frame.f_code.co_filename or ".").resolve() if script_file.with_suffix(".css").exists(): css_file = f"{script_file.stem}.css" elif script_file.is_dir() and (script_file / "taipy.css").exists(): @@ -2146,9 +2182,9 @@ def _set_state(self, state: State): def _get_webapp_path(self): _conf_webapp_path = ( - pathlib.Path(self._get_config("webapp_path", None)) if self._get_config("webapp_path", None) else None + Path(self._get_config("webapp_path", None)) if self._get_config("webapp_path", None) else None ) - _webapp_path = str((pathlib.Path(__file__).parent / "webapp").resolve()) + _webapp_path = str((Path(__file__).parent / "webapp").resolve()) if _conf_webapp_path: if _conf_webapp_path.is_dir(): _webapp_path = str(_conf_webapp_path.resolve()) diff --git a/taipy/gui/viselements.json b/taipy/gui/viselements.json index b77b34a7b8..82af1b2a42 100644 --- a/taipy/gui/viselements.json +++ b/taipy/gui/viselements.json @@ -1124,6 +1124,11 @@ "name": "lov[column_name]", "type": "list[str]|str", "doc": "The list of values of the indicated column." + }, + { + "name": "downloadable", + "type": "boolean", + "doc": "The indicator that would show the icon to allow the user to download the data as a csv file if True." } ] }