From 2579081df2a568192887d776a6965af323b7c4ee Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Thu, 18 Aug 2022 13:40:16 +0200 Subject: make it possible to list all repositories in the web UI --- src/hydrilla/proxy/state.py | 31 ++++- src/hydrilla/proxy/state_impl/concrete_state.py | 22 +-- src/hydrilla/proxy/state_impl/mappings.py | 2 +- src/hydrilla/proxy/state_impl/repos.py | 151 +++++++++++++++++++++ src/hydrilla/proxy/tables.sql | 31 ++--- src/hydrilla/proxy/web_ui/packages.py | 5 +- src/hydrilla/proxy/web_ui/repos.py | 33 ++++- .../proxy/web_ui/templates/base.html.jinja | 34 +++-- .../include/checkbox_tricks_style.css.jinja | 36 +++++ .../templates/include/item_list_style.css.jinja | 39 ++++++ .../proxy/web_ui/templates/packages.html.jinja | 71 ++++------ .../templates/packages__show_single.html.jinja | 11 +- .../proxy/web_ui/templates/repos.html.jinja | 43 +++++- .../web_ui/templates/repos__show_single.html.jinja | 97 +++++++++++++ 14 files changed, 497 insertions(+), 109 deletions(-) create mode 100644 src/hydrilla/proxy/state_impl/repos.py create mode 100644 src/hydrilla/proxy/web_ui/templates/include/checkbox_tricks_style.css.jinja create mode 100644 src/hydrilla/proxy/web_ui/templates/include/item_list_style.css.jinja create mode 100644 src/hydrilla/proxy/web_ui/templates/repos__show_single.html.jinja (limited to 'src/hydrilla/proxy') diff --git a/src/hydrilla/proxy/state.py b/src/hydrilla/proxy/state.py index 14d38b6..c3712f2 100644 --- a/src/hydrilla/proxy/state.py +++ b/src/hydrilla/proxy/state.py @@ -39,6 +39,7 @@ import typing as t from pathlib import Path from abc import ABC, abstractmethod from enum import Enum +from datetime import datetime from immutables import Map @@ -68,6 +69,9 @@ class Ref: """....""" id: str + def __post_init__(self): + assert isinstance(self.id, str) + RefType = t.TypeVar('RefType', bound=Ref) @@ -83,14 +87,13 @@ class Store(ABC, t.Generic[RefType]): class RepoRef(Ref): """....""" @abstractmethod - def remove(self, state: 'HaketiloState') -> None: + def remove(self) -> None: """....""" ... @abstractmethod def update( self, - state: 'HaketiloState', *, name: t.Optional[str] = None, url: t.Optional[str] = None @@ -99,10 +102,30 @@ class RepoRef(Ref): ... @abstractmethod - def refresh(self, state: 'HaketiloState') -> 'RepoIterationRef': + def refresh(self) -> 'RepoIterationRef': """....""" ... + @abstractmethod + def get_display_info(self) -> 'RepoDisplayInfo': + ... + +@dc.dataclass(frozen=True) +class RepoDisplayInfo: + ref: RepoRef + name: str + url: t.Optional[str] + deleted: t.Optional[bool] + last_refreshed: t.Optional[datetime] + resource_count: int + mapping_count: int + +class RepoStore(Store[RepoRef]): + @abstractmethod + def get_display_infos(self, include_deleted: bool = False) -> \ + t.Iterable[RepoDisplayInfo]: + ... + @dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] class RepoIterationRef(Ref): @@ -253,7 +276,7 @@ class HaketiloState(ABC): ... @abstractmethod - def get_repo(self, repo_id: str) -> RepoRef: + def repo_store(self) -> RepoStore: """....""" ... diff --git a/src/hydrilla/proxy/state_impl/concrete_state.py b/src/hydrilla/proxy/state_impl/concrete_state.py index b2b1033..46e7827 100644 --- a/src/hydrilla/proxy/state_impl/concrete_state.py +++ b/src/hydrilla/proxy/state_impl/concrete_state.py @@ -52,28 +52,12 @@ from .. import state as st from .. import policies from . import base from . import mappings +from . import repos from .load_packages import load_packages here = Path(__file__).resolve().parent -@dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] -class ConcreteRepoRef(st.RepoRef): - def remove(self, state: st.HaketiloState) -> None: - raise NotImplementedError() - - def update( - self, - state: st.HaketiloState, - *, - name: t.Optional[str] = None, - url: t.Optional[str] = None - ) -> ConcreteRepoRef: - raise NotImplementedError() - - def refresh(self, state: st.HaketiloState) -> ConcreteRepoIterationRef: - raise NotImplementedError() - @dc.dataclass(frozen=True, unsafe_hash=True) class ConcreteRepoIterationRef(st.RepoIterationRef): @@ -440,8 +424,8 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): self.policy_tree = new_policy_tree self.payloads_data = new_payloads_data - def get_repo(self, repo_id: str) -> st.RepoRef: - return ConcreteRepoRef(repo_id) + def repo_store(self) -> st.RepoStore: + return repos.ConcreteRepoStore(self) def get_repo_iteration(self, repo_iteration_id: str) -> st.RepoIterationRef: return ConcreteRepoIterationRef(repo_iteration_id) diff --git a/src/hydrilla/proxy/state_impl/mappings.py b/src/hydrilla/proxy/state_impl/mappings.py index 5e31814..3668784 100644 --- a/src/hydrilla/proxy/state_impl/mappings.py +++ b/src/hydrilla/proxy/state_impl/mappings.py @@ -130,4 +130,4 @@ class ConcreteMappingVersionStore(st.MappingVersionStore): info = st.MappingDisplayInfo(ref, item_info, status, is_orphan) result.append(info) - return result + return sorted(result, key=(lambda di: di.info)) diff --git a/src/hydrilla/proxy/state_impl/repos.py b/src/hydrilla/proxy/state_impl/repos.py new file mode 100644 index 0000000..be11a88 --- /dev/null +++ b/src/hydrilla/proxy/state_impl/repos.py @@ -0,0 +1,151 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +# Haketilo proxy data and configuration (RepoRef and RepoStore subtypes). +# +# This file is part of Hydrilla&Haketilo. +# +# Copyright (C) 2022 Wojtek Kosior +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# +# I, Wojtek Kosior, thereby promise not to sue for violation of this +# file's license. Although I request that you do not make use this code +# in a proprietary program, I am not going to enforce this in court. + +""" +This module provides an interface to interact with repositories configured +inside Haketilo. +""" + +# Enable using with Python 3.7. +from __future__ import annotations + +import typing as t +import dataclasses as dc + +from datetime import datetime + +from .. import state as st +from . import base + +def make_repo_display_info( + ref: st.RepoRef, + name: str, + url: t.Optional[str], + deleted: t.Optional[bool], + last_refreshed: t.Optional[int], + resource_count: int, + mapping_count: int +) -> st.RepoDisplayInfo: + last_refreshed_converted: t.Optional[datetime] = None + if last_refreshed is not None: + last_refreshed_converted = datetime.fromtimestamp(last_refreshed) + + return st.RepoDisplayInfo( + ref = ref, + name = name, + url = url, + deleted = deleted, + last_refreshed = last_refreshed_converted, + resource_count = resource_count, + mapping_count = mapping_count + ) + + +@dc.dataclass(frozen=True, unsafe_hash=True) +class ConcreteRepoRef(st.RepoRef): + """....""" + state: base.HaketiloStateWithFields = dc.field(hash=False, compare=False) + + def remove(self) -> None: + raise NotImplementedError() + + def update( + self, + *, + name: t.Optional[str] = None, + url: t.Optional[str] = None + ) -> st.RepoRef: + raise NotImplementedError() + + def refresh(self) -> st.RepoIterationRef: + raise NotImplementedError() + + def get_display_info(self) -> st.RepoDisplayInfo: + with self.state.cursor() as cursor: + cursor.execute( + ''' + SELECT + name, url, deleted, last_refreshed, + resource_count, mapping_count + FROM + repo_display_infos + WHERE + repo_id = ?; + ''', + (self.id,) + ) + + rows = cursor.fetchall() + + if rows == []: + raise st.MissingItemError() + + row, = rows + + return make_repo_display_info(self, *row) + + +@dc.dataclass(frozen=True) +class ConcreteRepoStore(st.RepoStore): + state: base.HaketiloStateWithFields + + def get(self, id: str) -> st.RepoRef: + return ConcreteRepoRef(id, self.state) + + def get_display_infos(self, include_deleted: bool = False) \ + -> t.Iterable[st.RepoDisplayInfo]: + with self.state.cursor() as cursor: + condition: str = 'TRUE' + if include_deleted: + condition = 'COALESCE(deleted = FALSE, TRUE)' + + cursor.execute( + f''' + SELECT + repo_id, name, url, deleted, last_refreshed, + resource_count, mapping_count + FROM + repo_display_infos + WHERE + {condition} + ORDER BY + repo_id != 1, name; + ''' + ) + + all_rows = cursor.fetchall() + + assert len(all_rows) > 0 and all_rows[0][0] == 1 + + result = [] + for row in all_rows: + repo_id, *rest = row + + ref = ConcreteRepoRef(str(repo_id), self.state) + + result.append(make_repo_display_info(ref, *rest)) + + return result diff --git a/src/hydrilla/proxy/tables.sql b/src/hydrilla/proxy/tables.sql index 0417613..a915f74 100644 --- a/src/hydrilla/proxy/tables.sql +++ b/src/hydrilla/proxy/tables.sql @@ -155,6 +155,20 @@ CREATE TABLE item_versions( REFERENCES repo_iterations (repo_iteration_id) ); +CREATE VIEW repo_display_infos +AS +SELECT + r.repo_id, r.name, r.url, r.deleted, r.last_refreshed, + COALESCE(SUM(i.type = 'R'), 0) AS resource_count, + COALESCE(SUM(i.type = 'M'), 0) AS mapping_count +FROM + repos AS r + LEFT JOIN repo_iterations AS ir USING (repo_id) + LEFT JOIN item_versions AS iv USING (repo_iteration_id) + LEFT JOIN items AS i USING (item_id) +GROUP BY + r.repo_id, r.name, r.url, r.deleted, r.last_refreshed; + CREATE TABLE payloads( payload_id INTEGER PRIMARY KEY, @@ -225,23 +239,6 @@ CREATE TABLE resolved_depended_resources( ON DELETE CASCADE ) WITHOUT ROWID; --- CREATE TABLE resolved_required_mappings( --- requiring_item_id INTEGER, --- required_mapping_item_id INTEGER, - --- PRIMARY KEY (requiring_item_id, required_mapping_item_id), - --- CHECK (requiring_item_id != required_mapping_item_id), - --- FOREIGN KEY (requiring_item_id) --- REFERENCES items (item_id), --- -- Note: the referenced mapping shall have installed=TRUE. --- FOREIGN KEY (required_mapping_item_id) --- REFERENCES mappings (item_id), --- FOREIGN KEY (required_mapping_item_id) --- REFERENCES items (item_id) --- ); - CREATE TABLE files( file_id INTEGER PRIMARY KEY, diff --git a/src/hydrilla/proxy/web_ui/packages.py b/src/hydrilla/proxy/web_ui/packages.py index a618ca0..f38ea13 100644 --- a/src/hydrilla/proxy/web_ui/packages.py +++ b/src/hydrilla/proxy/web_ui/packages.py @@ -103,12 +103,9 @@ def load_from_disk_post() -> werkzeug.Response: def packages() -> flask.Response: state = t.cast(_app.WebUIApp, flask.current_app)._haketilo_state - display_infos = state.mapping_version_store().get_display_infos() - sorted_infos = sorted(display_infos, key=(lambda di: di.info)) - html = flask.render_template( 'packages.html.jinja', - display_infos = sorted_infos + display_infos = state.mapping_version_store().get_display_infos() ) return flask.make_response(html, 200) diff --git a/src/hydrilla/proxy/web_ui/repos.py b/src/hydrilla/proxy/web_ui/repos.py index 0f55b2a..d4e81c0 100644 --- a/src/hydrilla/proxy/web_ui/repos.py +++ b/src/hydrilla/proxy/web_ui/repos.py @@ -35,10 +35,41 @@ import typing as t import flask +from .. import state as st +from . import _app + bp = flask.Blueprint('repos', __package__) @bp.route('/repos') def repos() -> flask.Response: - html = flask.render_template('repos.html.jinja') + state = t.cast(_app.WebUIApp, flask.current_app)._haketilo_state + + local_semirepo_info, *repo_infos = state.repo_store().get_display_infos() + + html = flask.render_template( + 'repos.html.jinja', + local_semirepo_info = local_semirepo_info, + display_infos = repo_infos + ) return flask.make_response(html, 200) + +@bp.route('/repos/view/', methods=['GET']) +def show_repo(repo_id: str) -> flask.Response: + state = t.cast(_app.WebUIApp, flask.current_app)._haketilo_state + + try: + store = state.repo_store() + display_info = store.get(repo_id).get_display_info() + + html = flask.render_template( + 'repos__show_single.html.jinja', + display_info = display_info + ) + return flask.make_response(html, 200) + except st.MissingItemError: + flask.abort(404) + +@bp.route('/repos/view/', methods=['POST']) +def update_repo(repo_id: str) -> flask.Response: + raise NotImplementedError() diff --git a/src/hydrilla/proxy/web_ui/templates/base.html.jinja b/src/hydrilla/proxy/web_ui/templates/base.html.jinja index 4a9adf8..bca5948 100644 --- a/src/hydrilla/proxy/web_ui/templates/base.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/base.html.jinja @@ -25,16 +25,30 @@ in a proprietary work, I am not going to enforce this in court. {% block head %} {% block title required %}{% endblock %} - Haketilo proxy {% endblock %} diff --git a/src/hydrilla/proxy/web_ui/templates/include/checkbox_tricks_style.css.jinja b/src/hydrilla/proxy/web_ui/templates/include/checkbox_tricks_style.css.jinja new file mode 100644 index 0000000..b4c8edc --- /dev/null +++ b/src/hydrilla/proxy/web_ui/templates/include/checkbox_tricks_style.css.jinja @@ -0,0 +1,36 @@ +{# +Spdx-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 + + +Proxy web UI reusable stylesheet for checkbox-based dynamically displayed +elements. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior + +Dual licensed under +* GNU General Public License v3.0 or later and +* Creative Commons Attribution Share Alike 4.0 International. + +You can choose to use either of these licenses or both. + + +I, Wojtek Kosior, thereby promise not to sue for violation of this +file's licenses. Although I request that you do not make use this code +in a proprietary work, I am not going to enforce this in court. +#} + +input.chbx-tricks-show-hide, input.chbx-tricks-hide-show { + display: none !important; +} + +input.chbx-tricks-show-hide:checked+*+*, +input.chbx-tricks-hide-show:not(:checked)+*+* { + display: none !important; +} + +input.chbx-tricks-hide-show:checked+*+*, +input.chbx-tricks-show-hide:not(:checked)+*+* { + display: none !important; +} diff --git a/src/hydrilla/proxy/web_ui/templates/include/item_list_style.css.jinja b/src/hydrilla/proxy/web_ui/templates/include/item_list_style.css.jinja new file mode 100644 index 0000000..332d5f9 --- /dev/null +++ b/src/hydrilla/proxy/web_ui/templates/include/item_list_style.css.jinja @@ -0,0 +1,39 @@ +{# +Spdx-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 + + +Proxy web UI reusable stylesheet for lists. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior + +Dual licensed under +* GNU General Public License v3.0 or later and +* Creative Commons Attribution Share Alike 4.0 International. + +You can choose to use either of these licenses or both. + + +I, Wojtek Kosior, thereby promise not to sue for violation of this +file's licenses. Although I request that you do not make use this code +in a proprietary work, I am not going to enforce this in court. +#} +ul#item_list { + padding: 0; +} + +ul#item_list > li { + list-style-type: none; + max-width: 100%; + overflow-x: scroll; + white-space: nowrap; + margin: 5px; +} + +ul#item_list > li > :only-child { + display: block; + padding: 5px; + border: 2px solid #999; + border-radius: 5px; +} diff --git a/src/hydrilla/proxy/web_ui/templates/packages.html.jinja b/src/hydrilla/proxy/web_ui/templates/packages.html.jinja index d0ba5cb..48ef80b 100644 --- a/src/hydrilla/proxy/web_ui/templates/packages.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/packages.html.jinja @@ -20,59 +20,34 @@ file's licenses. Although I request that you do not make use this code in a proprietary work, I am not going to enforce this in court. #} {% extends "base.html.jinja" %} -{% block title %}Available packages{% endblock %} +{% block title %} {{ _('web_ui.packages.title') }} {% endblock %} {% block style %} -{{ super() }} - -ul#packages_list { - padding: 0; -} - -ul#packages_list > li { - list-style-type: none; - max-width: 100%; - overflow-x: scroll; - white-space: nowrap; - padding: 5px; - margin: 5px; - border: 2px solid #999; - border-radius: 5px; -} - -ul#packages_list > li > a { - display: block; - text-decoration: inherit; - color: inherit; -} - -.package-identifier { - font-size: 80%; - color: #555; -} + {{ super() }} + {% include 'include/item_list_style.css.jinja' %} {% endblock %} {% block main %} -

