mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-04-07 11:03:37 +00:00
Compare commits
2 Commits
dcb8724ed2
...
2.20.0rc5
| Author | SHA1 | Date | |
|---|---|---|---|
| 90a78e99e7 | |||
| a05eab9042 |
@@ -125,7 +125,7 @@ Again, the most checks can be performed by `tox` command, though some additional
|
||||
def __hash__(self) -> int: ... # basically any magic (or look-alike) method
|
||||
```
|
||||
|
||||
Methods inside one group should be ordered alphabetically, the only exceptions are `__init__` (`__post_init__` for dataclasses), `__new__` and `__del__` methods which should be defined first. For test methods it is recommended to follow the order in which functions are defined.
|
||||
Methods inside one group should be ordered alphabetically, the only exceptions are `__init__` (`__post_init__` for dataclasses), `__new__` and `__del__` methods which should be defined first. For test methods it is recommended to follow the order in which functions are defined. Same idea applies to frontend classes.
|
||||
|
||||
Though, we would like to highlight abstract methods (i.e. ones which raise `NotImplementedError`), we still keep in global order at the moment.
|
||||
|
||||
@@ -172,8 +172,9 @@ Again, the most checks can be performed by `tox` command, though some additional
|
||||
)
|
||||
```
|
||||
|
||||
* One file should define only one class, exception is class satellites in case if file length remains less than 400 lines.
|
||||
* It is possible to create file which contains some functions (e.g. `ahriman.core.util`), but in this case you would need to define `__all__` attribute.
|
||||
* Imports goes in alphabetical order, no relative imports allowed. Same rule applies to frontend classes.
|
||||
* One file should define only one class, exception is class satellites in case if file length remains less than 400 lines. Same rule applies to frontend classes.
|
||||
* It is possible to create file which contains some functions (e.g. `ahriman.core.utils`), but in this case you would need to define `__all__` attribute.
|
||||
* The file size mentioned above must be applicable in general. In case of big classes consider splitting them into traits. Note, however, that `pylint` includes comments and docstrings into counter, thus you need to check file size by other tools.
|
||||
* No global variable is allowed outside of `ahriman` module. `ahriman.core.context` is also special case.
|
||||
* Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent.
|
||||
@@ -226,6 +227,8 @@ Again, the most checks can be performed by `tox` command, though some additional
|
||||
|
||||
The projects also uses typing checks (provided by `mypy`) and some linter checks provided by `pylint` and `bandit`. Those checks must be passed successfully for any open pull requests.
|
||||
|
||||
Frontend checks normally are performed by `eslint` (e.g. `npx run eslint`).
|
||||
|
||||
## Developers how to
|
||||
|
||||
### Run automated checks
|
||||
|
||||
43
docs/_static/architecture.dot
vendored
43
docs/_static/architecture.dot
vendored
@@ -250,7 +250,9 @@ digraph G {
|
||||
ahriman_web_schemas [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas",shape="box"];
|
||||
ahriman_web_schemas_any_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nany_schema"];
|
||||
ahriman_web_schemas_aur_package_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\naur_package_schema"];
|
||||
ahriman_web_schemas_auth_info_schema [fillcolor="#c45431",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nauth_info_schema"];
|
||||
ahriman_web_schemas_auth_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nauth_schema"];
|
||||
ahriman_web_schemas_auto_refresh_interval_schema [fillcolor="#c45431",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nauto_refresh_interval_schema"];
|
||||
ahriman_web_schemas_build_options_schema [fillcolor="#d04e24",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nbuild_options_schema"];
|
||||
ahriman_web_schemas_changes_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nchanges_schema"];
|
||||
ahriman_web_schemas_configuration_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nconfiguration_schema"];
|
||||
@@ -261,6 +263,7 @@ digraph G {
|
||||
ahriman_web_schemas_event_search_schema [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas\.\nevent_search_schema",shape="box"];
|
||||
ahriman_web_schemas_file_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nfile_schema"];
|
||||
ahriman_web_schemas_info_schema [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas\.\ninfo_schema",shape="box"];
|
||||
ahriman_web_schemas_info_v2_schema [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas\.\ninfo_v2_schema",shape="box"];
|
||||
ahriman_web_schemas_internal_status_schema [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas\.\ninternal_status_schema",shape="box"];
|
||||
ahriman_web_schemas_log_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nlog_schema"];
|
||||
ahriman_web_schemas_login_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nlogin_schema"];
|
||||
@@ -289,11 +292,12 @@ digraph G {
|
||||
ahriman_web_schemas_status_schema [fillcolor="#ca4116",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nstatus_schema"];
|
||||
ahriman_web_schemas_update_flags_schema [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas\.\nupdate_flags_schema",shape="box"];
|
||||
ahriman_web_schemas_worker_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nworker_schema"];
|
||||
ahriman_web_server_info [fillcolor="#93371a",fontcolor="#ffffff",label="ahriman\.\nweb\.\nserver_info"];
|
||||
ahriman_web_views [fillcolor="#f94810",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews"];
|
||||
ahriman_web_views_api_docs [fillcolor="#794434",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\napi\.\ndocs"];
|
||||
ahriman_web_views_api_swagger [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\napi\.\nswagger"];
|
||||
ahriman_web_views_base [fillcolor="#952603",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nbase"];
|
||||
ahriman_web_views_index [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nindex"];
|
||||
ahriman_web_views_index [fillcolor="#794434",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nindex"];
|
||||
ahriman_web_views_static [fillcolor="#884d3a",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nstatic"];
|
||||
ahriman_web_views_status_view_guard [fillcolor="#ef3e06",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nstatus_view_guard"];
|
||||
ahriman_web_views_v1_auditlog_events [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nauditlog\.\nevents"];
|
||||
@@ -316,13 +320,14 @@ digraph G {
|
||||
ahriman_web_views_v1_service_search [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nservice\.\nsearch"];
|
||||
ahriman_web_views_v1_service_update [fillcolor="#724031",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nservice\.\nupdate"];
|
||||
ahriman_web_views_v1_service_upload [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nservice\.\nupload"];
|
||||
ahriman_web_views_v1_status_info [fillcolor="#724031",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nstatus\.\ninfo"];
|
||||
ahriman_web_views_v1_status_info [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nstatus\.\ninfo"];
|
||||
ahriman_web_views_v1_status_metrics [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nstatus\.\nmetrics"];
|
||||
ahriman_web_views_v1_status_repositories [fillcolor="#724031",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nstatus\.\nrepositories"];
|
||||
ahriman_web_views_v1_status_status [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nstatus\.\nstatus"];
|
||||
ahriman_web_views_v1_user_login [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nuser\.\nlogin"];
|
||||
ahriman_web_views_v1_user_logout [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nuser\.\nlogout"];
|
||||
ahriman_web_views_v2_packages_logs [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv2\.\npackages\.\nlogs"];
|
||||
ahriman_web_views_v2_status_info [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv2\.\nstatus\.\ninfo"];
|
||||
ahriman_web_web [fillcolor="#733826",fontcolor="#ffffff",label="ahriman\.\nweb\.\nweb"];
|
||||
aioauth_client [fillcolor="#c07d40",shape="folder"];
|
||||
aiohttp [fillcolor="#f9b506",shape="folder"];
|
||||
@@ -489,10 +494,10 @@ digraph G {
|
||||
ahriman_core -> ahriman_web_keys [fillcolor="#ef3e06",minlen="2"];
|
||||
ahriman_core -> ahriman_web_middlewares_auth_handler [fillcolor="#ef3e06",minlen="3"];
|
||||
ahriman_core -> ahriman_web_routes [fillcolor="#ef3e06",minlen="2"];
|
||||
ahriman_core -> ahriman_web_server_info [fillcolor="#ef3e06",minlen="2"];
|
||||
ahriman_core -> ahriman_web_views_api_docs [fillcolor="#ef3e06",minlen="3"];
|
||||
ahriman_core -> ahriman_web_views_api_swagger [fillcolor="#ef3e06",minlen="3"];
|
||||
ahriman_core -> ahriman_web_views_base [fillcolor="#ef3e06",minlen="3"];
|
||||
ahriman_core -> ahriman_web_views_index [fillcolor="#ef3e06",minlen="3"];
|
||||
ahriman_core -> ahriman_web_views_status_view_guard [fillcolor="#ef3e06",minlen="3"];
|
||||
ahriman_core -> ahriman_web_views_v1_distributed_workers [fillcolor="#ef3e06",minlen="3"];
|
||||
ahriman_core -> ahriman_web_views_v1_packages_logs [fillcolor="#ef3e06",minlen="3"];
|
||||
@@ -542,13 +547,13 @@ digraph G {
|
||||
ahriman_core_archive_archive_trigger -> ahriman_core_archive [fillcolor="blue",weight="3"];
|
||||
ahriman_core_auth -> ahriman_web_keys [fillcolor="blue",minlen="2"];
|
||||
ahriman_core_auth -> ahriman_web_middlewares_auth_handler [fillcolor="blue",minlen="3"];
|
||||
ahriman_core_auth -> ahriman_web_server_info [fillcolor="blue",minlen="2"];
|
||||
ahriman_core_auth -> ahriman_web_views_base [fillcolor="blue",minlen="3"];
|
||||
ahriman_core_auth -> ahriman_web_views_index [fillcolor="blue",minlen="3"];
|
||||
ahriman_core_auth -> ahriman_web_views_v1_user_login [fillcolor="blue",minlen="3"];
|
||||
ahriman_core_auth -> ahriman_web_views_v1_user_logout [fillcolor="blue",minlen="3"];
|
||||
ahriman_core_auth -> ahriman_web_web [fillcolor="blue",minlen="2"];
|
||||
ahriman_core_auth_auth -> ahriman_core_auth [fillcolor="blue",weight="3"];
|
||||
ahriman_core_auth_helpers -> ahriman_web_views_index [fillcolor="#d04e24",minlen="3"];
|
||||
ahriman_core_auth_helpers -> ahriman_web_server_info [fillcolor="#d04e24",minlen="3"];
|
||||
ahriman_core_auth_helpers -> ahriman_web_views_v1_user_login [fillcolor="#d04e24",minlen="3"];
|
||||
ahriman_core_auth_helpers -> ahriman_web_views_v1_user_logout [fillcolor="#d04e24",minlen="3"];
|
||||
ahriman_core_auth_mapping -> ahriman_core_auth_auth [fillcolor="blue",weight="3"];
|
||||
@@ -991,6 +996,7 @@ digraph G {
|
||||
ahriman_core_types -> ahriman_core_report_jinja_template [fillcolor="#f94810",minlen="2",weight="2"];
|
||||
ahriman_core_types -> ahriman_core_report_rss [fillcolor="#f94810",minlen="2",weight="2"];
|
||||
ahriman_core_types -> ahriman_core_utils [fillcolor="#f94810",weight="2"];
|
||||
ahriman_core_types -> ahriman_web_server_info [fillcolor="#f94810",minlen="2"];
|
||||
ahriman_core_types -> ahriman_web_views_v1_distributed_workers [fillcolor="#f94810",minlen="3"];
|
||||
ahriman_core_types -> ahriman_web_views_v1_packages_packages [fillcolor="#f94810",minlen="3"];
|
||||
ahriman_core_types -> ahriman_web_views_v1_service_search [fillcolor="#f94810",minlen="3"];
|
||||
@@ -1060,8 +1066,9 @@ digraph G {
|
||||
ahriman_core_utils -> ahriman_models_repository_paths [fillcolor="#db3805",minlen="2"];
|
||||
ahriman_core_utils -> ahriman_models_repository_stats [fillcolor="#db3805",minlen="2"];
|
||||
ahriman_core_utils -> ahriman_models_worker [fillcolor="#db3805",minlen="2"];
|
||||
ahriman_core_utils -> ahriman_web_server_info [fillcolor="#db3805",minlen="2"];
|
||||
ahriman_core_utils -> ahriman_web_views_api_swagger [fillcolor="#db3805",minlen="3"];
|
||||
ahriman_core_utils -> ahriman_web_views_index [fillcolor="#db3805",minlen="3"];
|
||||
ahriman_core_utils -> ahriman_web_views_base [fillcolor="#db3805",minlen="3"];
|
||||
ahriman_core_utils -> ahriman_web_views_v1_packages_logs [fillcolor="#db3805",minlen="3"];
|
||||
ahriman_core_utils -> ahriman_web_views_v1_service_upload [fillcolor="#db3805",minlen="3"];
|
||||
ahriman_models -> ahriman_application_ahriman [fillcolor="#f94810",minlen="2"];
|
||||
@@ -1245,6 +1252,7 @@ digraph G {
|
||||
ahriman_models -> ahriman_web_views_v1_user_login [fillcolor="#f94810",minlen="3"];
|
||||
ahriman_models -> ahriman_web_views_v1_user_logout [fillcolor="#f94810",minlen="3"];
|
||||
ahriman_models -> ahriman_web_views_v2_packages_logs [fillcolor="#f94810",minlen="3"];
|
||||
ahriman_models -> ahriman_web_views_v2_status_info [fillcolor="#f94810",minlen="3"];
|
||||
ahriman_models -> ahriman_web_web [fillcolor="#f94810",minlen="2"];
|
||||
ahriman_models_action -> ahriman_application_handlers_change [fillcolor="#e75222",minlen="3"];
|
||||
ahriman_models_action -> ahriman_application_handlers_patch [fillcolor="#e75222",minlen="3"];
|
||||
@@ -1653,6 +1661,7 @@ digraph G {
|
||||
ahriman_models_user_access -> ahriman_web_views_v1_user_login [fillcolor="#f94810",minlen="3"];
|
||||
ahriman_models_user_access -> ahriman_web_views_v1_user_logout [fillcolor="#f94810",minlen="3"];
|
||||
ahriman_models_user_access -> ahriman_web_views_v2_packages_logs [fillcolor="#f94810",minlen="3"];
|
||||
ahriman_models_user_access -> ahriman_web_views_v2_status_info [fillcolor="#f94810",minlen="3"];
|
||||
ahriman_models_waiter -> ahriman_application_lock [fillcolor="#c45431",minlen="2"];
|
||||
ahriman_models_waiter -> ahriman_core_report_remote_call [fillcolor="#c45431",minlen="3"];
|
||||
ahriman_models_worker -> ahriman_application_application_workers_remote_updater [fillcolor="#e9410c",minlen="3"];
|
||||
@@ -1663,7 +1672,9 @@ digraph G {
|
||||
ahriman_web -> ahriman_application_handlers_web [fillcolor="#f94810",minlen="3"];
|
||||
ahriman_web_apispec -> ahriman_web_schemas_any_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_schemas_aur_package_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_schemas_auth_info_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_schemas_auth_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_schemas_auto_refresh_interval_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_schemas_build_options_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_schemas_changes_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_schemas_configuration_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
@@ -1674,6 +1685,7 @@ digraph G {
|
||||
ahriman_web_apispec -> ahriman_web_schemas_event_search_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_schemas_file_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_schemas_info_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_schemas_info_v2_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_schemas_internal_status_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_schemas_log_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_schemas_login_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
@@ -1702,9 +1714,9 @@ digraph G {
|
||||
ahriman_web_apispec -> ahriman_web_schemas_status_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_schemas_update_flags_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_schemas_worker_schema [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_server_info [fillcolor="#e53b05",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_views_api_docs [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_views_api_swagger [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_views_index [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_views_v1_auditlog_events [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_views_v1_distributed_workers [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_views_v1_packages_changes [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
@@ -1732,6 +1744,7 @@ digraph G {
|
||||
ahriman_web_apispec -> ahriman_web_views_v1_user_login [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_views_v1_user_logout [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_views_v2_packages_logs [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_views_v2_status_info [fillcolor="#e53b05",minlen="2",weight="2"];
|
||||
ahriman_web_apispec -> ahriman_web_web [fillcolor="#e53b05",weight="2"];
|
||||
ahriman_web_apispec_decorators -> ahriman_web_views_v1_auditlog_events [fillcolor="#bd3104",minlen="2",weight="2"];
|
||||
ahriman_web_apispec_decorators -> ahriman_web_views_v1_distributed_workers [fillcolor="#bd3104",minlen="2",weight="2"];
|
||||
@@ -1760,6 +1773,7 @@ digraph G {
|
||||
ahriman_web_apispec_decorators -> ahriman_web_views_v1_user_login [fillcolor="#bd3104",minlen="2",weight="2"];
|
||||
ahriman_web_apispec_decorators -> ahriman_web_views_v1_user_logout [fillcolor="#bd3104",minlen="2",weight="2"];
|
||||
ahriman_web_apispec_decorators -> ahriman_web_views_v2_packages_logs [fillcolor="#bd3104",minlen="2",weight="2"];
|
||||
ahriman_web_apispec_decorators -> ahriman_web_views_v2_status_info [fillcolor="#bd3104",minlen="2",weight="2"];
|
||||
ahriman_web_apispec_info -> ahriman_web_web [fillcolor="#a14f35",minlen="2",weight="2"];
|
||||
ahriman_web_cors -> ahriman_web_web [fillcolor="#b0573a",weight="2"];
|
||||
ahriman_web_keys -> ahriman_web_apispec_info [fillcolor="#823017",minlen="2",weight="2"];
|
||||
@@ -1799,9 +1813,14 @@ digraph G {
|
||||
ahriman_web_schemas -> ahriman_web_views_v1_status_status [fillcolor="blue",minlen="2",weight="2"];
|
||||
ahriman_web_schemas -> ahriman_web_views_v1_user_login [fillcolor="blue",minlen="2",weight="2"];
|
||||
ahriman_web_schemas -> ahriman_web_views_v2_packages_logs [fillcolor="blue",minlen="2",weight="2"];
|
||||
ahriman_web_schemas -> ahriman_web_views_v2_status_info [fillcolor="blue",minlen="2",weight="2"];
|
||||
ahriman_web_schemas_any_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"];
|
||||
ahriman_web_schemas_aur_package_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"];
|
||||
ahriman_web_schemas_auth_info_schema -> ahriman_web_schemas [fillcolor="#c45431",weight="3"];
|
||||
ahriman_web_schemas_auth_info_schema -> ahriman_web_schemas_info_v2_schema [fillcolor="#c45431",weight="3"];
|
||||
ahriman_web_schemas_auth_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"];
|
||||
ahriman_web_schemas_auto_refresh_interval_schema -> ahriman_web_schemas [fillcolor="#c45431",weight="3"];
|
||||
ahriman_web_schemas_auto_refresh_interval_schema -> ahriman_web_schemas_info_v2_schema [fillcolor="#c45431",weight="3"];
|
||||
ahriman_web_schemas_build_options_schema -> ahriman_web_schemas [fillcolor="#d04e24",weight="3"];
|
||||
ahriman_web_schemas_build_options_schema -> ahriman_web_schemas_package_names_schema [fillcolor="#d04e24",weight="3"];
|
||||
ahriman_web_schemas_build_options_schema -> ahriman_web_schemas_update_flags_schema [fillcolor="#d04e24",weight="3"];
|
||||
@@ -1815,6 +1834,7 @@ digraph G {
|
||||
ahriman_web_schemas_event_search_schema -> ahriman_web_schemas [fillcolor="blue",weight="3"];
|
||||
ahriman_web_schemas_file_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"];
|
||||
ahriman_web_schemas_info_schema -> ahriman_web_schemas [fillcolor="blue",weight="3"];
|
||||
ahriman_web_schemas_info_v2_schema -> ahriman_web_schemas [fillcolor="blue",weight="3"];
|
||||
ahriman_web_schemas_internal_status_schema -> ahriman_web_schemas [fillcolor="blue",weight="3"];
|
||||
ahriman_web_schemas_log_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"];
|
||||
ahriman_web_schemas_login_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"];
|
||||
@@ -1847,6 +1867,7 @@ digraph G {
|
||||
ahriman_web_schemas_remote_schema -> ahriman_web_schemas_package_schema [fillcolor="#b44d2d",weight="3"];
|
||||
ahriman_web_schemas_repository_id_schema -> ahriman_web_schemas [fillcolor="#ef3e06",weight="3"];
|
||||
ahriman_web_schemas_repository_id_schema -> ahriman_web_schemas_info_schema [fillcolor="#ef3e06",weight="3"];
|
||||
ahriman_web_schemas_repository_id_schema -> ahriman_web_schemas_info_v2_schema [fillcolor="#ef3e06",weight="3"];
|
||||
ahriman_web_schemas_repository_id_schema -> ahriman_web_schemas_internal_status_schema [fillcolor="#ef3e06",weight="3"];
|
||||
ahriman_web_schemas_repository_id_schema -> ahriman_web_schemas_package_status_schema [fillcolor="#ef3e06",weight="3"];
|
||||
ahriman_web_schemas_repository_id_schema -> ahriman_web_schemas_package_version_schema [fillcolor="#ef3e06",weight="3"];
|
||||
@@ -1860,8 +1881,13 @@ digraph G {
|
||||
ahriman_web_schemas_status_schema -> ahriman_web_schemas_package_status_schema [fillcolor="#ca4116",weight="3"];
|
||||
ahriman_web_schemas_update_flags_schema -> ahriman_web_schemas [fillcolor="blue",weight="3"];
|
||||
ahriman_web_schemas_worker_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"];
|
||||
ahriman_web_server_info -> ahriman_web_views_index [fillcolor="#93371a",minlen="2",weight="2"];
|
||||
ahriman_web_server_info -> ahriman_web_views_v1_status_info [fillcolor="#93371a",minlen="2",weight="2"];
|
||||
ahriman_web_server_info -> ahriman_web_views_v2_status_info [fillcolor="#93371a",minlen="2",weight="2"];
|
||||
ahriman_web_views -> ahriman_web_routes [fillcolor="#f94810",weight="2"];
|
||||
ahriman_web_views -> ahriman_web_server_info [fillcolor="#f94810",weight="2"];
|
||||
ahriman_web_views_base -> ahriman_web_routes [fillcolor="#952603",minlen="2",weight="2"];
|
||||
ahriman_web_views_base -> ahriman_web_server_info [fillcolor="#952603",minlen="2",weight="2"];
|
||||
ahriman_web_views_base -> ahriman_web_views_api_docs [fillcolor="#952603",weight="3"];
|
||||
ahriman_web_views_base -> ahriman_web_views_api_swagger [fillcolor="#952603",weight="3"];
|
||||
ahriman_web_views_base -> ahriman_web_views_index [fillcolor="#952603",weight="3"];
|
||||
@@ -1893,6 +1919,7 @@ digraph G {
|
||||
ahriman_web_views_base -> ahriman_web_views_v1_user_login [fillcolor="#952603",weight="3"];
|
||||
ahriman_web_views_base -> ahriman_web_views_v1_user_logout [fillcolor="#952603",weight="3"];
|
||||
ahriman_web_views_base -> ahriman_web_views_v2_packages_logs [fillcolor="#952603",weight="3"];
|
||||
ahriman_web_views_base -> ahriman_web_views_v2_status_info [fillcolor="#952603",weight="3"];
|
||||
ahriman_web_views_status_view_guard -> ahriman_web_views_v1_packages_changes [fillcolor="#ef3e06",weight="3"];
|
||||
ahriman_web_views_status_view_guard -> ahriman_web_views_v1_packages_dependencies [fillcolor="#ef3e06",weight="3"];
|
||||
ahriman_web_views_status_view_guard -> ahriman_web_views_v1_packages_logs [fillcolor="#ef3e06",weight="3"];
|
||||
@@ -1915,6 +1942,7 @@ digraph G {
|
||||
aiohttp -> ahriman_web_routes [fillcolor="#f9b506",minlen="2"];
|
||||
aiohttp -> ahriman_web_views_api_swagger [fillcolor="#f9b506",minlen="3"];
|
||||
aiohttp -> ahriman_web_views_base [fillcolor="#f9b506",minlen="3"];
|
||||
aiohttp -> ahriman_web_views_index [fillcolor="#f9b506",minlen="3"];
|
||||
aiohttp -> ahriman_web_views_static [fillcolor="#f9b506",minlen="3"];
|
||||
aiohttp -> ahriman_web_views_v1_auditlog_events [fillcolor="#f9b506",minlen="3"];
|
||||
aiohttp -> ahriman_web_views_v1_distributed_workers [fillcolor="#f9b506",minlen="3"];
|
||||
@@ -1943,6 +1971,7 @@ digraph G {
|
||||
aiohttp -> ahriman_web_views_v1_user_login [fillcolor="#f9b506",minlen="3"];
|
||||
aiohttp -> ahriman_web_views_v1_user_logout [fillcolor="#f9b506",minlen="3"];
|
||||
aiohttp -> ahriman_web_views_v2_packages_logs [fillcolor="#f9b506",minlen="3"];
|
||||
aiohttp -> ahriman_web_views_v2_status_info [fillcolor="#f9b506",minlen="3"];
|
||||
aiohttp -> ahriman_web_web [fillcolor="#f9b506",minlen="2"];
|
||||
aiohttp -> aiohttp_cors [fillcolor="#f9b506",minlen="2"];
|
||||
aiohttp -> aiohttp_jinja2 [fillcolor="#f9b506",minlen="2"];
|
||||
|
||||
@@ -18,16 +18,17 @@
|
||||
"@mui/x-data-grid": "^8.27.3",
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"highlight.js": "^11.11.0",
|
||||
"react": "^19.2.4",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "^19.2.4"
|
||||
"react-dom": "^19.2.4",
|
||||
"react-syntax-highlighter": "^16.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.3",
|
||||
"@stylistic/eslint-plugin": "^5.9.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"eslint": "^9.39.3",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
|
||||
@@ -38,20 +38,18 @@ const queryClient = new QueryClient({
|
||||
});
|
||||
|
||||
export default function App(): React.JSX.Element {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider theme={Theme}>
|
||||
<CssBaseline />
|
||||
return <QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider theme={Theme}>
|
||||
<CssBaseline />
|
||||
<NotificationProvider>
|
||||
<ClientProvider>
|
||||
<AuthProvider>
|
||||
<RepositoryProvider>
|
||||
<NotificationProvider>
|
||||
<AppLayout />
|
||||
</NotificationProvider>
|
||||
<AppLayout />
|
||||
</RepositoryProvider>
|
||||
</AuthProvider>
|
||||
</ClientProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
</NotificationProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>;
|
||||
}
|
||||
|
||||
@@ -17,14 +17,15 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { BaseClient } from "api/client/BaseClient";
|
||||
import { FetchMixin } from "api/client/FetchMixin";
|
||||
import { ServiceMixin } from "api/client/ServiceMixin";
|
||||
import { Client } from "api/client/Client";
|
||||
import { FetchClient } from "api/client/FetchClient";
|
||||
import { ServiceClient } from "api/client/ServiceClient";
|
||||
import type { LoginRequest } from "models/LoginRequest";
|
||||
import { applyMixins } from "utils";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
|
||||
export class AhrimanClient extends BaseClient {
|
||||
export class AhrimanClient extends Client {
|
||||
|
||||
readonly fetch = new FetchClient(this);
|
||||
readonly service = new ServiceClient(this);
|
||||
|
||||
async login(data: LoginRequest): Promise<void> {
|
||||
return this.request("/api/v1/login", { method: "POST", json: data });
|
||||
@@ -34,7 +35,3 @@ export class AhrimanClient extends BaseClient {
|
||||
return this.request("/api/v1/logout", { method: "POST" });
|
||||
}
|
||||
}
|
||||
|
||||
export interface AhrimanClient extends FetchMixin, ServiceMixin {}
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-declaration-merging */
|
||||
applyMixins(AhrimanClient, [FetchMixin, ServiceMixin]);
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
import { ApiError } from "api/client/ApiError";
|
||||
import type { RequestOptions } from "api/client/RequestOptions";
|
||||
|
||||
export class BaseClient {
|
||||
export class Client {
|
||||
|
||||
protected async request<T>(url: string, options: RequestOptions = {}): Promise<T> {
|
||||
const { method, query, json } = options;
|
||||
private static readonly DEFAULT_TIMEOUT = 30_000;
|
||||
|
||||
async request<T>(url: string, options: RequestOptions = {}): Promise<T> {
|
||||
const { method, query, json, timeout = Client.DEFAULT_TIMEOUT } = options;
|
||||
|
||||
let fullUrl = url;
|
||||
if (query) {
|
||||
@@ -43,30 +45,35 @@ export class BaseClient {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
const requestInit: RequestInit = {
|
||||
method: method || (json ? "POST" : "GET"),
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
if (json !== undefined) {
|
||||
requestInit.body = JSON.stringify(json);
|
||||
}
|
||||
|
||||
const response = await fetch(fullUrl, requestInit);
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(fullUrl, requestInit);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
throw new ApiError(response.status, response.statusText, body);
|
||||
}
|
||||
|
||||
if (response.redirected) {
|
||||
const contentType = response.headers.get("Content-Type") ?? "";
|
||||
if (!contentType.includes("application/json")) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("Content-Type") ?? "";
|
||||
if (contentType.includes("application/json")) {
|
||||
return await response.json() as T;
|
||||
}
|
||||
return await response.text() as T;
|
||||
return await response.json() as T;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { BaseClient } from "api/client/BaseClient";
|
||||
import type { Client } from "api/client/Client";
|
||||
import type { Changes } from "models/Changes";
|
||||
import type { Dependencies } from "models/Dependencies";
|
||||
import type { Event } from "models/Event";
|
||||
@@ -28,22 +28,28 @@ import type { PackageStatus } from "models/PackageStatus";
|
||||
import type { Patch } from "models/Patch";
|
||||
import { RepositoryId } from "models/RepositoryId";
|
||||
|
||||
export class FetchMixin extends BaseClient {
|
||||
export class FetchClient {
|
||||
|
||||
protected client: Client;
|
||||
|
||||
constructor(client: Client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async fetchPackage(packageBase: string, repository: RepositoryId): Promise<PackageStatus[]> {
|
||||
return this.request<PackageStatus[]>(`/api/v1/packages/${encodeURIComponent(packageBase)}`, {
|
||||
return this.client.request<PackageStatus[]>(`/api/v1/packages/${encodeURIComponent(packageBase)}`, {
|
||||
query: repository.toQuery(),
|
||||
});
|
||||
}
|
||||
|
||||
async fetchPackageChanges(packageBase: string, repository: RepositoryId): Promise<Changes> {
|
||||
return this.request<Changes>(`/api/v1/packages/${encodeURIComponent(packageBase)}/changes`, {
|
||||
return this.client.request<Changes>(`/api/v1/packages/${encodeURIComponent(packageBase)}/changes`, {
|
||||
query: repository.toQuery(),
|
||||
});
|
||||
}
|
||||
|
||||
async fetchPackageDependencies(packageBase: string, repository: RepositoryId): Promise<Dependencies> {
|
||||
return this.request<Dependencies>(`/api/v1/packages/${encodeURIComponent(packageBase)}/dependencies`, {
|
||||
return this.client.request<Dependencies>(`/api/v1/packages/${encodeURIComponent(packageBase)}/dependencies`, {
|
||||
query: repository.toQuery(),
|
||||
});
|
||||
}
|
||||
@@ -56,7 +62,7 @@ export class FetchMixin extends BaseClient {
|
||||
if (limit) {
|
||||
query.limit = limit;
|
||||
}
|
||||
return this.request<Event[]>("/api/v1/events", { query });
|
||||
return this.client.request<Event[]>("/api/v1/events", { query });
|
||||
}
|
||||
|
||||
async fetchPackageLogs(
|
||||
@@ -76,26 +82,28 @@ export class FetchMixin extends BaseClient {
|
||||
if (head) {
|
||||
query.head = true;
|
||||
}
|
||||
return this.request<LogRecord[]>(`/api/v2/packages/${encodeURIComponent(packageBase)}/logs`, { query });
|
||||
return this.client.request<LogRecord[]>(`/api/v2/packages/${encodeURIComponent(packageBase)}/logs`, { query });
|
||||
}
|
||||
|
||||
async fetchPackagePatches(packageBase: string): Promise<Patch[]> {
|
||||
return this.request<Patch[]>(`/api/v1/packages/${encodeURIComponent(packageBase)}/patches`);
|
||||
return this.client.request<Patch[]>(`/api/v1/packages/${encodeURIComponent(packageBase)}/patches`);
|
||||
}
|
||||
|
||||
async fetchPackages(repository: RepositoryId): Promise<PackageStatus[]> {
|
||||
return this.request<PackageStatus[]>("/api/v1/packages", { query: repository.toQuery() });
|
||||
return this.client.request<PackageStatus[]>("/api/v1/packages", { query: repository.toQuery() });
|
||||
}
|
||||
|
||||
async fetchServerInfo(): Promise<InfoResponse> {
|
||||
const info = await this.request<InfoResponse>("/api/v2/info");
|
||||
info.repositories = info.repositories.map(repositories =>
|
||||
new RepositoryId(repositories.architecture, repositories.repository),
|
||||
);
|
||||
return info;
|
||||
const info = await this.client.request<InfoResponse>("/api/v2/info");
|
||||
return {
|
||||
...info,
|
||||
repositories: info.repositories.map(repo =>
|
||||
new RepositoryId(repo.architecture, repo.repository),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async fetchServerStatus(repository: RepositoryId): Promise<InternalStatus> {
|
||||
return this.request<InternalStatus>("/api/v1/status", { query: repository.toQuery() });
|
||||
return this.client.request<InternalStatus>("/api/v1/status", { query: repository.toQuery() });
|
||||
}
|
||||
}
|
||||
@@ -21,4 +21,5 @@ export interface RequestOptions {
|
||||
method?: string;
|
||||
query?: Record<string, string | number | boolean>;
|
||||
json?: unknown;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
@@ -17,27 +17,33 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { BaseClient } from "api/client/BaseClient";
|
||||
import type { Client } from "api/client/Client";
|
||||
import type { AURPackage } from "models/AURPackage";
|
||||
import type { PackageActionRequest } from "models/PackageActionRequest";
|
||||
import type { PGPKey } from "models/PGPKey";
|
||||
import type { PGPKeyRequest } from "models/PGPKeyRequest";
|
||||
import type { RepositoryId } from "models/RepositoryId";
|
||||
|
||||
export class ServiceMixin extends BaseClient {
|
||||
export class ServiceClient {
|
||||
|
||||
protected client: Client;
|
||||
|
||||
constructor(client: Client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async servicePackageAdd(repository: RepositoryId, data: PackageActionRequest): Promise<void> {
|
||||
return this.request("/api/v1/service/add", { method: "POST", query: repository.toQuery(), json: data });
|
||||
return this.client.request("/api/v1/service/add", { method: "POST", query: repository.toQuery(), json: data });
|
||||
}
|
||||
|
||||
async servicePackagePatchRemove(packageBase: string, key: string): Promise<void> {
|
||||
return this.request(`/api/v1/packages/${encodeURIComponent(packageBase)}/patches/${encodeURIComponent(key)}`, {
|
||||
return this.client.request(`/api/v1/packages/${encodeURIComponent(packageBase)}/patches/${encodeURIComponent(key)}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
async servicePackageRemove(repository: RepositoryId, packages: string[]): Promise<void> {
|
||||
return this.request("/api/v1/service/remove", {
|
||||
return this.client.request("/api/v1/service/remove", {
|
||||
method: "POST",
|
||||
query: repository.toQuery(),
|
||||
json: { packages },
|
||||
@@ -45,7 +51,7 @@ export class ServiceMixin extends BaseClient {
|
||||
}
|
||||
|
||||
async servicePackageRequest(repository: RepositoryId, data: PackageActionRequest): Promise<void> {
|
||||
return this.request("/api/v1/service/request", {
|
||||
return this.client.request("/api/v1/service/request", {
|
||||
method: "POST",
|
||||
query: repository.toQuery(),
|
||||
json: data,
|
||||
@@ -53,11 +59,11 @@ export class ServiceMixin extends BaseClient {
|
||||
}
|
||||
|
||||
async servicePackageSearch(query: string): Promise<AURPackage[]> {
|
||||
return this.request<AURPackage[]>("/api/v1/service/search", { query: { for: query } });
|
||||
return this.client.request<AURPackage[]>("/api/v1/service/search", { query: { for: query } });
|
||||
}
|
||||
|
||||
async servicePackageUpdate(repository: RepositoryId, data: PackageActionRequest): Promise<void> {
|
||||
return this.request("/api/v1/service/update", {
|
||||
return this.client.request("/api/v1/service/update", {
|
||||
method: "POST",
|
||||
query: repository.toQuery(),
|
||||
json: data,
|
||||
@@ -65,15 +71,15 @@ export class ServiceMixin extends BaseClient {
|
||||
}
|
||||
|
||||
async servicePGPFetch(key: string, server: string): Promise<PGPKey> {
|
||||
return this.request<PGPKey>("/api/v1/service/pgp", { query: { key, server } });
|
||||
return this.client.request<PGPKey>("/api/v1/service/pgp", { query: { key, server } });
|
||||
}
|
||||
|
||||
async servicePGPImport(data: PGPKeyRequest): Promise<void> {
|
||||
return this.request("/api/v1/service/pgp", { method: "POST", json: data });
|
||||
return this.client.request("/api/v1/service/pgp", { method: "POST", json: data });
|
||||
}
|
||||
|
||||
async serviceRebuild(repository: RepositoryId, packages: string[]): Promise<void> {
|
||||
return this.request("/api/v1/service/rebuild", {
|
||||
return this.client.request("/api/v1/service/rebuild", {
|
||||
method: "POST",
|
||||
query: repository.toQuery(),
|
||||
json: { packages },
|
||||
@@ -17,7 +17,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import type { BuildStatus } from "models/BuildStatus.ts";
|
||||
import type { BuildStatus } from "models/BuildStatus";
|
||||
import type { Counters } from "models/Counters";
|
||||
import type React from "react";
|
||||
import { Pie } from "react-chartjs-2";
|
||||
|
||||
@@ -22,9 +22,7 @@ import CopyButton from "components/common/CopyButton";
|
||||
import React, { type RefObject } from "react";
|
||||
|
||||
interface CodeBlockProps {
|
||||
codeRef?: RefObject<HTMLElement | null>;
|
||||
preRef?: RefObject<HTMLElement | null>;
|
||||
className?: string;
|
||||
getText: () => string;
|
||||
height?: number | string;
|
||||
onScroll?: () => void;
|
||||
@@ -32,9 +30,7 @@ interface CodeBlockProps {
|
||||
}
|
||||
|
||||
export default function CodeBlock({
|
||||
codeRef,
|
||||
preRef,
|
||||
className,
|
||||
getText,
|
||||
height,
|
||||
onScroll,
|
||||
@@ -56,8 +52,8 @@ export default function CodeBlock({
|
||||
...wordBreak ? { whiteSpace: "pre-wrap", wordBreak: "break-all" } : {},
|
||||
}}
|
||||
>
|
||||
<code ref={codeRef} className={className}>
|
||||
{!codeRef && getText()}
|
||||
<code>
|
||||
{getText()}
|
||||
</code>
|
||||
</Box>
|
||||
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Alert, Slide } from "@mui/material";
|
||||
import type { Notification } from "contexts/Notification";
|
||||
import type { Notification } from "models/Notification";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface NotificationItemProps {
|
||||
notification: Notification;
|
||||
onClose: (id: number) => void;
|
||||
onClose: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function NotificationItem({ notification, onClose }: NotificationItemProps): React.JSX.Element {
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2026 ahriman team.
|
||||
*
|
||||
* This file is part of ahriman
|
||||
* (see https://github.com/arcan1s/ahriman).
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React from "react";
|
||||
|
||||
interface StringListProps {
|
||||
items: string[];
|
||||
}
|
||||
|
||||
export default function StringList({ items }: StringListProps): React.JSX.Element {
|
||||
return <>{items.join("\n")}</>;
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export default function DashboardDialog({ open, onClose }: DashboardDialogProps)
|
||||
|
||||
const { data: status } = useQuery<InternalStatus>({
|
||||
queryKey: current ? QueryKeys.status(current) : ["status"],
|
||||
queryFn: current ? () => client.fetchServerStatus(current) : skipToken,
|
||||
queryFn: current ? () => client.fetch.fetchServerStatus(current) : skipToken,
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
|
||||
@@ -47,14 +47,10 @@ export default function KeyImportDialog({ open, onClose }: KeyImportDialogProps)
|
||||
const [server, setServer] = useState("keyserver.ubuntu.com");
|
||||
const [keyBody, setKeyBody] = useState("");
|
||||
|
||||
const resetFields = (): void => {
|
||||
const handleClose = (): void => {
|
||||
setFingerprint("");
|
||||
setServer("keyserver.ubuntu.com");
|
||||
setKeyBody("");
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
resetFields();
|
||||
onClose();
|
||||
};
|
||||
|
||||
@@ -63,7 +59,7 @@ export default function KeyImportDialog({ open, onClose }: KeyImportDialogProps)
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await client.servicePGPFetch(fingerprint, server);
|
||||
const result = await client.service.servicePGPFetch(fingerprint, server);
|
||||
setKeyBody(result.key);
|
||||
} catch (exception) {
|
||||
const detail = ApiError.errorDetail(exception);
|
||||
@@ -76,7 +72,7 @@ export default function KeyImportDialog({ open, onClose }: KeyImportDialogProps)
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await client.servicePGPImport({ key: fingerprint, server });
|
||||
await client.service.servicePGPImport({ key: fingerprint, server });
|
||||
handleClose();
|
||||
showSuccess("Success", `Key ${fingerprint} has been imported`);
|
||||
} catch (exception) {
|
||||
|
||||
@@ -42,6 +42,7 @@ import { useDebounce } from "hooks/useDebounce";
|
||||
import { useNotification } from "hooks/useNotification";
|
||||
import { useSelectedRepository } from "hooks/useSelectedRepository";
|
||||
import type { AURPackage } from "models/AURPackage";
|
||||
import type { PackageActionRequest } from "models/PackageActionRequest";
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
interface EnvironmentVariable {
|
||||
@@ -77,11 +78,11 @@ export default function PackageAddDialog({ open, onClose }: PackageAddDialogProp
|
||||
|
||||
const { data: searchResults = [] } = useQuery<AURPackage[]>({
|
||||
queryKey: QueryKeys.search(debouncedSearch),
|
||||
queryFn: () => client.servicePackageSearch(debouncedSearch),
|
||||
queryFn: () => client.service.servicePackageSearch(debouncedSearch),
|
||||
enabled: debouncedSearch.length >= 3,
|
||||
});
|
||||
|
||||
const handleAdd: () => Promise<void> = async () => {
|
||||
const handleSubmit = async (action: "add" | "request"): Promise<void> => {
|
||||
if (!packageName) {
|
||||
return;
|
||||
}
|
||||
@@ -91,38 +92,18 @@ export default function PackageAddDialog({ open, onClose }: PackageAddDialogProp
|
||||
}
|
||||
try {
|
||||
const patches = environmentVariables.filter(variable => variable.key);
|
||||
await client.servicePackageAdd(repository, {
|
||||
packages: [packageName],
|
||||
patches,
|
||||
refresh: refreshDatabase,
|
||||
});
|
||||
const request: PackageActionRequest = { packages: [packageName], patches };
|
||||
if (action === "add") {
|
||||
request.refresh = refreshDatabase;
|
||||
await client.service.servicePackageAdd(repository, request);
|
||||
} else {
|
||||
await client.service.servicePackageRequest(repository, request);
|
||||
}
|
||||
handleClose();
|
||||
showSuccess("Success", `Packages ${packageName} have been added`);
|
||||
showSuccess("Success", `Packages ${packageName} have been ${action === "add" ? "added" : "requested"}`);
|
||||
} catch (exception) {
|
||||
const detail = ApiError.errorDetail(exception);
|
||||
showError("Action failed", `Package addition failed: ${detail}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRequest: () => Promise<void> = async () => {
|
||||
if (!packageName) {
|
||||
return;
|
||||
}
|
||||
const repository = repositorySelect.selectedRepository;
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const patches = environmentVariables.filter(variable => variable.key);
|
||||
await client.servicePackageRequest(repository, {
|
||||
packages: [packageName],
|
||||
patches,
|
||||
});
|
||||
handleClose();
|
||||
showSuccess("Success", `Packages ${packageName} have been requested`);
|
||||
} catch (exception) {
|
||||
const detail = ApiError.errorDetail(exception);
|
||||
showError("Action failed", `Package request failed: ${detail}`);
|
||||
showError("Action failed", `Package ${action} failed: ${detail}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -205,8 +186,8 @@ export default function PackageAddDialog({ open, onClose }: PackageAddDialogProp
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={() => void handleAdd()} variant="contained" startIcon={<PlayArrowIcon />}>add</Button>
|
||||
<Button onClick={() => void handleRequest()} variant="contained" color="success" startIcon={<AddIcon />}>request</Button>
|
||||
<Button onClick={() => void handleSubmit("add")} variant="contained" startIcon={<PlayArrowIcon />}>add</Button>
|
||||
<Button onClick={() => void handleSubmit("request")} variant="contained" color="success" startIcon={<AddIcon />}>request</Button>
|
||||
</DialogActions>
|
||||
</Dialog>;
|
||||
}
|
||||
|
||||
@@ -60,6 +60,11 @@ export default function PackageInfoDialog({
|
||||
const { showSuccess, showError } = useNotification();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [localPackageBase, setLocalPackageBase] = useState(packageBase);
|
||||
if (packageBase !== null && packageBase !== localPackageBase) {
|
||||
setLocalPackageBase(packageBase);
|
||||
}
|
||||
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
const [refreshDatabase, setRefreshDatabase] = useState(true);
|
||||
|
||||
@@ -72,21 +77,22 @@ export default function PackageInfoDialog({
|
||||
const autoRefresh = useAutoRefresh("package-info-autoreload-button", defaultInterval(autoRefreshIntervals));
|
||||
|
||||
const { data: packageData } = useQuery<PackageStatus[]>({
|
||||
queryKey: packageBase && current ? QueryKeys.package(packageBase, current) : ["packages"],
|
||||
queryFn: packageBase && current ? () => client.fetchPackage(packageBase, current) : skipToken,
|
||||
queryKey: localPackageBase && current ? QueryKeys.package(localPackageBase, current) : ["packages"],
|
||||
queryFn: localPackageBase && current ? () => client.fetch.fetchPackage(localPackageBase, current) : skipToken,
|
||||
enabled: open,
|
||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||
});
|
||||
|
||||
const { data: dependencies } = useQuery<Dependencies>({
|
||||
queryKey: packageBase && current ? QueryKeys.dependencies(packageBase, current) : ["dependencies"],
|
||||
queryFn: packageBase && current ? () => client.fetchPackageDependencies(packageBase, current) : skipToken,
|
||||
queryKey: localPackageBase && current ? QueryKeys.dependencies(localPackageBase, current) : ["dependencies"],
|
||||
queryFn: localPackageBase && current
|
||||
? () => client.fetch.fetchPackageDependencies(localPackageBase, current) : skipToken,
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const { data: patches = [] } = useQuery<Patch[]>({
|
||||
queryKey: packageBase ? QueryKeys.patches(packageBase) : ["patches"],
|
||||
queryFn: packageBase ? () => client.fetchPackagePatches(packageBase) : skipToken,
|
||||
queryKey: localPackageBase ? QueryKeys.patches(localPackageBase) : ["patches"],
|
||||
queryFn: localPackageBase ? () => client.fetch.fetchPackagePatches(localPackageBase) : skipToken,
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
@@ -96,24 +102,24 @@ export default function PackageInfoDialog({
|
||||
const headerStyle = status ? StatusHeaderStyles[status.status] : {};
|
||||
|
||||
const handleUpdate: () => Promise<void> = async () => {
|
||||
if (!packageBase || !current) {
|
||||
if (!localPackageBase || !current) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await client.servicePackageAdd(current, { packages: [packageBase], refresh: refreshDatabase });
|
||||
showSuccess("Success", `Run update for packages ${packageBase}`);
|
||||
await client.service.servicePackageAdd(current, { packages: [localPackageBase], refresh: refreshDatabase });
|
||||
showSuccess("Success", `Run update for packages ${localPackageBase}`);
|
||||
} catch (exception) {
|
||||
showError("Action failed", `Package update failed: ${ApiError.errorDetail(exception)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove: () => Promise<void> = async () => {
|
||||
if (!packageBase || !current) {
|
||||
if (!localPackageBase || !current) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await client.servicePackageRemove(current, [packageBase]);
|
||||
showSuccess("Success", `Packages ${packageBase} have been removed`);
|
||||
await client.service.servicePackageRemove(current, [localPackageBase]);
|
||||
showSuccess("Success", `Packages ${localPackageBase} have been removed`);
|
||||
onClose();
|
||||
} catch (exception) {
|
||||
showError("Action failed", `Could not remove package: ${ApiError.errorDetail(exception)}`);
|
||||
@@ -121,12 +127,12 @@ export default function PackageInfoDialog({
|
||||
};
|
||||
|
||||
const handleDeletePatch: (key: string) => Promise<void> = async key => {
|
||||
if (!packageBase) {
|
||||
if (!localPackageBase) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await client.servicePackagePatchRemove(packageBase, key);
|
||||
void queryClient.invalidateQueries({ queryKey: QueryKeys.patches(packageBase) });
|
||||
await client.service.servicePackagePatchRemove(localPackageBase, key);
|
||||
void queryClient.invalidateQueries({ queryKey: QueryKeys.patches(localPackageBase) });
|
||||
} catch (exception) {
|
||||
showError("Action failed", `Could not delete variable: ${ApiError.errorDetail(exception)}`);
|
||||
}
|
||||
@@ -136,7 +142,7 @@ export default function PackageInfoDialog({
|
||||
<DialogHeader onClose={handleClose} sx={headerStyle}>
|
||||
{pkg && status
|
||||
? `${pkg.base} ${status.status} at ${new Date(status.timestamp * 1000).toISOStringShort()}`
|
||||
: packageBase ?? ""}
|
||||
: localPackageBase ?? ""}
|
||||
</DialogHeader>
|
||||
|
||||
<DialogContent>
|
||||
@@ -157,18 +163,18 @@ export default function PackageInfoDialog({
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{tabIndex === 0 && packageBase && current &&
|
||||
{tabIndex === 0 && localPackageBase && current &&
|
||||
<BuildLogsTab
|
||||
packageBase={packageBase}
|
||||
packageBase={localPackageBase}
|
||||
repository={current}
|
||||
refreshInterval={autoRefresh.interval}
|
||||
/>
|
||||
}
|
||||
{tabIndex === 1 && packageBase && current &&
|
||||
<ChangesTab packageBase={packageBase} repository={current} />
|
||||
{tabIndex === 1 && localPackageBase && current &&
|
||||
<ChangesTab packageBase={localPackageBase} repository={current} />
|
||||
}
|
||||
{tabIndex === 2 && packageBase && current &&
|
||||
<EventsTab packageBase={packageBase} repository={current} />
|
||||
{tabIndex === 2 && localPackageBase && current &&
|
||||
<EventsTab packageBase={localPackageBase} repository={current} />
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function PackageRebuildDialog({ open, onClose }: PackageRebuildDi
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await client.serviceRebuild(repository, [dependency]);
|
||||
await client.service.serviceRebuild(repository, [dependency]);
|
||||
handleClose();
|
||||
showSuccess("Success", `Repository rebuild has been run for packages which depend on ${dependency}`);
|
||||
} catch (exception) {
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function AppLayout(): React.JSX.Element {
|
||||
|
||||
const { data: info } = useQuery<InfoResponse>({
|
||||
queryKey: QueryKeys.info,
|
||||
queryFn: () => client.fetchServerInfo(),
|
||||
queryFn: () => client.fetch.fetchServerInfo(),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function BuildLogsTab({
|
||||
|
||||
const { data: allLogs } = useQuery<LogRecord[]>({
|
||||
queryKey: QueryKeys.logs(packageBase, repository),
|
||||
queryFn: () => client.fetchPackageLogs(packageBase, repository),
|
||||
queryFn: () => client.fetch.fetchPackageLogs(packageBase, repository),
|
||||
enabled: !!packageBase,
|
||||
refetchInterval: refreshInterval > 0 ? refreshInterval : false,
|
||||
});
|
||||
@@ -112,7 +112,9 @@ export default function BuildLogsTab({
|
||||
const { data: versionLogs } = useQuery<LogRecord[]>({
|
||||
queryKey: QueryKeys.logsVersion(packageBase, repository, activeVersion?.version ?? "", activeVersion?.processId ?? ""),
|
||||
queryFn: activeVersion
|
||||
? () => client.fetchPackageLogs(packageBase, repository, activeVersion.version, activeVersion.processId)
|
||||
? () => client.fetch.fetchPackageLogs(
|
||||
packageBase, repository, activeVersion.version, activeVersion.processId,
|
||||
)
|
||||
: skipToken,
|
||||
placeholderData: keepPreviousData,
|
||||
refetchInterval: refreshInterval > 0 ? refreshInterval : false,
|
||||
|
||||
@@ -17,20 +17,19 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import "highlight.js/styles/github.css";
|
||||
|
||||
import { Box } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import CodeBlock from "components/common/CodeBlock";
|
||||
import hljs from "highlight.js/lib/core";
|
||||
import diff from "highlight.js/lib/languages/diff";
|
||||
import CopyButton from "components/common/CopyButton";
|
||||
import { QueryKeys } from "hooks/QueryKeys";
|
||||
import { useClient } from "hooks/useClient";
|
||||
import type { Changes } from "models/Changes";
|
||||
import type { RepositoryId } from "models/RepositoryId";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import React from "react";
|
||||
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import diff from "react-syntax-highlighter/dist/esm/languages/hljs/diff";
|
||||
import { githubGist } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
|
||||
hljs.registerLanguage("diff", diff);
|
||||
SyntaxHighlighter.registerLanguage("diff", diff);
|
||||
|
||||
interface ChangesTabProps {
|
||||
packageBase: string;
|
||||
@@ -39,23 +38,33 @@ interface ChangesTabProps {
|
||||
|
||||
export default function ChangesTab({ packageBase, repository }: ChangesTabProps): React.JSX.Element {
|
||||
const client = useClient();
|
||||
const codeRef = useRef<HTMLElement>(null);
|
||||
|
||||
const { data } = useQuery<Changes>({
|
||||
queryKey: QueryKeys.changes(packageBase, repository),
|
||||
queryFn: () => client.fetchPackageChanges(packageBase, repository),
|
||||
queryFn: () => client.fetch.fetchPackageChanges(packageBase, repository),
|
||||
enabled: !!packageBase,
|
||||
});
|
||||
|
||||
const changesText = data?.changes ?? "";
|
||||
|
||||
useEffect(() => {
|
||||
if (codeRef.current) {
|
||||
codeRef.current.innerHTML = hljs.highlight(changesText, { language: "diff" }).value;
|
||||
}
|
||||
}, [changesText]);
|
||||
|
||||
return <Box sx={{ mt: 1 }}>
|
||||
<CodeBlock codeRef={codeRef} className="language-diff" getText={() => changesText} height={400} />
|
||||
return <Box sx={{ position: "relative", mt: 1 }}>
|
||||
<SyntaxHighlighter
|
||||
language="diff"
|
||||
style={githubGist}
|
||||
customStyle={{
|
||||
padding: "16px",
|
||||
borderRadius: "4px",
|
||||
overflow: "auto",
|
||||
height: 400,
|
||||
fontSize: "0.8rem",
|
||||
fontFamily: "monospace",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{changesText}
|
||||
</SyntaxHighlighter>
|
||||
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
||||
<CopyButton getText={() => changesText} />
|
||||
</Box>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useClient } from "hooks/useClient";
|
||||
import type { Event } from "models/Event";
|
||||
import type { RepositoryId } from "models/RepositoryId";
|
||||
import type React from "react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface EventsTabProps {
|
||||
packageBase: string;
|
||||
@@ -50,16 +51,16 @@ export default function EventsTab({ packageBase, repository }: EventsTabProps):
|
||||
|
||||
const { data: events = [] } = useQuery<Event[]>({
|
||||
queryKey: QueryKeys.events(repository, packageBase),
|
||||
queryFn: () => client.fetchPackageEvents(repository, packageBase, 30),
|
||||
queryFn: () => client.fetch.fetchPackageEvents(repository, packageBase, 30),
|
||||
enabled: !!packageBase,
|
||||
});
|
||||
|
||||
const rows: EventRow[] = events.map((event, index) => ({
|
||||
const rows = useMemo<EventRow[]>(() => events.map((event, index) => ({
|
||||
id: index,
|
||||
timestamp: new Date(event.created * 1000).toISOStringShort(),
|
||||
event: event.event,
|
||||
message: event.message ?? "",
|
||||
}));
|
||||
})), [events]);
|
||||
|
||||
return <Box sx={{ mt: 1 }}>
|
||||
<EventDurationLineChart events={events} />
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Grid, Link, Typography } from "@mui/material";
|
||||
import StringList from "components/common/StringList";
|
||||
import type { Dependencies } from "models/Dependencies";
|
||||
import type { Package } from "models/Package";
|
||||
import React from "react";
|
||||
@@ -67,7 +66,7 @@ export default function PackageDetailsGrid({ pkg, dependencies }: PackageDetails
|
||||
return <>
|
||||
<Grid container spacing={1} sx={{ mt: 1 }}>
|
||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">packages</Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}><StringList items={packagesList.unique()} /></Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{packagesList.unique().join("\n")}</Typography></Grid>
|
||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">version</Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2">{pkg.version}</Typography></Grid>
|
||||
</Grid>
|
||||
@@ -81,16 +80,16 @@ export default function PackageDetailsGrid({ pkg, dependencies }: PackageDetails
|
||||
|
||||
<Grid container spacing={1} sx={{ mt: 0.5 }}>
|
||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">groups</Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}><StringList items={groups.unique()} /></Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{groups.unique().join("\n")}</Typography></Grid>
|
||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">licenses</Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}><StringList items={licenses.unique()} /></Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{licenses.unique().join("\n")}</Typography></Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={1} sx={{ mt: 0.5 }}>
|
||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">upstream</Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}>
|
||||
{upstreamUrls.map(url =>
|
||||
<Link key={url} href={url} target="_blank" rel="noopener" underline="hover" display="block" variant="body2">
|
||||
<Link key={url} href={url} target="_blank" rel="noopener noreferrer" underline="hover" display="block" variant="body2">
|
||||
{url}
|
||||
</Link>,
|
||||
)}
|
||||
@@ -99,7 +98,7 @@ export default function PackageDetailsGrid({ pkg, dependencies }: PackageDetails
|
||||
<Grid size={{ xs: 8, md: 5 }}>
|
||||
<Typography variant="body2">
|
||||
{aurUrl &&
|
||||
<Link href={aurUrl} target="_blank" rel="noopener" underline="hover">AUR link</Link>
|
||||
<Link href={aurUrl} target="_blank" rel="noopener noreferrer" underline="hover">AUR link</Link>
|
||||
}
|
||||
</Typography>
|
||||
</Grid>
|
||||
@@ -107,9 +106,9 @@ export default function PackageDetailsGrid({ pkg, dependencies }: PackageDetails
|
||||
|
||||
<Grid container spacing={1} sx={{ mt: 0.5 }}>
|
||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">depends</Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}><StringList items={allDepends} /></Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{allDepends.join("\n")}</Typography></Grid>
|
||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">implicitly depends</Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}><StringList items={implicitDepends.unique()} /></Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{implicitDepends.unique().join("\n")}</Typography></Grid>
|
||||
</Grid>
|
||||
</>;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
type GridRowId,
|
||||
useGridApiRef,
|
||||
} from "@mui/x-data-grid";
|
||||
import StringList from "components/common/StringList";
|
||||
import DashboardDialog from "components/dialogs/DashboardDialog";
|
||||
import KeyImportDialog from "components/dialogs/KeyImportDialog";
|
||||
import PackageAddDialog from "components/dialogs/PackageAddDialog";
|
||||
@@ -58,7 +57,7 @@ function createListColumn(
|
||||
...options,
|
||||
valueGetter: (value: string[]) => (value ?? []).join(" "),
|
||||
renderCell: (params: GridRenderCellParams<PackageRow>) =>
|
||||
<Box sx={{ whiteSpace: "pre-line" }}><StringList items={(params.row[field] as string[]) ?? []} /></Box>,
|
||||
<Box sx={{ whiteSpace: "pre-line" }}>{((params.row[field] as string[]) ?? []).join("\n")}</Box>,
|
||||
sortComparator: (left: string, right: string) => left.localeCompare(right),
|
||||
};
|
||||
}
|
||||
@@ -85,7 +84,7 @@ export default function PackageTable({ autoRefreshIntervals }: PackageTableProps
|
||||
minWidth: 150,
|
||||
renderCell: (params: GridRenderCellParams<PackageRow>) =>
|
||||
params.row.webUrl ?
|
||||
<Link href={params.row.webUrl} target="_blank" rel="noopener" underline="hover">
|
||||
<Link href={params.row.webUrl} target="_blank" rel="noopener noreferrer" underline="hover">
|
||||
{params.value as string}
|
||||
</Link>
|
||||
: params.value as string,
|
||||
@@ -114,7 +113,7 @@ export default function PackageTable({ autoRefreshIntervals }: PackageTableProps
|
||||
[],
|
||||
);
|
||||
|
||||
return <Box>
|
||||
return <Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||
<PackageTableToolbar
|
||||
hasSelection={table.selectionModel.length > 0}
|
||||
isAuthorized={table.isAuthorized}
|
||||
@@ -130,7 +129,7 @@ export default function PackageTable({ autoRefreshIntervals }: PackageTableProps
|
||||
onDashboardClick: () => table.setDialogOpen("dashboard"),
|
||||
onAddClick: () => table.setDialogOpen("add"),
|
||||
onUpdateClick: () => void table.handleUpdate(),
|
||||
onRefreshDbClick: () => void table.handleRefreshDb(),
|
||||
onRefreshDatabaseClick: () => void table.handleRefreshDatabase(),
|
||||
onRebuildClick: () => table.setDialogOpen("rebuild"),
|
||||
onRemoveClick: () => void table.handleRemove(),
|
||||
onKeyImportClick: () => table.setDialogOpen("keyImport"),
|
||||
@@ -177,7 +176,7 @@ export default function PackageTable({ autoRefreshIntervals }: PackageTableProps
|
||||
table.setSelectedPackage(String(params.id));
|
||||
}}
|
||||
sx={{
|
||||
height: 500,
|
||||
flex: 1,
|
||||
"& .MuiDataGrid-row": { cursor: "pointer" },
|
||||
}}
|
||||
density="compact"
|
||||
|
||||
@@ -46,7 +46,7 @@ export interface ToolbarActions {
|
||||
onDashboardClick: () => void;
|
||||
onAddClick: () => void;
|
||||
onUpdateClick: () => void;
|
||||
onRefreshDbClick: () => void;
|
||||
onRefreshDatabaseClick: () => void;
|
||||
onRebuildClick: () => void;
|
||||
onRemoveClick: () => void;
|
||||
onKeyImportClick: () => void;
|
||||
@@ -116,7 +116,7 @@ export default function PackageTableToolbar({
|
||||
<PlayArrowIcon fontSize="small" sx={{ mr: 1 }} /> update
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => {
|
||||
setPackagesAnchorEl(null); actions.onRefreshDbClick();
|
||||
setPackagesAnchorEl(null); actions.onRefreshDatabaseClick();
|
||||
}}>
|
||||
<DownloadIcon fontSize="small" sx={{ mr: 1 }} /> update pacman databases
|
||||
</MenuItem>
|
||||
|
||||
@@ -17,33 +17,41 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { ApiError } from "api/client/ApiError";
|
||||
import { AuthContext } from "contexts/AuthContext";
|
||||
import { useClient } from "hooks/useClient";
|
||||
import { useNotification } from "hooks/useNotification";
|
||||
import React, { type ReactNode, useCallback, useMemo, useState } from "react";
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }): React.JSX.Element {
|
||||
const client = useClient();
|
||||
const [state, setState] = useState({ enabled: true, username: null as string | null });
|
||||
const [authState, setAuthState] = useState({ enabled: true, username: null as string | null });
|
||||
const { showError } = useNotification();
|
||||
|
||||
const login = useCallback(async (username: string, password: string) => {
|
||||
await client.login({ username, password });
|
||||
setState(prev => ({ ...prev, username }));
|
||||
setAuthState(prev => ({ ...prev, username }));
|
||||
}, [client]);
|
||||
|
||||
const doLogout = useCallback(async () => {
|
||||
await client.logout();
|
||||
setState(prev => ({ ...prev, username: null }));
|
||||
}, [client]);
|
||||
const logout = useCallback(async () => {
|
||||
try {
|
||||
await client.logout();
|
||||
setAuthState(prev => ({ ...prev, username: null }));
|
||||
} catch (exception) {
|
||||
const detail = ApiError.errorDetail(exception);
|
||||
showError("Login error", `Could not log out: ${detail}`);
|
||||
}
|
||||
}, [client, showError]);
|
||||
|
||||
const isAuthorized = useMemo(() => !state.enabled || state.username !== null, [state.enabled, state.username]);
|
||||
const isAuthorized = useMemo(() =>
|
||||
!authState.enabled || authState.username !== null, [authState.enabled, authState.username],
|
||||
);
|
||||
|
||||
const value = useMemo(() => ({
|
||||
...state, isAuthorized, setAuthState: setState, login, logout: doLogout,
|
||||
}), [state, isAuthorized, login, doLogout]);
|
||||
...authState, isAuthorized, setAuthState, login, logout,
|
||||
}), [authState, isAuthorized, login, logout]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
return <AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
@@ -18,22 +18,26 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { type AlertColor, Box } from "@mui/material";
|
||||
import type { Notification } from "contexts/Notification";
|
||||
import NotificationItem from "components/common/NotificationItem";
|
||||
import { NotificationContext } from "contexts/NotificationContext";
|
||||
import NotificationItem from "contexts/NotificationItem";
|
||||
import React, { type ReactNode, useCallback, useMemo, useRef, useState } from "react";
|
||||
import type { Notification } from "models/Notification";
|
||||
import React, { type ReactNode, useCallback, useMemo, useState } from "react";
|
||||
|
||||
export function NotificationProvider({ children }: { children: ReactNode }): React.JSX.Element {
|
||||
const nextId = useRef(0);
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
|
||||
const addNotification = useCallback((title: string, message: string, severity: AlertColor) => {
|
||||
const id = nextId.current++;
|
||||
setNotifications(prev => [...prev, { id, title, message, severity }]);
|
||||
const id = `${severity}:${title}:${message}`;
|
||||
setNotifications(prev => {
|
||||
if (prev.some(notification => notification.id === id)) {
|
||||
return prev;
|
||||
}
|
||||
return [...prev, { id, title, message, severity }];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const removeNotification = useCallback((id: number) => {
|
||||
setNotifications(prev => prev.filter(notification => notification.id !== id));
|
||||
const removeNotification = useCallback((key: string) => {
|
||||
setNotifications(prev => prev.filter(notification => notification.id !== key));
|
||||
}, []);
|
||||
|
||||
const showSuccess = useCallback(
|
||||
@@ -47,28 +51,26 @@ export function NotificationProvider({ children }: { children: ReactNode }): Rea
|
||||
|
||||
const value = useMemo(() => ({ showSuccess, showError }), [showSuccess, showError]);
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider value={value}>
|
||||
{children}
|
||||
<Box
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: 16,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: theme => theme.zIndex.snackbar,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1,
|
||||
maxWidth: 500,
|
||||
width: "100%",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
{notifications.map(notification =>
|
||||
<NotificationItem key={notification.id} notification={notification} onClose={removeNotification} />,
|
||||
)}
|
||||
</Box>
|
||||
</NotificationContext.Provider>
|
||||
);
|
||||
return <NotificationContext.Provider value={value}>
|
||||
{children}
|
||||
<Box
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: 16,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: theme => theme.zIndex.snackbar,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1,
|
||||
maxWidth: 500,
|
||||
width: "100%",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
{notifications.map(notification =>
|
||||
<NotificationItem key={notification.id} notification={notification} onClose={removeNotification} />,
|
||||
)}
|
||||
</Box>
|
||||
</NotificationContext.Provider>;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { type RefObject, useRef } from "react";
|
||||
import { type RefObject, useCallback, useRef } from "react";
|
||||
|
||||
interface UseAutoScrollResult {
|
||||
preRef: RefObject<HTMLElement | null>;
|
||||
@@ -31,19 +31,19 @@ export function useAutoScroll(): UseAutoScrollResult {
|
||||
const initialScrollDone = useRef(false);
|
||||
const wasAtBottom = useRef(true);
|
||||
|
||||
const handleScroll: () => void = () => {
|
||||
const handleScroll = useCallback((): void => {
|
||||
if (preRef.current) {
|
||||
const element = preRef.current;
|
||||
wasAtBottom.current = element.scrollTop + element.clientHeight >= element.scrollHeight - 50;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const resetScroll: () => void = () => {
|
||||
const resetScroll = useCallback((): void => {
|
||||
initialScrollDone.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// scroll to bottom on initial load, then only if already near bottom and no active text selection
|
||||
const scrollToBottom: () => void = () => {
|
||||
const scrollToBottom = useCallback((): void => {
|
||||
if (!preRef.current) {
|
||||
return;
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export function useAutoScroll(): UseAutoScrollResult {
|
||||
element.scrollTop = element.scrollHeight;
|
||||
}
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { preRef, handleScroll, scrollToBottom, resetScroll };
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import { type Context, useContext } from "react";
|
||||
|
||||
export function useContextNotNull<T>(context: Context<T | null>): T {
|
||||
const ctx = useContext(context);
|
||||
if (!ctx) {
|
||||
if (ctx === null) {
|
||||
throw new Error("must be used within a Provider");
|
||||
}
|
||||
return ctx;
|
||||
|
||||
@@ -23,11 +23,12 @@ import { QueryKeys } from "hooks/QueryKeys";
|
||||
import { useClient } from "hooks/useClient";
|
||||
import { useNotification } from "hooks/useNotification";
|
||||
import { useRepository } from "hooks/useRepository";
|
||||
import type { RepositoryId } from "models/RepositoryId";
|
||||
|
||||
export interface UsePackageActionsResult {
|
||||
handleReload: () => void;
|
||||
handleUpdate: () => Promise<void>;
|
||||
handleRefreshDb: () => Promise<void>;
|
||||
handleRefreshDatabase: () => Promise<void>;
|
||||
handleRemove: () => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -40,80 +41,68 @@ export function usePackageActions(
|
||||
const { showSuccess, showError } = useNotification();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const invalidate = (repository: RepositoryId): void => {
|
||||
void queryClient.invalidateQueries({ queryKey: QueryKeys.packages(repository) });
|
||||
void queryClient.invalidateQueries({ queryKey: QueryKeys.status(repository) });
|
||||
};
|
||||
|
||||
const performAction = async (
|
||||
action: (repository: RepositoryId) => Promise<string>,
|
||||
errorMessage: string,
|
||||
): Promise<void> => {
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const successMessage = await action(current);
|
||||
showSuccess("Success", successMessage);
|
||||
invalidate(current);
|
||||
setSelectionModel([]);
|
||||
} catch (exception) {
|
||||
showError("Action failed", `${errorMessage}: ${ApiError.errorDetail(exception)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReload: () => void = () => {
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
void queryClient.invalidateQueries({ queryKey: QueryKeys.packages(current) });
|
||||
void queryClient.invalidateQueries({ queryKey: QueryKeys.status(current) });
|
||||
};
|
||||
|
||||
const handleUpdate: () => Promise<void> = async () => {
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (selectionModel.length === 0) {
|
||||
await client.servicePackageUpdate(current, { packages: [] });
|
||||
showSuccess("Success", "Repository update has been run");
|
||||
} else {
|
||||
await client.servicePackageAdd(current, { packages: selectionModel });
|
||||
showSuccess("Success", `Run update for packages ${selectionModel.join(", ")}`);
|
||||
}
|
||||
setSelectionModel([]);
|
||||
void queryClient.invalidateQueries({ queryKey: QueryKeys.packages(current) });
|
||||
void queryClient.invalidateQueries({ queryKey: QueryKeys.status(current) });
|
||||
} catch (exception) {
|
||||
const detail = ApiError.errorDetail(exception);
|
||||
showError("Action failed", `Packages update failed: ${detail}`);
|
||||
if (current !== null) {
|
||||
invalidate(current);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefreshDb: () => Promise<void> = async () => {
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await client.servicePackageUpdate(current, {
|
||||
packages: [],
|
||||
refresh: true,
|
||||
aur: false,
|
||||
local: false,
|
||||
manual: false,
|
||||
});
|
||||
showSuccess("Success", "Pacman database update has been requested");
|
||||
setSelectionModel([]);
|
||||
void queryClient.invalidateQueries({ queryKey: QueryKeys.packages(current) });
|
||||
void queryClient.invalidateQueries({ queryKey: QueryKeys.status(current) });
|
||||
} catch (exception) {
|
||||
const detail = ApiError.errorDetail(exception);
|
||||
showError("Action failed", `Could not update pacman databases: ${detail}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove: () => Promise<void> = async () => {
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
const handleUpdate = (): Promise<void> => performAction(async (repository): Promise<string> => {
|
||||
if (selectionModel.length === 0) {
|
||||
return;
|
||||
await client.service.servicePackageUpdate(repository, { packages: [] });
|
||||
return "Repository update has been run";
|
||||
}
|
||||
try {
|
||||
await client.servicePackageRemove(current, selectionModel);
|
||||
showSuccess("Success", `Packages ${selectionModel.join(", ")} have been removed`);
|
||||
setSelectionModel([]);
|
||||
void queryClient.invalidateQueries({ queryKey: QueryKeys.packages(current) });
|
||||
void queryClient.invalidateQueries({ queryKey: QueryKeys.status(current) });
|
||||
} catch (exception) {
|
||||
const detail = ApiError.errorDetail(exception);
|
||||
showError("Action failed", `Could not remove packages: ${detail}`);
|
||||
await client.service.servicePackageAdd(repository, { packages: selectionModel });
|
||||
return `Run update for packages ${selectionModel.join(", ")}`;
|
||||
}, "Packages update failed");
|
||||
|
||||
const handleRefreshDatabase = (): Promise<void> => performAction(async (repository): Promise<string> => {
|
||||
await client.service.servicePackageUpdate(repository, {
|
||||
packages: [],
|
||||
refresh: true,
|
||||
aur: false,
|
||||
local: false,
|
||||
manual: false,
|
||||
});
|
||||
return "Pacman database update has been requested";
|
||||
}, "Could not update pacman databases");
|
||||
|
||||
const handleRemove = (): Promise<void> => {
|
||||
if (selectionModel.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return performAction(async (repository): Promise<string> => {
|
||||
await client.service.servicePackageRemove(repository, selectionModel);
|
||||
return `Packages ${selectionModel.join(", ")} have been removed`;
|
||||
}, "Could not remove packages");
|
||||
};
|
||||
|
||||
return {
|
||||
handleReload,
|
||||
handleUpdate,
|
||||
handleRefreshDb,
|
||||
handleRefreshDatabase,
|
||||
handleRemove,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,13 +46,13 @@ export function usePackageData(autoRefreshIntervals: AutoRefreshInterval[]): Use
|
||||
|
||||
const { data: packages = [], isLoading } = useQuery({
|
||||
queryKey: current ? QueryKeys.packages(current) : ["packages"],
|
||||
queryFn: current ? () => client.fetchPackages(current) : skipToken,
|
||||
queryFn: current ? () => client.fetch.fetchPackages(current) : skipToken,
|
||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||
});
|
||||
|
||||
const { data: status } = useQuery({
|
||||
queryKey: current ? QueryKeys.status(current) : ["status"],
|
||||
queryFn: current ? () => client.fetchServerStatus(current) : skipToken,
|
||||
queryFn: current ? () => client.fetch.fetchServerStatus(current) : skipToken,
|
||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||
});
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export interface UsePackageTableResult {
|
||||
|
||||
handleReload: () => void;
|
||||
handleUpdate: () => Promise<void>;
|
||||
handleRefreshDb: () => Promise<void>;
|
||||
handleRefreshDatabase: () => Promise<void>;
|
||||
handleRemove: () => Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import type { AlertColor } from "@mui/material";
|
||||
|
||||
export interface Notification {
|
||||
id: number;
|
||||
id: string;
|
||||
title: string;
|
||||
message: string;
|
||||
severity: AlertColor;
|
||||
@@ -19,21 +19,6 @@
|
||||
*/
|
||||
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
||||
|
||||
// https://www.typescriptlang.org/docs/handbook/mixins.html#alternative-pattern
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument */
|
||||
export function applyMixins(clazz: any, classes: any[]): void {
|
||||
classes.forEach(baseClass => {
|
||||
Object.getOwnPropertyNames(baseClass.prototype).forEach(name => {
|
||||
Object.defineProperty(
|
||||
clazz.prototype,
|
||||
name,
|
||||
Object.getOwnPropertyDescriptor(baseClass.prototype, name) || Object.create(null),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument */
|
||||
|
||||
export function defaultInterval(intervals: AutoRefreshInterval[]): number {
|
||||
return intervals.find(interval => interval.is_active)?.interval ?? 0;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
pkgbase='ahriman'
|
||||
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
|
||||
pkgver=2.20.0rc4
|
||||
pkgver=2.20.0rc5
|
||||
pkgrel=1
|
||||
pkgdesc="ArcH linux ReposItory MANager"
|
||||
arch=('any')
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
<link rel="icon" href="/static/favicon.ico" />
|
||||
<script type="module" crossorigin src="/static/index.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/static/index.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.TH AHRIMAN "1" "2026\-02\-21" "ahriman 2.20.0rc4" "ArcH linux ReposItory MANager"
|
||||
.TH AHRIMAN "1" "2026\-03\-06" "ahriman 2.20.0rc5" "ArcH linux ReposItory MANager"
|
||||
.SH NAME
|
||||
ahriman \- ArcH linux ReposItory MANager
|
||||
.SH SYNOPSIS
|
||||
|
||||
@@ -245,7 +245,7 @@ _shtab_ahriman_init_options=(
|
||||
{--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:"
|
||||
"--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:"
|
||||
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
|
||||
"--packager[packager name and email (default\: None)]:packager:"
|
||||
"--packager[packager name and email]:packager:"
|
||||
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
|
||||
"--sign-key[sign key id (default\: None)]:sign_key:"
|
||||
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
|
||||
@@ -526,7 +526,7 @@ _shtab_ahriman_repo_init_options=(
|
||||
{--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:"
|
||||
"--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:"
|
||||
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
|
||||
"--packager[packager name and email (default\: None)]:packager:"
|
||||
"--packager[packager name and email]:packager:"
|
||||
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
|
||||
"--sign-key[sign key id (default\: None)]:sign_key:"
|
||||
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
|
||||
@@ -583,7 +583,7 @@ _shtab_ahriman_repo_setup_options=(
|
||||
{--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:"
|
||||
"--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:"
|
||||
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
|
||||
"--packager[packager name and email (default\: None)]:packager:"
|
||||
"--packager[packager name and email]:packager:"
|
||||
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
|
||||
"--sign-key[sign key id (default\: None)]:sign_key:"
|
||||
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
|
||||
@@ -757,7 +757,7 @@ _shtab_ahriman_service_setup_options=(
|
||||
{--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:"
|
||||
"--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:"
|
||||
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
|
||||
"--packager[packager name and email (default\: None)]:packager:"
|
||||
"--packager[packager name and email]:packager:"
|
||||
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
|
||||
"--sign-key[sign key id (default\: None)]:sign_key:"
|
||||
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
|
||||
@@ -792,7 +792,7 @@ _shtab_ahriman_setup_options=(
|
||||
{--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:"
|
||||
"--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:"
|
||||
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
|
||||
"--packager[packager name and email (default\: None)]:packager:"
|
||||
"--packager[packager name and email]:packager:"
|
||||
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
|
||||
"--sign-key[sign key id (default\: None)]:sign_key:"
|
||||
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
|
||||
|
||||
@@ -17,4 +17,4 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "2.20.0rc4"
|
||||
__version__ = "2.20.0rc5"
|
||||
|
||||
@@ -45,10 +45,10 @@ SUBPACKAGES = {
|
||||
prefix / "lib" / "systemd" / "system" / "ahriman-web.service",
|
||||
prefix / "lib" / "systemd" / "system" / "ahriman-web@.service",
|
||||
prefix / "share" / "ahriman" / "settings" / "ahriman.ini.d" / "00-web.ini",
|
||||
prefix / "share" / "ahriman" / "templates" / "build-status",
|
||||
prefix / "share" / "ahriman" / "templates" / "build-status.jinja2",
|
||||
prefix / "share" / "ahriman" / "templates" / "build-status-legacy.jinja2",
|
||||
prefix / "share" / "ahriman" / "templates" / "api.jinja2",
|
||||
prefix / "share" / "ahriman" / "templates" / "build-status",
|
||||
prefix / "share" / "ahriman" / "templates" / "build-status-classic.jinja2",
|
||||
prefix / "share" / "ahriman" / "templates" / "build-status.jinja2",
|
||||
prefix / "share" / "ahriman" / "templates" / "error.jinja2",
|
||||
prefix / "share" / "ahriman" / "templates" / "static",
|
||||
site_packages / "ahriman" / "application" / "handlers" / "web.py",
|
||||
|
||||
@@ -291,6 +291,28 @@ def test_filter_json_empty_value(package_ahriman: Package) -> None:
|
||||
assert "base" not in filter_json(probe, probe.keys())
|
||||
|
||||
|
||||
def test_filter_json_nested() -> None:
|
||||
"""
|
||||
must recursively filter None values from nested dicts
|
||||
"""
|
||||
assert filter_json({"a": {"b": None, "c": 1}}) == {"a": {"c": 1}}
|
||||
|
||||
|
||||
def test_filter_json_list() -> None:
|
||||
"""
|
||||
must recursively filter None values inside lists
|
||||
"""
|
||||
assert filter_json([{"a": 1, "b": None}]) == [{"a": 1}]
|
||||
|
||||
|
||||
def test_filter_json_scalar() -> None:
|
||||
"""
|
||||
must pass through scalar values unchanged
|
||||
"""
|
||||
assert filter_json("scalar") == "scalar"
|
||||
assert filter_json(42) == 42
|
||||
|
||||
|
||||
def test_full_version() -> None:
|
||||
"""
|
||||
must construct full version
|
||||
@@ -583,7 +605,7 @@ def test_walk(resource_path_root: Path) -> None:
|
||||
resource_path_root / "web" / "templates" / "utils" / "style.jinja2",
|
||||
resource_path_root / "web" / "templates" / "api.jinja2",
|
||||
resource_path_root / "web" / "templates" / "build-status.jinja2",
|
||||
resource_path_root / "web" / "templates" / "build-status-legacy.jinja2",
|
||||
resource_path_root / "web" / "templates" / "build-status-classic.jinja2",
|
||||
resource_path_root / "web" / "templates" / "email-index.jinja2",
|
||||
resource_path_root / "web" / "templates" / "error.jinja2",
|
||||
resource_path_root / "web" / "templates" / "repo-index.jinja2",
|
||||
|
||||
Reference in New Issue
Block a user