Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

table csv download #944

Merged
merged 3 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
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,
Expand Down Expand Up @@ -61,6 +62,7 @@
getTooltip,
defaultColumns,
OnRowClick,
DownloadAction,
} from "./tableUtils";
import {
useClassNames,
Expand Down Expand Up @@ -181,6 +183,7 @@
onAction = "",
size = DEFAULT_SIZE,
userData,
downloadable = false,
} = props;
const [rows, setRows] = useState<RowType[]>([]);
const [rowCount, setRowCount] = useState(1000); // need something > 0 to bootstrap the infinite loader
Expand Down Expand Up @@ -274,7 +277,7 @@
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);

Check warning on line 280 in frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 280 in frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 280 in frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
const colsOrder = Object.keys(baseColumns).sort(getsortByIndex(baseColumns));
const styTt = colsOrder.reduce<Record<string, Record<string, string>>>((pv, col) => {
if (baseColumns[col].style) {
Expand Down Expand Up @@ -305,7 +308,7 @@
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]);

Expand Down Expand Up @@ -405,6 +408,17 @@
[visibleStartIndex, dispatch, updateVarName, onAdd, module, userData]
);

const onDownload = useCallback(
() =>

Check warning on line 412 in frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
dispatch(
createSendActionNameAction(updateVarName, module, {
action: DownloadAction,
user_data: userData,
})

Check warning on line 417 in frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
),
[dispatch, updateVarName, module, userData]
);

const isItemLoaded = useCallback((index: number) => index < rows.length && !!rows[index], [rows]);

