diff --git a/ipygee/asset.py b/ipygee/asset.py index 379ad46..aaeb348 100644 --- a/ipygee/asset.py +++ b/ipygee/asset.py @@ -1,7 +1,8 @@ """The asset manager widget code and functionalities.""" from __future__ import annotations -from typing import List +from pathlib import Path +from typing import List, Optional import ee import geetools # noqa @@ -59,6 +60,18 @@ class AssetManager(v.Flex, HasSideCar): w_card: v.Card "The card hosting the list of items" + w_delete_dialog: v.Dialog + "The dialog to confirm the deletion of an asset" + + w_move_dialog: v.Dialog + "The dialog to confirm the move of an asset" + + w_asset_dialog: v.Dialog + "The dialog to view an asset" + + w_create_dialog: v.Dialog + "The dialog to create a new folder" + def __init__(self): """Initialize the class.""" # start by defining al the widgets @@ -66,17 +79,17 @@ def __init__(self): # fmt: off # add a line of buttons to reload and add new projects - self.w_new = v.Btn(color="error", children="NEW", elevation=2, class_="ma-1") + self.w_new = v.Btn(color="error", children="NEW", elevation=2, class_="ma-1", disabled=True) self.w_reload = v.Btn(children=[v.Icon(color="primary", children="mdi-reload")], elevation=2, class_="ma-1") - self.w_search = v.Btn(children=[v.Icon(color="primary", children="mdi-magnify")], elevation=2, class_="ma-1") + self.w_search = v.Btn(children=[v.Icon(color="primary", children="mdi-magnify")], elevation=2, class_="ma-1", disabled=True) w_main_line = v.Flex(children=[self.w_new, self.w_reload, self.w_search]) # generate the asset selector and the CRUD buttons self.w_selected = v.TextField(readonly=True, placeholder="Selected item", v_model="", clearable=True, outlined=True, class_="ma-1") - self.w_view = v.Btn(children=[v.Icon(color="primary", children="mdi-eye")]) - self.w_copy = v.Btn(children=[v.Icon(color="primary", children="mdi-content-copy")]) - self.w_move = v.Btn(children=[v.Icon(color="primary", children="mdi-file-move")]) - self.w_delete = v.Btn(children=[v.Icon(color="primary", children="mdi-trash-can")]) + self.w_view = v.Btn(children=[v.Icon(color="primary", children="mdi-eye")], disabled=True) + self.w_copy = v.Btn(children=[v.Icon(color="primary", children="mdi-content-copy")], disabled=True) + self.w_move = v.Btn(children=[v.Icon(color="primary", children="mdi-file-move")], disabled=True) + self.w_delete = v.Btn(children=[v.Icon(color="primary", children="mdi-trash-can")], disabled=True) w_btn_list = v.ItemGroup(class_="ma-1 v-btn-toggle",children=[self.w_view, self.w_copy, self.w_move, self.w_delete]) w_selected_line = v.Layout(row=True, children=[w_btn_list, self.w_selected], class_="ma-1") @@ -85,13 +98,34 @@ def __init__(self): self.w_list = v.List(dense=True, v_model=True, children=[w_group], outlined=True) self.w_card = v.Card(children=[self.w_list], outlined=True, class_="ma-1") - super().__init__(children=[w_main_line, w_selected_line, self.w_card], v_model="", class_="ma-1") + # create the hidden dialogs + self.w_delete_dialog = DeleteAssetDialog() + self.w_move_dialog = MoveAssetDialog() + self.w_asset_dialog = AssetDialog() + self.w_create_dialog = CreateFolderDialog() + + super().__init__(children=[ + self.w_delete_dialog, self.w_move_dialog, self.w_asset_dialog, self.w_create_dialog, + w_main_line, w_selected_line, self.w_card + ], v_model="", class_="ma-1") # fmt: on + # update the template of the DOM object to add a js method to copy to clipboard + # template with js behaviour + js_dir = Path(__file__).parent / "js" + clip = (js_dir / "jupyter_clip.js").read_text() + self.template = "" "" % clip + # add JS behaviour t.link((self, "selected_item"), (self, "v_model")) self.w_list.children[0].observe(self.on_item_select, "v_model") self.w_reload.on_event("click", self.on_reload) + self.w_copy.on_event("click", self.on_copy) + self.w_delete.on_event("click", self.on_delete) + self.w_selected.observe(self.activate_buttons, "v_model") + self.w_move.on_event("click", self.on_move) + self.w_view.on_event("click", self.on_view) + self.w_new.on_event("click", self.on_new) def get_items(self) -> List[v.ListItem]: """Create the list of items inside a folder.""" @@ -169,3 +203,341 @@ def on_item_select(self, change: dict): def on_reload(self, *args): """Reload the current folder.""" self.on_item_select(change={"new": self.folder}) + + def on_copy(self, *args): + """Copy the selected item to clipboard.""" + self.send({"method": "clip", "args": [self.w_selected.v_model]}) + self.w_copy.children[0].children = ["mdi-check"] + + @switch("loading", "disabled", member="w_card") + def on_delete(self, *args): + """Delete the selected item. + + Ask for confirmation before deleting via a dialog window. + """ + # make sure the current item is deletable. We can only delete assets i.e. + # files and folders. Projects and buckets are not deletable. + selected = self.w_selected.v_model + if selected in [".", ""] or ee.Asset(selected).is_project(): + return + + # open the delete dialog with the current file + self.w_delete_dialog.reload(ee.Asset(selected)) + self.w_delete_dialog.value = True + + @switch("loading", "disabled", member="w_card") + def on_move(self, *args): + """Copy the selected item. + + Ask for confirmation before moving via a dialog window. + """ + # make sure the current item is moveable. We can only move assets i.e. + # files and folders. Projects and buckets are not deletable. + selected = self.w_selected.v_model + if selected in [".", ""] or ee.Asset(selected).is_project(): + return + + # open the delete dialog with the current file + self.w_move_dialog.reload(ee.Asset(selected)) + self.w_move_dialog.value = True + + @switch("loading", "disabled", member="w_card") + def on_view(self, *args): + """Open the view dialog.""" + # make sure the current item is moveable. We can only move assets i.e. + # files and folders. Projects and buckets are not deletable. + selected = ee.Asset(self.w_selected.v_model) + if self.w_selected.v_model in [".", ""] or selected.is_project() or selected.is_folder(): + return + + # open the delete dialog with the current file + self.w_asset_dialog.reload(ee.Asset(selected)) + self.w_asset_dialog.value = True + + @switch("loading", "disabled", member="w_card") + def on_new(self, *args): + """Create a new folder cia the dialog.""" + # We need to be at least in a project to be able to create a new folder + selected = self.w_selected.v_model + if selected in [".", ""]: + return + + self.w_create_dialog.reload(ee.Asset(self.folder)) + self.w_create_dialog.value = True + + def activate_buttons(self, change: dict): + """Activate the appropriate buttons whenever the selected item changes.""" + # reset everything + self.w_new.disabled = True + self.w_view.disabled = True + self.w_move.disabled = True + self.w_delete.disabled = True + self.w_copy.disabled = True + self.w_copy.children[0].children = ["mdi-content-copy"] + + # We can activate the new button for projects + asset = ee.Asset(change["new"]) + if asset.is_absolute(): + self.w_new.disabled = False + + # we need to exit if the selected item is a project or a root + if change["new"] in [".", ""] or asset.is_project(): + return + + # reactivate delete move and copy for assets + if asset.exists(): + self.w_delete.disabled = False + self.w_move.disabled = False + self.w_copy.disabled = False + + # we can only view files + if not asset.is_folder(): + self.w_view.disabled = False + + +class DeleteAssetDialog(v.Dialog): + """A dialog to confirm the deletion of an asset.""" + + # -- Variables ------------------------------------------------------------- + + asset: ee.Asset + "The asset to delete" + + # -- Widgets --------------------------------------------------------------- + w_confirm: v.Btn + "The confirm button" + + w_cancel: v.Btn + "The cancel button" + + def __init__(self, asset: Optional[ee.Asset] = None): + """Initialize the class.""" + # start by defining all the widgets + self.w_confirm = v.Btn(children=[v.Icon(children="mdi-check"), "Confirm"], color="primary") + self.w_cancel = v.Btn(children=[v.Icon(children=["mdi-times"]), "Cancel"]) + w_title = v.CardTitle(children=["Delete the assets"]) + disclaimer = 'Clicking on "confirm" will definitively delete all the following asset. This action is definitive.' # fmt: skip + option = 'Click on "cancel" to abort the deletion.' + + self.ul = v.Html(tag="ul", children=[]) + w_content = v.CardText(children=[disclaimer, option, self.ul]) + + w_actions = v.CardActions(children=[v.Spacer(), self.w_cancel, self.w_confirm]) + + self.w_card = v.Card(children=[w_title, w_content, w_actions]) + + super().__init__(children=[self.w_card], max_width="50%", persistent=True) + + # js interaction with the btns + self.w_confirm.on_event("click", self.on_confirm) + self.w_cancel.on_event("click", self.on_cancel) + + def reload(self, asset: ee.Asset): + """Reload the dialog with a new asset.""" + # We should never arrive here with a non asset + # but to avoid catastrophic destruction we will empty the list first + if asset is None or str(asset) == ".": + self.ul.children = [] + + # save the asset as a member and read it + self.asset = asset + assets = asset.iterdir(recursive=True) if asset.is_folder() else [asset] + self.ul.children = [v.Html(tag="li", children=[str(a)]) for a in assets] + + @switch("loading", "disabled", member="w_card") + def on_confirm(self, *args): + """Confirm the deletion.""" + # delete the asset and close the dialog + if self.asset.is_folder(): + self.asset.rmdir(recursive=True, dry_run=False) + else: + self.asset.delete() + self.value = False + + @switch("loading", "disabled", member="w_card") + def on_cancel(self, *args): + """Exit without doing anything.""" + self.value = False + + +class MoveAssetDialog(v.Dialog): + """A dialog to confirm the move of an asset.""" + + # -- Variables ------------------------------------------------------------- + + asset: ee.Asset + "The asset to delete" + + # -- Widgets --------------------------------------------------------------- + w_asset: v.TextField + "The destination to move" + + w_confirm: v.Btn + "The confirm button" + + w_cancel: v.Btn + "The cancel button" + + def __init__(self, asset: Optional[ee.Asset] = None): + """Initialize the class.""" + # start by defining all the widgets + # fmt: off + self.w_asset = v.TextField(placeholder="Destination", v_model="", clearable=True, outlined=True, class_="ma-1") + self.w_confirm = v.Btn(children=[v.Icon(children="mdi-check"), "Confirm"], color="primary") + self.w_cancel = v.Btn(children=[v.Icon(children=["mdi-times"]), "Cancel"]) + w_title = v.CardTitle(children=["Delete the assets"]) + disclaimer = 'Clicking on "confirm" will move the following asset to the destination. This initial asset is not deleted.' + option = 'Click on "cancel" to abort the move.' + self.ul = v.Html(tag="ul", children=[]) + w_content = v.CardText(children=[self.w_asset, disclaimer, option, self.ul]) + w_actions = v.CardActions(children=[v.Spacer(), self.w_cancel, self.w_confirm]) + self.w_card = v.Card(children=[w_title, w_content, w_actions]) + # fmt: on + + super().__init__(children=[self.w_card], max_width="50%", persistent=True) + + # js interaction with the btns + self.w_confirm.on_event("click", self.on_confirm) + self.w_cancel.on_event("click", self.on_cancel) + + def reload(self, asset: ee.Asset): + """Reload the dialog with a new asset.""" + # We should never arrive here with a non asset + # but to avoid catastrophic destruction we will empty the list first + if asset is None or str(asset) == ".": + self.ul.children = [] + + # save the asset as a member and read it + self.asset = asset + assets = asset.iterdir(recursive=True) if asset.is_folder() else [asset] + self.ul.children = [v.Html(tag="li", children=[str(a)]) for a in assets] + + @switch("loading", "disabled", member="w_card") + def on_confirm(self, *args): + """Confirm the deletion.""" + # remove the warnings + self.w_asset.error_messages = [] + + # delete the asset and close the dialog + try: + self.asset.move(ee.Asset(self.w_asset.v_model)) + self.value = False + except Exception as e: + self.w_asset.error_messages = [str(e)] + + @switch("loading", "disabled", member="w_card") + def on_cancel(self, *args): + """Exit without doing anything.""" + self.value = False + + +class AssetDialog(v.Dialog): + """A dialog to view an asset.""" + + # -- Variables ------------------------------------------------------------- + + asset: ee.Asset + "The asset to delete" + + # -- Widgets --------------------------------------------------------------- + + w_exit: v.Btn + "The exit button" + + def __init__(self, asset: Optional[ee.Asset] = None): + """Initialize the class.""" + # start by defining all the widgets + # fmt: off + self.w_exit = v.Btn(children=[v.Icon(children="mdi-check"), "Exit"]) + self.w_title = v.CardTitle(children=["Delete the assets"]) + w_content = v.CardText(children=[""]) + w_actions = v.CardActions(children=[v.Spacer(), self.w_exit]) + self.w_card = v.Card(children=[self.w_title, w_content, w_actions]) + # fmt: on + + super().__init__(children=[self.w_card], max_width="50%", persistent=True) + + # js interaction with the btns + self.w_exit.on_event("click", self.on_exit) + + def reload(self, asset: ee.Asset): + """Reload the dialog with a new asset.""" + # We should never arrive here with a non asset + # but to avoid catastrophic destruction we will empty the list first + if asset is None or str(asset) == ".": + self.ul.children = [] + + # save the asset as a member and read it + self.asset = asset + self.w_title.children = [f"Viewing {asset.name}"] + + @switch("loading", "disabled", member="w_card") + def on_exit(self, *args): + """Exit without doing anything.""" + self.value = False + + +class CreateFolderDialog(v.Dialog): + """A dialog to create a new folder asset.""" + + # -- Variables ------------------------------------------------------------- + + folder: ee.Asset + "The current folder where to create the new folder." + + # -- Widgets --------------------------------------------------------------- + w_asset: v.TextField + "The destination to move" + + w_confirm: v.Btn + "The confirm button" + + w_cancel: v.Btn + "The cancel button" + + def __init__(self, asset: Optional[ee.Asset] = None): + """Initialize the class.""" + # start by defining all the widgets + # fmt: off + self.w_asset = v.TextField(placeholder="Folder name", v_model="", clearable=True, outlined=True, class_="ma-1") + self.w_confirm = v.Btn(children=[v.Icon(children="mdi-check"), "Confirm"], color="primary") + self.w_cancel = v.Btn(children=[v.Icon(children=["mdi-times"]), "Cancel"]) + w_title = v.CardTitle(children=["Create a new folder"]) + w_content = v.CardText(children=[self.w_asset]) + w_actions = v.CardActions(children=[v.Spacer(), self.w_cancel, self.w_confirm]) + self.w_card = v.Card(children=[w_title, w_content, w_actions]) + # fmt: on + + super().__init__(children=[self.w_card], max_width="50%", persistent=True) + + # js interaction with the btns + self.w_confirm.on_event("click", self.on_confirm) + self.w_cancel.on_event("click", self.on_cancel) + + def reload(self, folder: ee.Asset): + """Reload the dialog with a new asset.""" + # check the new destination is at least a project + if not ee.Asset(folder).is_absolute(): + return + + self.folder = folder + self.w_asset.prefix = f"{folder}/" + self.w_asset.v_model = "" + + @switch("loading", "disabled", member="w_card") + def on_confirm(self, *args): + """Confirm the deletion.""" + # remove the warnings + self.w_asset.error_messages = [] + + # crezte the folder and close the dialog + try: + (self.folder / self.w_asset.v_model).mkdir(exist_ok=True, parents=True) + self.value = False + except Exception as e: + self.w_asset.error_messages = [str(e)] + + @switch("loading", "disabled", member="w_card") + def on_cancel(self, *args): + """Exit without doing anything.""" + self.value = False diff --git a/ipygee/js/jupyter_clip.js b/ipygee/js/jupyter_clip.js new file mode 100644 index 0000000..02605b6 --- /dev/null +++ b/ipygee/js/jupyter_clip.js @@ -0,0 +1,7 @@ +var tempInput = document.createElement("input"); +tempInput.value = _txt; +document.body.appendChild(tempInput); +tempInput.focus(); +tempInput.select(); +document.execCommand("copy"); +document.body.removeChild(tempInput);