diff --git a/dtool_lookup_gui/models/datasets.py b/dtool_lookup_gui/models/datasets.py index 27469b4f..80e2cf5e 100644 --- a/dtool_lookup_gui/models/datasets.py +++ b/dtool_lookup_gui/models/datasets.py @@ -268,25 +268,22 @@ def __init__(self, uri=None, dataset_info=None): raise ValueError('Please provide either `uri` or `dateset_info`.') @classmethod - async def search(cls, keyword): + async def search(cls, keyword, page_number, page_size, pagination={}): async with ConfigurationBasedLookupClient() as lookup: - datasets = await lookup.search(keyword) - + datasets = await lookup.search(keyword, page_number=page_number, page_size=page_size, pagination=pagination) return [await cls.from_lookup(lookup_dict) for lookup_dict in datasets] @classmethod - async def query(cls, query_text): + async def query(cls, query_text, *args, **kwargs): async with ConfigurationBasedLookupClient() as lookup: - datasets = await lookup.by_query(query_text) - + datasets = await lookup.by_query(query_text, *args, **kwargs) return [await cls.from_lookup(lookup_dict) for lookup_dict in datasets] @classmethod - async def query_all(cls): - """Query all datasets from lookup server.""" + async def query_all(cls, page_number=1, page_size=8, pagination={}): + """Query all datasets from the lookup server.""" async with ConfigurationBasedLookupClient() as lookup: - datasets = await lookup.all() - + datasets = await lookup.all(page_number=page_number, page_size=page_size, pagination=pagination) return [await cls.from_lookup(lookup_dict) for lookup_dict in datasets] def __str__(self): diff --git a/dtool_lookup_gui/views/main_window.py b/dtool_lookup_gui/views/main_window.py index 5b27c8d7..52c2a660 100644 --- a/dtool_lookup_gui/views/main_window.py +++ b/dtool_lookup_gui/views/main_window.py @@ -38,6 +38,7 @@ import dtool_lookup_api.core.config from dtool_lookup_api.core.LookupClient import ConfigurationBasedLookupClient + # As of dtool-lookup-api 0.5.0, the following line still is a necessity to # disable prompting for credentials on the command line. This behavior # will change in future versions. @@ -101,7 +102,7 @@ class MainWindow(Gtk.ApplicationWindow): search_entry = Gtk.Template.Child() - #copy_dataset_spinner = Gtk.Template.Child() + # copy_dataset_spinner = Gtk.Template.Child() base_uri_list_box = Gtk.Template.Child() dataset_list_box = Gtk.Template.Child() @@ -155,6 +156,20 @@ class MainWindow(Gtk.ApplicationWindow): error_bar = Gtk.Template.Child() error_label = Gtk.Template.Child() + + first_page_button = Gtk.Template.Child() + prev_page_button = Gtk.Template.Child() + curr_page_button = Gtk.Template.Child() + next_page_advancer_button = Gtk.Template.Child() + page_advancer_button = Gtk.Template.Child() + nextoption_page_button = Gtk.Template.Child() + last_page_button = Gtk.Template.Child() + + main_statusbar = Gtk.Template.Child() + + + + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -241,6 +256,10 @@ def __init__(self, *args, **kwargs): _logger.debug(f"Constructed main window for app '{self.application.get_application_id()}'") + self.pagination = {} + + + # utility methods def refresh(self): """Refresh view.""" @@ -291,18 +310,29 @@ async def _refresh_base_uri_list_box(self): def _update_search_summary(self, datasets): row = self.base_uri_list_box.search_results_row total_size = sum([0 if dataset.size_int is None else dataset.size_int for dataset in datasets]) - row.info_label.set_text(f'{len(datasets)} datasets, {sizeof_fmt(total_size).strip()}') + total_value = self.pagination['total'] + row.info_label.set_text(f'{total_value} datasets, {sizeof_fmt(total_size).strip()}') - async def _fetch_search_results(self, keyword, on_show=None): + def _update_main_statusbar(self): + total_number = self.pagination['total'] + current_page = self.pagination['page'] + self.main_statusbar.push(0, f"Total Number of Dataset: {total_number}, Current page: {current_page}") + + async def _fetch_search_results(self, keyword, on_show=None, page_number=1, page_size=8): row = self.base_uri_list_box.search_results_row row.start_spinner() + self.pagination = {} # Add pagination dictionary + + + try: - # datasets = await DatasetModel.search(keyword) if keyword: if is_valid_query(keyword): _logger.debug("Valid query specified.") - datasets = await DatasetModel.query(keyword) + datasets = await DatasetModel.query(keyword, page_number=page_number, page_size=page_size, + pagination=self.pagination) # Pass pagination dictionary and + # page_number, page_size else: _logger.debug("Specified search text is not a valid query, just perform free text search.") # NOTE: server side allows a dict with the key-value pairs @@ -312,10 +342,13 @@ async def _fetch_search_results(self, keyword, on_show=None): # constructs on the server side. With the special treatment # of the 'uuid' keyword above, should we introduce similar # options for the other available keywords? - datasets = await DatasetModel.search(keyword) + datasets = await DatasetModel.search(keyword, page_number=page_number, page_size=page_size, + pagination=self.pagination) # Pass pagination dictionary and + # page_number, page_size else: _logger.debug("No keyword specified, list all datasets.") - datasets = await DatasetModel.query_all() + datasets = await DatasetModel.query_all(page_number=page_number, page_size=page_size,pagination=self.pagination) # Pass pagination dictionary and + if len(datasets) > self._max_nb_datasets: _logger.warning( @@ -324,6 +357,7 @@ async def _fetch_search_results(self, keyword, on_show=None): datasets = datasets[:self._max_nb_datasets] # Limit number of datasets that are shown row.search_results = datasets # Cache datasets self._update_search_summary(datasets) + self._update_main_statusbar() if self.base_uri_list_box.get_selected_row() == row: # Only update if the row is still selected self.dataset_list_box.fill(datasets, on_show=on_show) @@ -335,6 +369,8 @@ async def _fetch_search_results(self, keyword, on_show=None): self.main_stack.set_visible_child(self.main_paned) row.stop_spinner() + + def _search_by_uuid(self, uuid): search_text = dump_single_line_query_text({"uuid": uuid}) self._search_by_search_text(search_text) @@ -489,7 +525,7 @@ def do_get_item(self, action, value): dataset = self.dataset_list_box.get_selected_row().dataset items = self._get_selected_items() - if len(items) !=1: + if len(items) != 1: raise ValueError("Can only get one item at a time.") item_name, item_uuid = items[0] @@ -504,6 +540,8 @@ async def _get_item(dataset, item_uuid): asyncio.create_task(_get_item(dataset, item_uuid)) + + def do_refresh_view(self, action, value): """Refresh view by reloading base uri list, """ self.refresh() @@ -517,6 +555,7 @@ def on_settings_clicked(self, widget): def on_logging_clicked(self, widget): self.log_window.show() + @Gtk.Template.Callback() def on_about_clicked(self, widget): self.about_dialog.show() @@ -687,6 +726,7 @@ def on_add_items_clicked(self, widget): pass dialog.destroy() + @Gtk.Template.Callback() def on_manifest_row_activated(self, tree_view, path, column): """Handler for "row-activated" signal. @@ -696,7 +736,7 @@ def on_manifest_row_activated(self, tree_view, path, column): is pressed. (https://www.gnu.org/software/guile-gnome/docs/gtk/html/GtkTreeView.html)""" items = self._get_selected_items() - if len(items) !=1: + if len(items) != 1: raise ValueError("Can only get one item at a time.") item_name, item_uuid = items[0] self._show_get_item_dialog(item_name, item_uuid) @@ -750,6 +790,77 @@ def on_error_bar_response(self, widget, response_id): if response_id == Gtk.ResponseType.CLOSE: self.error_bar.set_revealed(False) + @Gtk.Template.Callback() + def on_first_page_button_clicked(self, widget): + page_number = 1 + self.pagination['page'] = page_number + asyncio.create_task( + self._fetch_search_results(keyword=None, on_show=None, page_number=page_number)) + self.curr_page_button.set_label(str(page_number)) + self.page_advancer_button.set_label(str(page_number - 1)) + self.next_page_advancer_button.set_label(str(page_number + 1)) + + + @Gtk.Template.Callback() + def on_nextoption_page_button_clicked(self, widget): + if self.pagination['page'] < self.pagination['last_page']: + page_number = self.pagination['page'] + 1 + self.curr_page_button.set_label(str(page_number)) + self.page_advancer_button.set_label(str(page_number - 1)) + self.next_page_advancer_button.set_label(str(page_number + 1)) + asyncio.create_task( + self._fetch_search_results(keyword=None, on_show=None, page_number=page_number)) + + @Gtk.Template.Callback() + def on_last_page_button_clicked(self, widget): + page_number = self.pagination['last_page'] + self.curr_page_button.set_label(str(page_number)) + self.page_advancer_button.set_label(str(page_number - 1)) + self.next_page_advancer_button.set_label(str(page_number + 1)) + asyncio.create_task( + self._fetch_search_results(keyword=None, on_show=None, page_number=page_number)) + + @Gtk.Template.Callback() + def on_prev_page_button_clicked(self, widget): + if self.pagination['page'] > 1: + self.pagination['page'] -= 1 + page_number = self.pagination['page'] + self.curr_page_button.set_label(str(page_number)) # Update curr_page_button label + self.page_advancer_button.set_label(str(page_number - 1)) + self.next_page_advancer_button.set_label(str(page_number + 1)) + asyncio.create_task( + self._fetch_search_results(keyword=None, on_show=None, page_number=page_number)) + + @Gtk.Template.Callback() + def curr_page_button_clicked(self, widget): + page_number = self.pagination['page'] + asyncio.create_task( + self._fetch_search_results(keyword=None, on_show=None, page_number=page_number)) + self.curr_page_button.set_label(str(page_number)) + self.page_advancer_button.set_label(str(page_number - 1)) + self.next_page_advancer_button.set_label(str(page_number + 1)) + + @Gtk.Template.Callback() + def page_advancer_button_clicked(self, widget): + page_number = self.pagination['page'] + if page_number > 1: + page_number -= 1 + asyncio.create_task( + self._fetch_search_results(keyword=None, on_show=None, page_number=page_number)) + self.curr_page_button.set_label(str(page_number)) + self.page_advancer_button.set_label(str(page_number - 1)) + self.next_page_advancer_button.set_label(str(page_number + 1)) + + @Gtk.Template.Callback() + def next_page_advancer_button_clicked(self, widget): + if self.pagination['page'] < self.pagination['last_page']: + page_number = self.pagination['page'] + 1 + asyncio.create_task( + self._fetch_search_results(keyword=None, on_show=None, page_number=page_number)) + self.curr_page_button.set_label(str(page_number)) + self.page_advancer_button.set_label(str(page_number - 1)) + self.next_page_advancer_button.set_label(str(page_number + 1)) + # @Gtk.Template.Callback(), not in .ui def on_copy_clicked(self, widget): async def _copy(): @@ -802,7 +913,6 @@ def _show_get_item_dialog(self, item_name, item_uuid): dest_file = os.path.join(default_dir, item_name) self.activate_action('get-item', GLib.Variant.new_string(dest_file)) - # TODO: move to the model def _add_item(self, fpath): handle = os.path.basename(fpath) @@ -895,4 +1005,4 @@ async def _compute_dependencies(self, dataset): self.dependency_stack.set_visible_child(self.dependency_view) def show_error(self, exception): - _logger.error(traceback.format_exc()) \ No newline at end of file + _logger.error(traceback.format_exc()) diff --git a/dtool_lookup_gui/views/main_window.ui b/dtool_lookup_gui/views/main_window.ui index 8452b661..47aa6c07 100644 --- a/dtool_lookup_gui/views/main_window.ui +++ b/dtool_lookup_gui/views/main_window.ui @@ -233,12 +233,127 @@ True False - natural - + True False - + vertical + + + True + False + + + + True + True + 0 + + + + + + True + False + True + + + First + True + True + True + + + + 0 + 0 + + + + + Prev + True + True + True + + + + 1 + 0 + + + + + Last + True + True + True + + + + 6 + 0 + + + + + Next + True + True + True + + + + 5 + 0 + + + + + 2 + True + True + True + + + + 4 + 0 + + + + + 1 + True + True + True + + + + 3 + 0 + + + + + 0 + True + True + True + + + + 2 + 0 + + + + + False + True + 1 + + @@ -980,16 +1095,17 @@ or search for a dataset True False - + True False - 10 - 10 10 10 6 6 2 + page0 diff --git a/requirements.txt b/requirements.txt index 2877a3c8..cb1e84bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,36 +1,36 @@ # -# This file is autogenerated by pip-compile with python 3.8 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: # # pip-compile requirements.in # -aiohttp==3.8.1 +aiohttp==3.8.4 # via # -r requirements.in # dtool-lookup-api -aiosignal==1.2.0 +aiosignal==1.3.1 # via aiohttp -asgiref==3.5.0 +asgiref==3.7.2 # via dtool-lookup-api async-timeout==4.0.2 # via aiohttp -attrs==21.4.0 +attrs==23.1.0 # via aiohttp -boto3==1.21.22 +boto3==1.28.2 # via dtool-s3 -botocore==1.24.22 +botocore==1.31.2 # via # boto3 # s3transfer -certifi==2021.10.8 +certifi==2023.5.7 # via requests -cffi==1.15.0 +cffi==1.15.1 # via cryptography -charset-normalizer==2.0.12 +charset-normalizer==3.2.0 # via # aiohttp # requests -click==8.0.4 +click==8.1.4 # via # click-plugins # dtool-cli @@ -39,7 +39,7 @@ click==8.0.4 # dtool-s3 click-plugins==1.1.1 # via dtool-cli -cryptography==36.0.2 +cryptography==41.0.2 # via jwt dtool-cli==0.7.1 # via @@ -52,7 +52,7 @@ dtool-http==0.5.1 # via dtool-create dtool-info==0.16.2 # via -r requirements.in -dtool-lookup-api==0.5.0 +dtool-lookup-api==0.6.0 # via -r requirements.in dtool-s3==0.14.1 # via -r requirements.in @@ -71,19 +71,19 @@ dtoolcore==3.18.2 # dtool-s3 # dtool-smb # dtool-symlink -frozenlist==1.3.0 +frozenlist==1.3.3 # via # aiohttp # aiosignal -gbulb==0.6.3 +gbulb==0.6.4 # via -r requirements.in -idna==3.3 +idna==3.4 # via # requests # yarl -jinja2==3.0.3 +jinja2==3.1.2 # via dtool-info -jmespath==1.0.0 +jmespath==1.0.1 # via # boto3 # botocore @@ -93,29 +93,27 @@ markupsafe==2.0.1 # via # -r requirements.in # jinja2 -multidict==6.0.2 +multidict==6.0.4 # via # aiohttp # yarl -numpy==1.22.3 +numpy==1.24.4 # via scipy -packaging==21.3 +packaging==23.1 # via dtool-s3 -pyasn1==0.4.8 +pyasn1==0.5.0 # via pysmb -pycairo==1.21.0 +pycairo==1.24.0 # via pygobject pycparser==2.21 # via cffi -pygments==2.11.2 +pygments==2.15.1 # via dtool-info -pygobject==3.42.0 +pygobject==3.44.1 # via # -r requirements.in # gbulb -pyparsing==3.0.7 - # via packaging -pysmb==1.2.7 +pysmb==1.2.9.1 # via dtool-smb python-dateutil==2.8.2 # via botocore @@ -123,23 +121,27 @@ pyyaml==6.0 # via # -r requirements.in # dtool-lookup-api -requests==2.27.1 +requests==2.31.0 # via dtool-http -ruamel-yaml==0.17.21 +ruamel-yaml==0.17.32 # via # -r requirements.in # dtool-create -ruamel-yaml-clib==0.2.6 +ruamel-yaml-clib==0.2.7 # via ruamel-yaml -s3transfer==0.5.2 +s3transfer==0.6.1 # via boto3 -scipy==1.8.0 +scipy==1.10.1 # via -r requirements.in six==1.16.0 # via python-dateutil -urllib3==1.26.9 +tqdm==4.65.0 + # via pysmb +typing-extensions==4.7.1 + # via asgiref +urllib3==1.26.16 # via # botocore # requests -yarl==1.7.2 +yarl==1.9.2 # via aiohttp