Compare commits

...
6 Commits
25 changed files with 165 additions and 124 deletions

No files matched your search

@@ -1,4 +1,9 @@
# AUTOMATICALLY GENERATED by https://github.com/tqdm/shtab
# Usage:
# 1) Copy this to somewhere (e.g. ~/.local/share/bash_completion/ahriman).
# 2) Add the following line to your .bashrc:
# source ~/.local/share/bash_completion/ahriman
# See also: https://github.com/scop/bash-completion/blob/main/doc/configuration.md
_shtab_ahriman_subparsers=(add aur-search check clean config config-validate copy daemon help help-commands-unsafe help-updates help-version init key-import package-add package-archives package-changes package-changes-remove package-copy package-hold package-pkgbuild package-pkgbuild-remove package-remove package-rollback package-status package-status-remove package-status-update package-unhold package-update patch-add patch-list patch-remove patch-set-add rebuild remove remove-unknown repo-backup repo-check repo-clean repo-config repo-config-validate repo-create-keyring repo-create-mirrorlist repo-daemon repo-init repo-rebuild repo-remove-unknown repo-report repo-restore repo-setup repo-sign repo-statistics repo-status-update repo-sync repo-tree repo-triggers repo-update report run search service-clean service-config service-config-validate service-key-import service-repositories service-run service-setup service-shell service-tree-migrate setup shell sign status status-update sync update user-add user-list user-remove version web web-reload)
@@ -626,7 +631,7 @@ _set_parser_defaults() {
completed_positional_actions=0
_set_new_action "pos_${completed_positional_actions}" true
_set_new_action "pos_$completed_positional_actions" true
}
# $1=action identifier
@@ -686,13 +691,13 @@ _shtab_ahriman() {
local this_word="${COMP_WORDS[$word_index]}"
if [[ $pos_only = 1 || " $this_word " != " -- " ]]; then
if [[ -n $sub_parsers && " ${sub_parsers[@]} " == *" ${this_word} "* ]]; then
if [[ -n $sub_parsers && " ${sub_parsers[@]} " == *" $this_word "* ]]; then
# valid subcommand: add it to the prefix & reset the current action
prefix="${prefix}_$(_shtab_replace_nonword $this_word)"
_set_parser_defaults
fi
if [[ " ${current_option_strings[@]} " == *" ${this_word} "* ]]; then
if [[ " ${current_option_strings[@]} " == *" $this_word "* ]]; then
# a new action should be acquired (due to recognised option string or
# no more input expected from current action);
# the next positional action can fill in here
@@ -706,7 +711,7 @@ _shtab_ahriman() {
(( $word_index + 1 - $current_action_args_start_index - $pos_only >= \
$current_action_nargs )); then
$current_action_is_positional && let "completed_positional_actions += 1"
_set_new_action "pos_${completed_positional_actions}" true
_set_new_action "pos_$completed_positional_actions" true
fi
else
pos_only=1 # "--" delimiter encountered
@@ -718,23 +723,31 @@ _shtab_ahriman() {
# Generate the completions
COMPREPLY=()
if [[ $pos_only = 0 && "${completing_word}" == -* ]]; then
if [[ $pos_only = 0 && "$completing_word" == -* &&
( -z "$current_action_compgen" || "$current_action_is_positional" = true ) ]]; then
# optional argument started: use option strings
while IFS= read -r line; do COMPREPLY+=("$line"); done < <(
compgen -W "${current_option_strings[*]}" -- "${completing_word}")
elif [[ "${previous_word}" =~ ^[0-9\&]*[\<\>]\>?$ ]]; then
compgen -W "${current_option_strings[*]}" -- "$completing_word")
elif [[ "$previous_word" =~ ^[0-9\&]*[\<\>]\>?$ ]]; then
# handle redirection operators
while IFS= read -r line; do COMPREPLY+=("$line"); done < <(compgen -f -- "${completing_word}")
compopt -o filenames 2>/dev/null || : # bash>=4
while IFS= read -r line; do COMPREPLY+=("$line"); done < <(compgen -f -- "$completing_word")
else
# use choices & compgen
[ -n "${current_action_compgen}" ] &&
local action_compgen_word="$completing_word"
# handle tab-completing in the middle of a line (#248 <- #116)
[[ -n "$current_action_compgen" && "$completing_word" == -* ]] && action_compgen_word=""
[ -n "$current_action_compgen" ] && {
[[ "$current_action_compgen" =~ _(file|dir|glob|FILE|DIR|GLOB)|File|Dir|Glob ]] &&
compopt -o filenames 2>/dev/null || : # bash>=4
while IFS= read -r line; do COMPREPLY+=("$line"); done < <(
"${current_action_compgen}" "${completing_word}")
"$current_action_compgen" "$action_compgen_word")
}
while IFS= read -r line; do COMPREPLY+=("$line"); done < <(
compgen -W "${current_action_choices[*]}" -- "${completing_word}")
compgen -W "${current_action_choices[*]}" -- "$completing_word")
fi
return 0
}
complete -o filenames -F _shtab_ahriman ahriman
complete -F _shtab_ahriman ahriman
@@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2026\-08\-18" "ahriman 2.22.1" "ArcH linux ReposItory MANager"
.TH AHRIMAN "1" "2026\-10\-13" "ahriman 2.22.2" "ArcH linux ReposItory MANager"
.SH NAME
ahriman \- ArcH linux ReposItory MANager
.SH SYNOPSIS
@@ -1,6 +1,11 @@
#compdef ahriman
# AUTOMATICALLY GENERATED by https://github.com/tqdm/shtab
# Usage:
# 1) Copy this to a file named _ahriman (e.g. ~/.local/share/zsh_completion/_ahriman).
# 2) Add the following line to your .zshrc:
# fpath=(~/.local/share/zsh_completion $fpath)
# See also: https://github.com/zsh-users/zsh-completions/blob/master/zsh-completions-howto.org
_shtab_ahriman_commands() {
+1 -1
View File
@@ -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.22.1"
__version__ = "2.22.2"
@@ -28,7 +28,6 @@ from ahriman import __version__
from ahriman.application.handlers.handler import Handler
from ahriman.application.help_formatter import _HelpFormatter
from ahriman.core.module_loader import implementations
from ahriman.core.utils import enum_values
from ahriman.models.log_handler import LogHandler
@@ -75,7 +74,7 @@ Start web service (requires additional configuration):
parser.add_argument("-l", "--lock", help="lock file", type=Path, default=Path("ahriman.pid"))
parser.add_argument("--log-handler", help="explicit log handler specification. If none set, the handler will be "
"guessed from environment",
type=LogHandler, choices=enum_values(LogHandler))
type=LogHandler, choices=tuple(LogHandler))
parser.add_argument("-q", "--quiet", help="force disable any logging", action="store_true")
parser.add_argument("--report", help="force enable or disable reporting to web service",
action=argparse.BooleanOptionalAction, default=True)
@@ -23,7 +23,7 @@ from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.application.handlers.update import Update
from ahriman.core.configuration import Configuration
from ahriman.core.utils import enum_values, extract_user
from ahriman.core.utils import extract_user
from ahriman.models.package_source import PackageSource
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId
@@ -85,7 +85,7 @@ class Add(Handler):
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
type=PackageSource, choices=tuple(PackageSource), default=PackageSource.Auto)
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build",
action="append")
@@ -22,7 +22,7 @@ import argparse
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.core.utils import enum_values, extract_user
from ahriman.core.utils import extract_user
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
@@ -88,7 +88,7 @@ class Rebuild(Handler):
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
action="store_true")
parser.add_argument("-s", "--status", help="filter packages by status. Requires --from-database to be set",
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum))
type=BuildStatusEnum, choices=tuple(BuildStatusEnum))
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
return parser
@@ -29,7 +29,6 @@ from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InitializeError, MissingArchitectureError
from ahriman.core.utils import enum_values
from ahriman.models.repository_id import RepositoryId
from ahriman.models.sign_settings import SignSettings
from ahriman.models.user import User
@@ -112,7 +111,7 @@ class Setup(Handler):
parser.add_argument("--server", help="server to be used for devtools. If none set, local files will be used")
parser.add_argument("--sign-key", help="sign key id")
parser.add_argument("--sign-target", help="sign options", action="append",
type=SignSettings.from_option, choices=enum_values(SignSettings))
type=SignSettings.from_option, choices=tuple(SignSettings))
parser.add_argument("--web-port", help="port of the web service", type=int)
parser.add_argument("--web-unix-socket", help="path to unix socket used for interprocess communications",
type=Path)
@@ -28,7 +28,7 @@ from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import EventStatsPrinter, PackageStatsPrinter, RepositoryStatsPrinter
from ahriman.core.utils import enum_values, pretty_datetime
from ahriman.core.utils import pretty_datetime
from ahriman.models.event import Event, EventType
from ahriman.models.repository_id import RepositoryId
@@ -85,7 +85,7 @@ class Statistics(Handler):
parser.add_argument("package", help="fetch only events for the specified package", nargs="?")
parser.add_argument("--chart", help="create updates chart and save it to the specified path", type=Path)
parser.add_argument("-e", "--event", help="event type filter",
type=EventType, choices=enum_values(EventType), default=EventType.PackageUpdated)
type=EventType, choices=tuple(EventType), default=EventType.PackageUpdated)
parser.add_argument("--from-date", help="only fetch events which are newer than the date")
parser.add_argument("--limit", help="limit response by specified amount of events", type=int, default=-1)
parser.add_argument("--offset", help="skip specified amount of events", type=int, default=0)
@@ -26,7 +26,6 @@ from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import PackagePrinter, StatusPrinter
from ahriman.core.types import Comparable
from ahriman.core.utils import enum_values
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.repository_id import RepositoryId
@@ -93,7 +92,7 @@ class Status(Handler):
parser.add_argument("--info", help="show additional package information",
action=argparse.BooleanOptionalAction, default=False)
parser.add_argument("-s", "--status", help="filter packages by status",
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum))
type=BuildStatusEnum, choices=tuple(BuildStatusEnum))
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
return parser
@@ -22,7 +22,6 @@ import argparse
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.core.utils import enum_values
from ahriman.models.action import Action
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.repository_id import RepositoryId
@@ -97,7 +96,7 @@ class StatusUpdate(Handler):
"If no packages supplied, service status will be updated",
nargs="*")
parser.add_argument("-s", "--status", help="new package build status",
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum), default=BuildStatusEnum.Success)
type=BuildStatusEnum, choices=tuple(BuildStatusEnum), default=BuildStatusEnum.Success)
parser.set_defaults(action=Action.Update, lock=None, quiet=True, report=False, unsafe=True)
return parser
@@ -115,7 +114,7 @@ class StatusUpdate(Handler):
parser = root.add_parser("repo-status-update", help="update repository status",
description="update repository status on the status page")
parser.add_argument("-s", "--status", help="new status",
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum), default=BuildStatusEnum.Success)
type=BuildStatusEnum, choices=tuple(BuildStatusEnum), default=BuildStatusEnum.Success)
parser.set_defaults(action=Action.Update, lock=None, package=[], quiet=True, report=False, unsafe=True)
return parser
@@ -25,7 +25,6 @@ from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import PasswordError
from ahriman.core.formatters import UserPrinter
from ahriman.core.utils import enum_values
from ahriman.models.action import Action
from ahriman.models.repository_id import RepositoryId
from ahriman.models.user import User
@@ -89,7 +88,7 @@ class Users(Handler):
"-p", "--password", help="user password. Blank password will be treated as empty password, "
"which is in particular must be used for OAuth2 authorization type.")
parser.add_argument("-R", "--role", help="user access level",
type=UserAccess, choices=enum_values(UserAccess), default=UserAccess.Read)
type=UserAccess, choices=tuple(UserAccess), default=UserAccess.Read)
parser.set_defaults(action=Action.Update, architecture="", exit_code=False, lock=None, quiet=True,
report=False, repository="")
return parser
@@ -110,8 +109,7 @@ class Users(Handler):
parser.add_argument("username", help="filter users by username", nargs="?")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
action="store_true")
parser.add_argument("-R", "--role", help="filter users by role", type=UserAccess,
choices=enum_values(UserAccess))
parser.add_argument("-R", "--role", help="filter users by role", type=UserAccess, choices=tuple(UserAccess))
parser.set_defaults(action=Action.List, architecture="", lock=None, quiet=True, report=False, repository="",
unsafe=True)
return parser
@@ -83,6 +83,15 @@ Process stderr:
{self.stderr}"""
class DependencyResolutionError(RuntimeError):
"""
exception raised when package dependencies cannot be resolved
"""
def __init__(self, unprocessed: list[str]) -> None:
RuntimeError.__init__(self, f"Could not process dependencies. Unresolved packages: {unprocessed}")
class DuplicateRunError(RuntimeError):
"""
exception which will be raised if there is another application instance
+6 -1
View File
@@ -22,7 +22,7 @@ from __future__ import annotations
from collections.abc import Iterable
from functools import partial
from ahriman.core.exceptions import PartitionError
from ahriman.core.exceptions import DependencyResolutionError, PartitionError
from ahriman.core.utils import minmax, partition
from ahriman.models.package import Package
@@ -226,6 +226,9 @@ class Tree:
Returns:
list[list[Package]]: sorted list of packages lists based on their dependencies
Raises:
DependencyResolutionError: dependency cycle prevents ordering packages
"""
unsorted: list[list[Leaf]] = []
@@ -235,6 +238,8 @@ class Tree:
# additional workaround with partial in order to hide cell-var-from-loop pylint warning
predicate = partial(Leaf.is_root, packages=unprocessed)
new_level, unprocessed = partition(unprocessed, predicate)
if not new_level:
raise DependencyResolutionError([leaf.package.base for leaf in unprocessed])
unsorted.append(new_level)
# move leaves to the end if they are not required at the next level
-15
View File
@@ -31,7 +31,6 @@ import subprocess
from collections.abc import Callable, Iterable, Iterator, Mapping
from dataclasses import asdict
from enum import Enum
from filelock import FileLock
from pathlib import Path
from pwd import getpwuid
@@ -46,7 +45,6 @@ __all__ = [
"check_output",
"check_user",
"dataclass_view",
"enum_values",
"extract_user",
"filelock",
"filter_json",
@@ -242,19 +240,6 @@ def dataclass_view(instance: Any) -> dict[str, Any]:
return asdict(instance, dict_factory=lambda fields: {key: value for key, value in fields if value is not None})
def enum_values(enum: type[Enum]) -> list[str]:
"""
generate list of enumeration values from the source
Args:
enum(type[Enum]): source enumeration class
Returns:
list[str]: available enumeration values as string
"""
return [str(key.value) for key in enum] # explicit str conversion for typing
def extract_user() -> str | None:
"""
extract user from system environment
+12 -1
View File
@@ -1,6 +1,6 @@
import pytest
from ahriman.core.exceptions import PartitionError
from ahriman.core.exceptions import DependencyResolutionError, PartitionError
from ahriman.core.tree import Leaf, Tree
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
@@ -270,3 +270,14 @@ def test_tree_partitions() -> None:
first, second = tree.partitions(count=3)
assert first == [leaf1.package, leaf2.package, leaf3.package, leaf5.package]
assert second == [leaf4.package]
def test_tree_levels_cycle(leaf_ahriman: Leaf, leaf_python_schedule: Leaf) -> None:
"""
must raise an exception if dependencies contain a cycle
"""
leaf_ahriman.dependencies = set(leaf_python_schedule.items)
leaf_python_schedule.dependencies = set(leaf_ahriman.items)
with pytest.raises(DependencyResolutionError):
Tree([leaf_ahriman, leaf_python_schedule]).levels()
@@ -221,15 +221,6 @@ def test_dataclass_view_without_none(package_ahriman: Package) -> None:
assert Package.from_json(result) == package_ahriman
def test_enum_values() -> None:
"""
must correctly generate choices from enumeration classes
"""
values = enum_values(PackageSource)
for value in values:
assert PackageSource(value).value == value
def test_extract_user() -> None:
"""
must extract user from system environment
@@ -87,7 +87,8 @@ class EventBusView(BaseView):
"""
while response.is_connected():
try:
event_type, data = await wait_for(queue.get(), timeout=response.ping_interval)
# pylint: disable=protected-access
event_type, data = await wait_for(queue.get(), timeout=response._ping_interval)
except TimeoutError:
continue
except QueueShutDown:
@@ -3,6 +3,7 @@ import pytest
from aiohttp.test_utils import TestClient
from aiohttp.web import HTTPBadRequest
from aiohttp_sse import EventSourceResponse
from asyncio import Queue
from multidict import MultiDict
from pytest_mock import MockerFixture
@@ -95,21 +96,20 @@ def test_routes() -> None:
async def test_run_timeout() -> None:
"""
must handle timeout and continue loop
must recheck the connection after a queue timeout
"""
queue = Queue()
queue = AsyncMock(spec_set=Queue)
queue.get.side_effect = TimeoutError
response = AsyncMock(spec_set=EventSourceResponse())
response._ping_interval = 0.01
response.is_connected.side_effect = [True, False]
async def _shutdown() -> None:
await asyncio.sleep(0.05)
queue.shutdown()
response = AsyncMock()
response.is_connected = lambda: True
response.ping_interval = 0.01
asyncio.create_task(_shutdown())
await EventBusView._run(response, queue)
assert response.is_connected.call_count == 2
queue.get.assert_awaited_once()
response.send.assert_not_awaited()
def test_topics() -> None:
"""
+1 -1
View File
@@ -2,7 +2,7 @@
pkgbase='ahriman'
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
pkgver=2.22.1
pkgver=2.22.2
pkgrel=1
pkgdesc="ArcH linux ReposItory MANager"
arch=('any')
+3 -8
View File
@@ -5,7 +5,7 @@ digraph G {
rankdir = TB;
node [style=filled,fillcolor="#ffffff",fontcolor="#000000",fontname=Helvetica,fontsize=10];
ahriman_application_ahriman [fillcolor="#814b37",fontcolor="#ffffff",label="ahriman\.\napplication\.\nahriman"];
ahriman_application_ahriman [fillcolor="#884f3a",fontcolor="#ffffff",label="ahriman\.\napplication\.\nahriman"];
ahriman_application_application [fillcolor="blue",fontcolor="white",label="ahriman\.\napplication\.\napplication",shape="box"];
ahriman_application_application_application [fillcolor="blue",fontcolor="white",label="ahriman\.\napplication\.\napplication\.\napplication",shape="box"];
ahriman_application_application_application_packages [fillcolor="blue",fontcolor="white",label="ahriman\.\napplication\.\napplication\.\napplication_packages",shape="box"];
@@ -41,12 +41,12 @@ digraph G {
ahriman_application_handlers_run [fillcolor="#8f533d",fontcolor="#ffffff",label="ahriman\.\napplication\.\nhandlers\.\nrun"];
ahriman_application_handlers_search [fillcolor="#6b3e2e",fontcolor="#ffffff",label="ahriman\.\napplication\.\nhandlers\.\nsearch"];
ahriman_application_handlers_service_updates [fillcolor="#814b37",fontcolor="#ffffff",label="ahriman\.\napplication\.\nhandlers\.\nservice_updates"];
ahriman_application_handlers_setup [fillcolor="#6b3e2e",fontcolor="#ffffff",label="ahriman\.\napplication\.\nhandlers\.\nsetup"];
ahriman_application_handlers_setup [fillcolor="#724331",fontcolor="#ffffff",label="ahriman\.\napplication\.\nhandlers\.\nsetup"];
ahriman_application_handlers_shell [fillcolor="#814b37",fontcolor="#ffffff",label="ahriman\.\napplication\.\nhandlers\.\nshell"];
ahriman_application_handlers_sign [fillcolor="#884f3a",fontcolor="#ffffff",label="ahriman\.\napplication\.\nhandlers\.\nsign"];
ahriman_application_handlers_statistics [fillcolor="#6b3e2e",fontcolor="#ffffff",label="ahriman\.\napplication\.\nhandlers\.\nstatistics"];
ahriman_application_handlers_status [fillcolor="#6b3e2e",fontcolor="#ffffff",label="ahriman\.\napplication\.\nhandlers\.\nstatus"];
ahriman_application_handlers_status_update [fillcolor="#724331",fontcolor="#ffffff",label="ahriman\.\napplication\.\nhandlers\.\nstatus_update"];
ahriman_application_handlers_status_update [fillcolor="#794734",fontcolor="#ffffff",label="ahriman\.\napplication\.\nhandlers\.\nstatus_update"];
ahriman_application_handlers_structure [fillcolor="#794734",fontcolor="#ffffff",label="ahriman\.\napplication\.\nhandlers\.\nstructure"];
ahriman_application_handlers_tree_migrate [fillcolor="#814b37",fontcolor="#ffffff",label="ahriman\.\napplication\.\nhandlers\.\ntree_migrate"];
ahriman_application_handlers_triggers [fillcolor="#8a472e",fontcolor="#ffffff",label="ahriman\.\napplication\.\nhandlers\.\ntriggers"];
@@ -1080,19 +1080,14 @@ digraph G {
ahriman_core_upload_upload -> ahriman_core_upload_s3 [fillcolor="blue",weight="3"];
ahriman_core_upload_upload -> ahriman_core_upload_upload_trigger [fillcolor="blue",weight="3"];
ahriman_core_upload_upload_trigger -> ahriman_core_upload [fillcolor="blue",weight="3"];
ahriman_core_utils -> ahriman_application_ahriman [fillcolor="#db4005",minlen="2"];
ahriman_core_utils -> ahriman_application_application_application_packages [fillcolor="#db4005",minlen="3"];
ahriman_core_utils -> ahriman_application_handlers_add [fillcolor="#db4005",minlen="3"];
ahriman_core_utils -> ahriman_application_handlers_daemon [fillcolor="#db4005",minlen="3"];
ahriman_core_utils -> ahriman_application_handlers_rebuild [fillcolor="#db4005",minlen="3"];
ahriman_core_utils -> ahriman_application_handlers_rollback [fillcolor="#db4005",minlen="3"];
ahriman_core_utils -> ahriman_application_handlers_setup [fillcolor="#db4005",minlen="3"];
ahriman_core_utils -> ahriman_application_handlers_statistics [fillcolor="#db4005",minlen="3"];
ahriman_core_utils -> ahriman_application_handlers_status [fillcolor="#db4005",minlen="3"];
ahriman_core_utils -> ahriman_application_handlers_status_update [fillcolor="#db4005",minlen="3"];
ahriman_core_utils -> ahriman_application_handlers_tree_migrate [fillcolor="#db4005",minlen="3"];
ahriman_core_utils -> ahriman_application_handlers_update [fillcolor="#db4005",minlen="3"];
ahriman_core_utils -> ahriman_application_handlers_users [fillcolor="#db4005",minlen="3"];
ahriman_core_utils -> ahriman_application_lock [fillcolor="#db4005",minlen="2"];
ahriman_core_utils -> ahriman_core_alpm_pacman [fillcolor="#db4005",minlen="2",weight="2"];
ahriman_core_utils -> ahriman_core_alpm_repo [fillcolor="#db4005",minlen="2",weight="2"];
+15 -12
View File
@@ -2,34 +2,37 @@
"dependencies": {
"@emotion/react": ">=11.14.0 <11.15.0",
"@emotion/styled": ">=11.14.0 <11.15.0",
"@mui/icons-material": ">=9.3.0 <9.4.0",
"@mui/material": ">=9.3.0 <9.4.0",
"@mui/x-data-grid": ">=9.11.0 <9.12.0",
"@tanstack/react-query": ">=5.101.0 <5.102.0",
"@mui/icons-material": ">=9.5.0 <9.6.0",
"@mui/material": ">=9.5.0 <9.6.0",
"@mui/x-data-grid": ">=9.15.0 <9.16.0",
"@tanstack/react-query": ">=5.104.0 <5.105.0",
"chart.js": ">=4.5.0 <4.6.0",
"react": ">=19.2.0 <19.3.0",
"react": ">=19.3.0 <19.4.0",
"react-chartjs-2": ">=5.3.0 <5.4.0",
"react-dom": ">=19.2.0 <19.3.0",
"react-dom": ">=19.3.0 <19.4.0",
"react-error-boundary": ">=6.1.0 <6.2.0",
"react-syntax-highlighter": ">=16.1.0 <16.2.0"
},
"devDependencies": {
"@eslint/js": ">=9.39.0 <9.40.0",
"@stylistic/eslint-plugin": ">=5.10.0 <5.11.0",
"@types/react": ">=19.2.0 <19.3.0",
"@types/react-dom": ">=19.2.0 <19.3.0",
"@types/react": ">=19.3.0 <19.4.0",
"@types/react-dom": ">=19.3.0 <19.4.0",
"@types/react-syntax-highlighter": ">=15.5.0 <15.6.0",
"@vitejs/plugin-react": ">=6.0.0 <6.1.0",
"@vitejs/plugin-react": ">=6.1.0 <6.2.0",
"eslint": ">=9.39.0 <9.40.0",
"eslint-plugin-react": ">=7.37.0 <7.38.0",
"eslint-plugin-react-hooks": ">=7.1.0 <7.2.0",
"eslint-plugin-react-refresh": ">=0.5.0 <0.6.0",
"eslint-plugin-simple-import-sort": ">=14.0.0 <14.1.0",
"typescript": ">=6.0.0 <6.1.0",
"typescript-eslint": ">=8.67.0 <8.68.0",
"vite": ">=8.2.0 <8.3.0"
"typescript-eslint": ">=8.71.0 <8.72.0",
"vite": ">=8.3.0 <8.4.0"
},
"name": "ahriman",
"overrides": {
"@mui/x-internals": "$@mui/x-data-grid"
},
"private": true,
"scripts": {
"build": "tsc && vite build",
@@ -39,5 +42,5 @@
"preview": "vite preview"
},
"type": "module",
"version": "2.22.1"
"version": "2.22.2"
}
+35 -5
View File
@@ -17,10 +17,13 @@
* 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 DeleteIcon from "@mui/icons-material/Delete";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import { Box, Link } from "@mui/material";
import {
DataGrid,
GRID_CHECKBOX_SELECTION_COL_DEF,
GridActionsCellItem,
type GridColDef,
type GridFilterModel,
type GridRowId,
@@ -56,6 +59,7 @@ function createListColumn(
export default function PackageTable(): React.JSX.Element {
const table = usePackageTable();
const { handleRemove, handleUpdate, isAuthorized } = table;
const apiRef = useGridApiRef();
const debouncedSearch = useDebounce(table.searchText, 300);
@@ -96,8 +100,31 @@ export default function PackageTable(): React.JSX.Element {
<StatusCell isHeld={params.row.isHeld} status={params.row.status} />,
width: 120,
},
...isAuthorized ? [{
field: "actions",
type: "actions",
headerName: "actions",
width: 100,
disableExport: true,
getActions: ({ row }) => [
<GridActionsCellItem
key="update"
icon={<PlayArrowIcon />}
label={`Update ${row.base}`}
title={`Update ${row.base}`}
onClick={() => void handleUpdate([row.base])}
/>,
<GridActionsCellItem
key="remove"
icon={<DeleteIcon />}
label={`Remove ${row.base}`}
title={`Remove ${row.base}`}
onClick={() => void handleRemove([row.base])}
/>,
],
} satisfies GridColDef<PackageRow>] : [],
],
[],
[handleRemove, handleUpdate, isAuthorized],
);
return <Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
@@ -110,8 +137,10 @@ export default function PackageTable(): React.JSX.Element {
onRebuildClick: () => table.setDialogOpen("rebuild"),
onRefreshDatabaseClick: () => void table.handleRefreshDatabase(),
onReloadClick: table.handleReload,
onRemoveClick: () => void table.handleRemove(),
onUpdateClick: () => void table.handleUpdate(),
onRemoveClick: () => void table.handleRemove(table.selectionModel)
.then(() => table.setSelectionModel([])),
onUpdateClick: () => void table.handleUpdate(table.selectionModel)
.then(() => table.setSelectionModel([])),
}}
isAuthorized={table.isAuthorized}
hasSelection={table.selectionModel.length > 0}
@@ -123,6 +152,7 @@ export default function PackageTable(): React.JSX.Element {
<DataGrid
apiRef={apiRef}
checkboxSelection
localeText={{ checkboxSelectionHeaderName: "selection" }}
columnVisibilityModel={table.columnVisibility}
columns={columns}
density="compact"
@@ -134,8 +164,8 @@ export default function PackageTable(): React.JSX.Element {
}}
loading={table.isLoading}
onCellClick={(params, event) => {
// Don't open info dialog when clicking checkbox or link
if (params.field === GRID_CHECKBOX_SELECTION_COL_DEF.field) {
// Don't open info dialog when clicking checkbox, actions, or link
if (params.field === GRID_CHECKBOX_SELECTION_COL_DEF.field || params.field === "actions") {
return;
}
if ((event.target as HTMLElement).closest("a")) {
+23 -24
View File
@@ -24,29 +24,27 @@ import { useClient } from "hooks/useClient";
import { useNotification } from "hooks/useNotification";
import { useRepository } from "hooks/useRepository";
import type { RepositoryId } from "models/RepositoryId";
import { useCallback } from "react";
export interface UsePackageActionsResult {
handleRefreshDatabase: () => Promise<void>;
handleReload: () => void;
handleRemove: () => Promise<void>;
handleUpdate: () => Promise<void>;
handleRemove: (packages: string[]) => Promise<void>;
handleUpdate: (packages: string[]) => Promise<void>;
}
export function usePackageActions(
selectionModel: string[],
setSelectionModel: (model: string[]) => void,
): UsePackageActionsResult {
export function usePackageActions(): UsePackageActionsResult {
const client = useClient();
const { currentRepository } = useRepository();
const { showSuccess, showError } = useNotification();
const queryClient = useQueryClient();
const invalidate = (repository: RepositoryId): void => {
const invalidate = useCallback((repository: RepositoryId): void => {
void queryClient.invalidateQueries({ queryKey: QueryKeys.packages(repository) });
void queryClient.invalidateQueries({ queryKey: QueryKeys.status(repository) });
};
}, [queryClient]);
const performAction = async (
const performAction = useCallback(async (
action: (repository: RepositoryId) => Promise<string>,
errorMessage: string,
): Promise<void> => {
@@ -57,11 +55,10 @@ export function usePackageActions(
const successMessage = await action(currentRepository);
showSuccess("Success", successMessage);
invalidate(currentRepository);
setSelectionModel([]);
} catch (exception) {
showError("Action failed", `${errorMessage}: ${ApiError.errorDetail(exception)}`);
}
};
}, [currentRepository, invalidate, showError, showSuccess]);
const handleReload = (): void => {
if (currentRepository !== null) {
@@ -69,14 +66,16 @@ export function usePackageActions(
}
};
const handleUpdate = (): Promise<void> => performAction(async (repository): Promise<string> => {
if (selectionModel.length === 0) {
await client.service.servicePackageUpdate(repository, { packages: [] });
return "Repository update has been run";
}
await client.service.servicePackageAdd(repository, { packages: selectionModel });
return `Run update for packages ${selectionModel.join(", ")}`;
}, "Packages update failed");
const handleUpdate = useCallback((packages: string[]): Promise<void> => {
return performAction(async (repository): Promise<string> => {
if (packages.length === 0) {
await client.service.servicePackageUpdate(repository, { packages: [] });
return "Repository update has been run";
}
await client.service.servicePackageAdd(repository, { packages });
return `Run update for packages ${packages.join(", ")}`;
}, "Packages update failed");
}, [client, performAction]);
const handleRefreshDatabase = (): Promise<void> => performAction(async (repository): Promise<string> => {
await client.service.servicePackageUpdate(repository, {
@@ -89,15 +88,15 @@ export function usePackageActions(
return "Pacman database update has been requested";
}, "Could not update pacman databases");
const handleRemove = (): Promise<void> => {
if (selectionModel.length === 0) {
const handleRemove = useCallback((packages: string[]): Promise<void> => {
if (packages.length === 0) {
return Promise.resolve();
}
return performAction(async (repository): Promise<string> => {
await client.service.servicePackageRemove(repository, selectionModel);
return `Packages ${selectionModel.join(", ")} have been removed`;
await client.service.servicePackageRemove(repository, packages);
return `Packages ${packages.join(", ")} have been removed`;
}, "Could not remove packages");
};
}, [client, performAction]);
return {
handleRefreshDatabase,
+3 -3
View File
@@ -30,8 +30,8 @@ export interface UsePackageTableResult {
filterModel: GridFilterModel;
handleRefreshDatabase: () => Promise<void>;
handleReload: () => void;
handleRemove: () => Promise<void>;
handleUpdate: () => Promise<void>;
handleRemove: (packages: string[]) => Promise<void>;
handleUpdate: (packages: string[]) => Promise<void>;
isAuthorized: boolean;
isLoading: boolean;
paginationModel: { page: number; pageSize: number };
@@ -52,7 +52,7 @@ export interface UsePackageTableResult {
export function usePackageTable(): UsePackageTableResult {
const { rows, isLoading, isAuthorized, status } = usePackageData();
const tableState = useTableState();
const actions = usePackageActions(tableState.selectionModel, tableState.setSelectionModel);
const actions = usePackageActions();
return {
isLoading,