{{ _('web_ui.h3.packages') }}

-
    +

    {{ _('web_ui.packages.heading') }}

    + {% endblock %} diff --git a/src/hydrilla/proxy/web_ui/templates/packages__show_single.html.jinja b/src/hydrilla/proxy/web_ui/templates/packages__show_single.html.jinja index 5e20dd7..eb526c4 100644 --- a/src/hydrilla/proxy/web_ui/templates/packages__show_single.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/packages__show_single.html.jinja @@ -20,12 +20,17 @@ file's licenses. Although I request that you do not make use this code in a proprietary work, I am not going to enforce this in court. #} {% extends "base.html.jinja" %} -{% block title %} Package details {% endblock %} +{% block title %} {{ _('web_ui.packages.single.title') }} {% endblock %} {% block style %} -{{ super() }} + {{ super() }} {% endblock %} {% block main %} -

    {{ _('web_ui.h3.package_{}').format(display_info.info.long_name) }}

    +

    + {{ + _('web_ui.packages.single.heading.name_{}') + .format(display_info.info.long_name) + }} +

    {{ display_info.info.versioned_identifier }}
    diff --git a/src/hydrilla/proxy/web_ui/templates/repos.html.jinja b/src/hydrilla/proxy/web_ui/templates/repos.html.jinja index 59eb1bd..94b7e2b 100644 --- a/src/hydrilla/proxy/web_ui/templates/repos.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/repos.html.jinja @@ -20,7 +20,46 @@ file's licenses. Although I request that you do not make use this code in a proprietary work, I am not going to enforce this in court. #} {% extends "base.html.jinja" %} -{% block title %}Repositories{% endblock %} +{% block title %}{{ _('web_ui.repos.title') }}{% endblock %} +{% block style %} + {{ super() }} + + {% include 'include/item_list_style.css.jinja' %} +{% endblock %} {% block main %} - Not implemented yet :( +

    {{ _('web_ui.repos.heading') }}

    + {% endblock %} diff --git a/src/hydrilla/proxy/web_ui/templates/repos__show_single.html.jinja b/src/hydrilla/proxy/web_ui/templates/repos__show_single.html.jinja new file mode 100644 index 0000000..9abb00d --- /dev/null +++ b/src/hydrilla/proxy/web_ui/templates/repos__show_single.html.jinja @@ -0,0 +1,97 @@ +{# +Spdx-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 + + +Proxy web UI repository settings page. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior + +Dual licensed under +* GNU General Public License v3.0 or later and +* Creative Commons Attribution Share Alike 4.0 International. + +You can choose to use either of these licenses or both. + + +I, Wojtek Kosior, thereby promise not to sue for violation of this +file's licenses. Although I request that you do not make use this code +in a proprietary work, I am not going to enforce this in court. +#} +{% extends "base.html.jinja" %} +{% block title %} {{ _('web_ui.repos.single.title') }} {% endblock %} +{% block style %} + {{ super() }} + + {% include 'include/checkbox_tricks_style.css.jinja' %} +{% endblock %} +{% block main %} +

    + {% if display_info.ref.id != '1' %} + {{ + _('web_ui.repos.single.heading.name_{}') + .format(display_info.info.long_name) + }} + {% else %} + {{ _('web_ui.repos.local_packages_semirepo') }} + {% endif %} +

    + {% if display_info.deleted == True %} +
    + {{ _('web_ui.repos.single.repo_is_deleted') }} +
    + {% elif display_info.deleted == False %} + +
    +
    + {{ display_info.url }} +
    +
    + +
    +
    +
    + +
    + +
    +
    + + +
    +
    +
    + {% if display_info.last_refreshed is None %} + {{ _('web_ui.repos.single.repo_never_refreshed') }} + {% else %} + {{ + _('web_ui.repos.single.last_refreshed_{}') + .format(display_info.last_refreshed.strftime('%F %H:%M')) + }} + {% endif %} +
    + + +
    +
    + {% endif %} +
    + {{ + _('web_ui.repos.item_count_{mappings}_{resources}') + .format( + mappings = display_info.mapping_count, + resources = display_info.resource_count + ) + }} +
    +{% endblock %} -- cgit v1.2.3