const onCellValidation: OnCellValidation = useCallback(
Expand Down Expand Up @@ -528,28 +542,39 @@
sx={columns[col].width ? { width: columns[col].width } : {}}
>
{columns[col].dfid === EDIT_COL ? (
[
active && onAdd ? (
<Tooltip title="Add a row" key="addARow">
<IconButton
onClick={onAddRowClick}
size="small"
sx={iconInRowSx}
>
<AddIcon fontSize="inherit" />
</IconButton>
</Tooltip>
) : null,
active && filter ? (
<TableFilter
key="filter"
columns={columns}
colsOrder={colsOrder}
onValidate={setAppliedFilters}
appliedFilters={appliedFilters}
className={className}
/>
) : null,
active && downloadable ? (

Check warning on line 567 in frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 567 in frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
<Tooltip title="Download as CSV" key="downloadCsv">

Check warning on line 568 in frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
<IconButton
onClick={onDownload}
size="small"
sx={iconInRowSx}
>
<Download fontSize="inherit" />
</IconButton>
</Tooltip>
) : null,

Check warning on line 577 in frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
]

Check warning on line 578 in frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
) : (
<TableSortLabel
Expand Down
29 changes: 27 additions & 2 deletions frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
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 { createRequestTableUpdateAction, createSendActionNameAction } from "../../context/taipyReducers";
import {
Expand Down Expand Up @@ -67,6 +68,7 @@
getRowIndex,
getTooltip,
OnRowClick,
DownloadAction,
} from "./tableUtils";
import {
useClassNames,
Expand Down Expand Up @@ -102,6 +104,7 @@
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<Record<string, unknown>>({});
Expand Down Expand Up @@ -142,7 +145,7 @@
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);

Check warning on line 148 in frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 148 in frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
const colsOrder = Object.keys(baseColumns).sort(getsortByIndex(baseColumns));
const styTt = colsOrder.reduce<Record<string, Record<string, string>>>((pv, col) => {
if (baseColumns[col].style) {
Expand Down Expand Up @@ -173,7 +176,7 @@
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);

Expand Down Expand Up @@ -311,6 +314,17 @@
[startIndex, dispatch, updateVarName, onAdd, module, userData]
);

const onDownload = useCallback(
() =>

Check warning on line 318 in frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
dispatch(
createSendActionNameAction(updateVarName, module, {
action: DownloadAction,
user_data: userData,
})

Check warning on line 323 in frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
),
[dispatch, updateVarName, module, userData]
);

const tableContainerSx = useMemo(() => ({ maxHeight: height }), [height]);

const pso = useMemo(() => {
Expand Down Expand Up @@ -441,6 +455,17 @@
className={className}
/>
) : null,
active && downloadable ? (
<Tooltip title="Download as CSV" key="downloadCsv">

Check warning on line 459 in frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
<IconButton
onClick={onDownload}
size="small"
sx={iconInRowSx}
>
<Download fontSize="inherit" />
</IconButton>
</Tooltip>
) : null,
]
) : (
<TableSortLabel
Expand Down
3 changes: 3 additions & 0 deletions frontend/taipy-gui/src/components/Taipy/tableUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,11 @@ export interface TaipyTableProps extends TaipyActiveProps, TaipyMultiSelectProps
size?: "small" | "medium";
defaultKey?: string; // for testing purposes only
userData?: unknown;
downloadable?: boolean;
}

export const DownloadAction = "__Taipy__download_csv";

export type PageSizeOptionsType = (
| number
| {
Expand Down
1 change: 1 addition & 0 deletions taipy/gui/_renderers/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ class _Factory:
("filter", PropertyType.boolean),
("hover_text", PropertyType.dynamic_string),
("size",),
("downloadable", PropertyType.boolean),
]
)
._set_propagate()
Expand Down
7 changes: 6 additions & 1 deletion taipy/gui/data/pandas_data_accessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ def __get_data( # noqa: C901
except Exception as e:
_warn(f"Dataframe filtering: invalid query '{query}' on {value.head()}", e)

dictret: t.Optional[t.Dict[str, t.Any]]
if paged:
aggregates = payload.get("aggregates")
applies = payload.get("applies")
Expand Down Expand Up @@ -375,7 +376,11 @@ def __get_data( # noqa: C901
except Exception as e:
_warn(f"Limit rows error with {decimator} for Dataframe", e)
value = self.__build_transferred_cols(gui, columns, t.cast(pd.DataFrame, value), is_copied=is_copied)
dictret = self.__format_data(value, data_format, "list", data_extraction=True)
if payload.get("csv") is True:
ret_payload["df"] = value
dictret = None
else:
dictret = self.__format_data(value, data_format, "list", data_extraction=True)
ret_payload["value"] = dictret
return ret_payload

Expand Down
54 changes: 45 additions & 9 deletions taipy/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import json
import math
import os
import pathlib
import re
import sys
import tempfile
Expand All @@ -26,6 +25,8 @@
import warnings
from importlib import metadata, util
from importlib.util import find_spec
from pathlib import Path
from tempfile import mkstemp
from types import FrameType, FunctionType, LambdaType, ModuleType, SimpleNamespace
from urllib.parse import unquote, urlencode, urlparse

Expand Down Expand Up @@ -224,6 +225,8 @@ class Gui:
_HTML_CONTENT_KEY = "__taipy_html_content"
__USER_CONTENT_CB = "custom_user_content_cb"
__ROBOTO_FONT = "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
__DOWNLOAD_ACTION = "__Taipy__download_csv"
__DOWNLOAD_DELETE_ACTION = "__Taipy__download_delete_csv"

__RE_HTML = re.compile(r"(.*?)\.html$")
__RE_MD = re.compile(r"(.*?)\.md$")
Expand Down Expand Up @@ -342,7 +345,7 @@ def __init__(

# get taipy version
try:
gui_file = pathlib.Path(__file__ or ".").resolve()
gui_file = Path(__file__ or ".").resolve()
with open(gui_file.parent / "version.json") as version_file:
self.__version = json.load(version_file)
except Exception as e: # pragma: no cover
Expand Down Expand Up @@ -898,7 +901,7 @@ def __append_libraries_to_status(self, status: t.Dict[str, t.Any]):
elts.append(elt_dict)
status.update({"libraries": libraries})

def _serve_status(self, template: pathlib.Path) -> 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(
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -1320,14 +1323,47 @@ 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):
FredLL-Avaiga marked this conversation as resolved.
Show resolved Hide resolved
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")
else:
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.")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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():
Expand All @@ -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())
Expand Down
5 changes: 5 additions & 0 deletions taipy/gui/viselements.json
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,11 @@
"name": "lov[<i>column_name</i>]",
"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."
}
]
}
Expand Down
Loading