Compare commits

...
7 Commits
Author SHA1 Message Date
arcanis 7f28e47ac4 fix: add files to git with force flag
It has been reported that AUR repositories might contain .gitignore,
which prevents PKGBUILD/.SRCINFO/whatever to be added without --force
flag. This commit simply adds force flag for all those operations, which
should be fine, because we don't commit wildcards

Closes #167
2026-10-17 12:17:40 +03:00
arcanis 0b371b3e77 feat: add tooltip for held packages 2026-10-16 13:09:17 +03:00
arcanis ef47c6fa72 feat: distinguish between repo update and package update in ui 2026-10-16 13:03:48 +03:00
arcanis 81ea175034 bug: always keep existing package version in archive 2026-10-16 12:11:51 +03:00
arcanis b73be7abcc feat: only remove packages from build queue which were actually built 2026-10-16 11:49:52 +03:00
arcanis 4537969d5d fix: handle is_held flag in all update processes
Previos implementation has ignored this flag in dependencies checks,
local package update and so on
2026-10-14 13:44:37 +03:00
arcanis 47c39f93b7 feat: align tables vertically in center 2026-10-13 13:22:00 +03:00
15 changed files with 152 additions and 29 deletions

No files matched your search

@@ -158,6 +158,8 @@ class ApplicationRepository(ApplicationProperties):
# process already built packages if any # process already built packages if any
if built_packages := self.repository.packages_built(): # speedup a bit if built_packages := self.repository.packages_built(): # speedup a bit
build_result = self.repository.process_update(built_packages, packagers) build_result = self.repository.process_update(built_packages, packagers)
for package in build_result.success:
self.database.build_queue_clear(package.base)
self.on_result(build_result) self.on_result(build_result)
result.merge(build_result) result.merge(build_result)
@@ -172,6 +174,8 @@ class ApplicationRepository(ApplicationProperties):
for num, partition in enumerate(partitions): for num, partition in enumerate(partitions):
self.logger.info("processing chunk #%i %s", num, [package.base for package in partition]) self.logger.info("processing chunk #%i %s", num, [package.base for package in partition])
build_result = builder.update(partition, packagers, bump_pkgrel=bump_pkgrel) build_result = builder.update(partition, packagers, bump_pkgrel=bump_pkgrel)
for package in partition:
self.database.build_queue_clear(package.base)
self.on_result(build_result) self.on_result(build_result)
result.merge(build_result) result.merge(build_result)
@@ -165,7 +165,7 @@ class Sources(LazyLogging):
# extract local files... # extract local files...
files = ["PKGBUILD", ".SRCINFO"] + [str(path) for path in Pkgbuild.local_files(sources_dir)] files = ["PKGBUILD", ".SRCINFO"] + [str(path) for path in Pkgbuild.local_files(sources_dir)]
instance.add(sources_dir, *files) instance.add(sources_dir, *files, force=True)
# ...and commit them # ...and commit them
instance.commit(sources_dir) instance.commit(sources_dir)
@@ -232,7 +232,7 @@ class Sources(LazyLogging):
git_url, branch = remote.git_source() git_url, branch = remote.git_source()
check_output(*instance.git(), "push", "--quiet", git_url, branch, cwd=sources_dir, logger=instance.logger) check_output(*instance.git(), "push", "--quiet", git_url, branch, cwd=sources_dir, logger=instance.logger)
def add(self, sources_dir: Path, *pattern: str, intent_to_add: bool = False) -> None: def add(self, sources_dir: Path, *pattern: str, intent_to_add: bool = False, force: bool = False) -> None:
""" """
track found files via git track found files via git
@@ -241,6 +241,7 @@ class Sources(LazyLogging):
*pattern(str): glob patterns *pattern(str): glob patterns
intent_to_add(bool, optional): record only the fact that it will be added later, acts as intent_to_add(bool, optional): record only the fact that it will be added later, acts as
--intent-to-add git flag (Default value = False) --intent-to-add git flag (Default value = False)
force(bool, optional): allow adding ignored files (Default value = False)
""" """
# glob directory to find files which match the specified patterns # glob directory to find files which match the specified patterns
found_files: list[Path] = [] found_files: list[Path] = []
@@ -251,6 +252,8 @@ class Sources(LazyLogging):
self.logger.info("found matching files %s", found_files) self.logger.info("found matching files %s", found_files)
# add them to index # add them to index
args = ["--intent-to-add"] if intent_to_add else [] args = ["--intent-to-add"] if intent_to_add else []
if force:
args.append("--force")
check_output(*self.git(), "add", *args, *[str(fn.relative_to(sources_dir)) for fn in found_files], check_output(*self.git(), "add", *args, *[str(fn.relative_to(sources_dir)) for fn in found_files],
cwd=sources_dir, logger=self.logger) cwd=sources_dir, logger=self.logger)
@@ -87,7 +87,12 @@ class ArchiveRotationTrigger(Trigger):
if self.keep_built_packages == 0: if self.keep_built_packages == 0:
return return
to_remove = repository.package_archives(package.base) current = next(iter(repository.packages([package.base])), None)
to_remove = [
single
for single in repository.package_archives(package.base)
if current is None or single.version != current.version
]
for single in to_remove[:-self.keep_built_packages]: for single in to_remove[:-self.keep_built_packages]:
self.logger.info("removing version %s of package %s", single.version, single.base) self.logger.info("removing version %s of package %s", single.version, single.base)
@@ -36,6 +36,17 @@ class UpdateHandler(PackageInfo, Cleaner):
trait to get package update list trait to get package update list
""" """
def _updates_ignored(self) -> set[str]:
"""
collect package bases excluded from automatic updates
Returns:
set[str]: held and configured ignored package bases
"""
return set(self._ignore_list) | {
package.base for package, status in self.reporter.package_get(None) if status.is_held
}
def updates_aur(self, filter_packages: Iterable[str], *, vcs: bool) -> list[Package]: def updates_aur(self, filter_packages: Iterable[str], *, vcs: bool) -> list[Package]:
""" """
check AUR for updates check AUR for updates
@@ -58,9 +69,7 @@ class UpdateHandler(PackageInfo, Cleaner):
continue continue
raise UnknownPackageError(package.base) raise UnknownPackageError(package.base)
ignore_list = self._ignore_list + [ ignore_list = self._updates_ignored()
package.base for package, status in self.reporter.package_get(None) if status.is_held
]
result: list[Package] = [] result: list[Package] = []
for local in self.packages(filter_packages): for local in self.packages(filter_packages):
@@ -106,8 +115,13 @@ class UpdateHandler(PackageInfo, Cleaner):
return files return files
ignore_list = self._updates_ignored()
result: list[Package] = [] result: list[Package] = []
for local in self.packages(filter_packages): for local in self.packages(filter_packages):
if local.base in ignore_list:
self.logger.info("package %s is held, skip update check", local.base)
continue
dependencies = self.reporter.package_dependencies_get(local.base) dependencies = self.reporter.package_dependencies_get(local.base)
if not dependencies.paths: if not dependencies.paths:
continue # skip check if no package dependencies found continue # skip check if no package dependencies found
@@ -141,9 +155,13 @@ class UpdateHandler(PackageInfo, Cleaner):
""" """
packages = {local.base: local for local in self.packages()} packages = {local.base: local for local in self.packages()}
local_versions = {package_base: package.version for package_base, package in packages.items()} local_versions = {package_base: package.version for package_base, package in packages.items()}
ignore_list = self._updates_ignored()
result: list[Package] = [] result: list[Package] = []
for cache_dir in self.paths.cache.iterdir(): for cache_dir in self.paths.cache.iterdir():
if cache_dir.name in ignore_list:
self.logger.info("package %s is held, skip update check", cache_dir.name)
continue
with self.in_package_context(cache_dir.name, local_versions.get(cache_dir.name)): with self.in_package_context(cache_dir.name, local_versions.get(cache_dir.name)):
try: try:
source = RemoteSource( source = RemoteSource(
@@ -162,6 +180,9 @@ class UpdateHandler(PackageInfo, Cleaner):
continue # we don't add packages automatically continue # we don't add packages automatically
if local.remote.is_remote: if local.remote.is_remote:
continue # avoid checking AUR packages continue # avoid checking AUR packages
if local.base in ignore_list:
self.logger.info("package %s is held, skip update check", local.base)
continue
if PackageVersion(local).is_outdated(remote, self.configuration, calculate_version=vcs): if PackageVersion(local).is_outdated(remote, self.configuration, calculate_version=vcs):
self.reporter.set_pending(local.base) self.reporter.set_pending(local.base)
@@ -191,7 +212,6 @@ class UpdateHandler(PackageInfo, Cleaner):
self.reporter.set_pending(local.base) self.reporter.set_pending(local.base)
self.event(local.base, EventType.PackageOutdated, "Manual update is requested") self.event(local.base, EventType.PackageOutdated, "Manual update is requested")
self.clear_queue()
except Exception: except Exception:
self.logger.exception("could not load packages from database") self.logger.exception("could not load packages from database")
@@ -188,6 +188,7 @@ def test_update(application_repository: ApplicationRepository, package_ahriman:
""" """
must process package updates must process package updates
""" """
application_repository.database.build_queue_insert(package_ahriman)
paths = [package.filepath for package in package_ahriman.packages.values()] paths = [package.filepath for package in package_ahriman.packages.values()]
tree = Tree([Leaf(package_ahriman)]) tree = Tree([Leaf(package_ahriman)])
prebuilt_result = Result() prebuilt_result = Result()
@@ -207,12 +208,15 @@ def test_update(application_repository: ApplicationRepository, package_ahriman:
update_mock.assert_called_once_with(paths, Packagers("username")) update_mock.assert_called_once_with(paths, Packagers("username"))
on_result_mock.assert_has_calls([MockCall(prebuilt_result), MockCall(result)]) on_result_mock.assert_has_calls([MockCall(prebuilt_result), MockCall(result)])
assert application_repository.database.build_queue_get() == []
def test_update_prebuilt_filter(application_repository: ApplicationRepository, package_ahriman: Package, result: Result, def test_update_prebuilt_filter(application_repository: ApplicationRepository, package_ahriman: Package, result: Result,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must filter out packages which were successfully prebuilt must filter out packages which were successfully prebuilt
""" """
application_repository.database.build_queue_insert(package_ahriman)
paths = [package.filepath for package in package_ahriman.packages.values()] paths = [package.filepath for package in package_ahriman.packages.values()]
resolve_mock = mocker.patch("ahriman.application.application.workers.local_updater.LocalUpdater.partition", resolve_mock = mocker.patch("ahriman.application.application.workers.local_updater.LocalUpdater.partition",
@@ -224,6 +228,8 @@ def test_update_prebuilt_filter(application_repository: ApplicationRepository, p
application_repository.update([package_ahriman], Packagers("username"), bump_pkgrel=True) application_repository.update([package_ahriman], Packagers("username"), bump_pkgrel=True)
resolve_mock.assert_called_once_with([]) resolve_mock.assert_called_once_with([])
assert application_repository.database.build_queue_get() == []
def test_update_empty(application_repository: ApplicationRepository, package_ahriman: Package, result: Result, def test_update_empty(application_repository: ApplicationRepository, package_ahriman: Package, result: Result,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
@@ -195,7 +195,7 @@ def test_init(sources: Sources, mocker: MockerFixture) -> None:
sources.init(local) sources.init(local)
check_output_mock.assert_called_once_with(*sources.git(), "init", "--quiet", "--initial-branch", check_output_mock.assert_called_once_with(*sources.git(), "init", "--quiet", "--initial-branch",
sources.DEFAULT_BRANCH, cwd=local, logger=pytest.helpers.anyvar(int)) sources.DEFAULT_BRANCH, cwd=local, logger=pytest.helpers.anyvar(int))
add_mock.assert_called_once_with(local, "PKGBUILD", ".SRCINFO", "local") add_mock.assert_called_once_with(local, "PKGBUILD", ".SRCINFO", "local", force=True)
commit_mock.assert_called_once_with(local) commit_mock.assert_called_once_with(local)
@@ -347,6 +347,17 @@ def test_add_skip(sources: Sources, mocker: MockerFixture) -> None:
check_output_mock.assert_not_called() check_output_mock.assert_not_called()
def test_add_force(sources: Sources, tmp_path: Path, mocker: MockerFixture) -> None:
"""
must add files to git with --force flag
"""
mocker.patch("pathlib.Path.glob", return_value=[Path("local/1"), Path("local/2")])
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.check_output")
sources.add(Path("local"), "PKGBUILD", force=True)
assert "--force" in check_output_mock.call_args.args
def test_commit(sources: Sources, mocker: MockerFixture) -> None: def test_commit(sources: Sources, mocker: MockerFixture) -> None:
""" """
must commit changes must commit changes
@@ -22,10 +22,10 @@ def test_configuration_sections(configuration: Configuration) -> None:
def test_archives_remove(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package, def test_archives_remove(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package,
repository: Repository, mocker: MockerFixture) -> None: repository: Repository, mocker: MockerFixture) -> None:
""" """
must remove older packages must keep the current package and the configured amount of additional packages
""" """
packages = [] packages = []
for i in range(5): for i in range(1, 7):
generated = replace(package_ahriman, version=str(i)) generated = replace(package_ahriman, version=str(i))
generated.packages = { generated.packages = {
key: replace(value, filename=str(i)) key: replace(value, filename=str(i))
@@ -33,14 +33,17 @@ def test_archives_remove(archive_rotation_trigger: ArchiveRotationTrigger, packa
} }
packages.append(generated) packages.append(generated)
archive_rotation_trigger.keep_built_packages = 2
mocker.patch.object(repository, "packages", return_value=[replace(package_ahriman, version="3")])
mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=packages) mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=packages)
mocker.patch("pathlib.Path.glob", return_value=[Path(str(i)) for i in range(5)]) mocker.patch("pathlib.Path.glob", side_effect=lambda pattern: [Path(pattern.removesuffix("*"))])
unlink_mock = mocker.patch("pathlib.Path.unlink", autospec=True) unlink_mock = mocker.patch("pathlib.Path.unlink", autospec=True)
archive_rotation_trigger.archives_remove(package_ahriman, repository) archive_rotation_trigger.archives_remove(package_ahriman, repository)
unlink_mock.assert_has_calls([ unlink_mock.assert_has_calls([
MockCall(Path("0")),
MockCall(Path("1")), MockCall(Path("1")),
MockCall(Path("2")),
MockCall(Path("4")),
]) ])
@@ -14,6 +14,20 @@ from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource from ahriman.models.remote_source import RemoteSource
def test_updates_ignored(update_handler: UpdateHandler, package_ahriman: Package,
package_python_schedule: Package, mocker: MockerFixture) -> None:
"""
must combine configured ignored packages and held packages only
"""
update_handler._ignore_list = ["ignored-package"]
mocker.patch("ahriman.core.status.local_client.LocalClient.package_get", return_value=[
(package_ahriman, BuildStatus(is_held=True)),
(package_python_schedule, BuildStatus(is_held=False)),
])
assert update_handler._updates_ignored() == {"ignored-package", package_ahriman.base}
def test_updates_aur(update_handler: UpdateHandler, package_ahriman: Package, def test_updates_aur(update_handler: UpdateHandler, package_ahriman: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
@@ -194,6 +208,18 @@ def test_updates_dependencies(update_handler: UpdateHandler, package_ahriman: Pa
pytest.helpers.anyvar(str, True)) pytest.helpers.anyvar(str, True))
def test_updates_dependencies_ignore(update_handler: UpdateHandler, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must skip held packages when checking broken dependencies
"""
mocker.patch("ahriman.core.status.local_client.LocalClient.package_get",
return_value=[(package_ahriman, BuildStatus(is_held=True))])
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
assert update_handler.updates_dependencies([]) == []
def test_updates_dependencies_skip_unknown(update_handler: UpdateHandler, package_ahriman: Package, def test_updates_dependencies_skip_unknown(update_handler: UpdateHandler, package_ahriman: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
@@ -249,6 +275,19 @@ def test_updates_local(update_handler: UpdateHandler, package_ahriman: Package,
calculate_version=True) calculate_version=True)
def test_updates_local_ignore(update_handler: UpdateHandler, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must skip held packages before fetching local sources
"""
mocker.patch("ahriman.core.status.local_client.LocalClient.package_get",
return_value=[(package_ahriman, BuildStatus(is_held=True))])
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
mocker.patch("pathlib.Path.iterdir", return_value=[Path(package_ahriman.base)])
assert update_handler.updates_local(vcs=True) == []
def test_updates_local_ignore_vcs(update_handler: UpdateHandler, package_ahriman: Package, def test_updates_local_ignore_vcs(update_handler: UpdateHandler, package_ahriman: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
@@ -306,17 +345,32 @@ def test_updates_local_with_failures(update_handler: UpdateHandler, package_ahri
assert not update_handler.updates_local(vcs=True) assert not update_handler.updates_local(vcs=True)
def test_updates_manual_clear(update_handler: UpdateHandler, mocker: MockerFixture) -> None: def test_updates_local_held_metadata(update_handler: UpdateHandler, package_ahriman: Package,
mocker: MockerFixture) -> None:
""" """
requesting manual updates must clear packages directory must respect the held base even when the source directory has a different name
""" """
mocker.patch("pathlib.Path.iterdir", return_value=[]) package_ahriman.remote = RemoteSource(source=PackageSource.Local)
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages") mocker.patch.object(update_handler.reporter, "package_get",
return_value=[(package_ahriman, BuildStatus(is_held=True))])
mocker.patch.object(update_handler, "packages", return_value=[package_ahriman])
mocker.patch("pathlib.Path.iterdir", return_value=[Path("different-base")])
mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman)
update_handler.updates_manual() assert update_handler.updates_local(vcs=True) == []
from ahriman.core.repository.cleaner import Cleaner
Cleaner.clear_queue.assert_called_once_with() def test_updates_manual_preserves_queue(update_handler: UpdateHandler, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
discovery must leave manual requests queued for execution
"""
update_handler.database.build_queue_insert(package_ahriman)
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
assert update_handler.updates_manual() == [package_ahriman]
assert update_handler.database.build_queue_get() == [package_ahriman]
def test_updates_manual_status_known(update_handler: UpdateHandler, package_ahriman: Package, def test_updates_manual_status_known(update_handler: UpdateHandler, package_ahriman: Package,
+4
View File
@@ -257,6 +257,10 @@ There are few ways for packages to be marked as out-of-date and hence requiring
#. In addition to the above, if package is named as VCS (e.g. has suffix ``-git``) and the last update was more than specified threshold ago, the service will also try to fetch sources and check if the revision is newer than the built one. #. In addition to the above, if package is named as VCS (e.g. has suffix ``-git``) and the last update was more than specified threshold ago, the service will also try to fetch sources and check if the revision is newer than the built one.
#. In addition, there is ability to check if the dependencies of the package have been updated (e.g. if linked library has been renamed or the modules directory - e.g. in case of python and ruby packages - has been changed). And if so, the package will be marked as out-of-date as well. #. In addition, there is ability to check if the dependencies of the package have been updated (e.g. if linked library has been renamed or the modules directory - e.g. in case of python and ruby packages - has been changed). And if so, the package will be marked as out-of-date as well.
Held packages and bases listed in ``build.ignore_packages`` are excluded from automatic AUR, official-repository, local-source, and broken-dependency update checks. These checks leave their package status unchanged. Manual updates and explicit rebuilds remain available.
Reading manual updates, including during ``repo-update --dry-run``, leaves the build queue intact. Execution removes requests by package base after a build chunk returns or a remote worker accepts it; successful prebuilt imports also remove requests by package base. Interrupted chunks and work that was not selected remain queued. Concurrent requests for the same package base are not distinguished; request revisions and crash recovery are separate work.
Update packages Update packages
^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
+1 -1
View File
@@ -207,7 +207,7 @@ Web server settings. This feature requires ``aiohttp`` libraries to be installed
Describes settings for packages archives management extensions. Describes settings for packages archives management extensions.
* ``keep_built_packages`` - keep this amount of built packages with different versions, integer, required. ``0`` will effectively disable archives removal. * ``keep_built_packages`` - keep this amount of additional built package versions alongside the version currently published in the repository, integer, required. ``0`` will effectively disable archives removal.
``keyring`` group ``keyring`` group
----------------- -----------------
+1 -1
View File
@@ -44,7 +44,7 @@ This trigger will be called right after build process (``on_result``). It will p
``ahriman.core.housekeeping.ArchiveRotationTrigger`` ``ahriman.core.housekeeping.ArchiveRotationTrigger``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This trigger removes old package versions from the archive directory. It implements ``on_result`` and, after each build, compares available versions for updated packages and removes the older ones, keeping only the last N versions as configured by ``keep_built_packages`` option. This trigger removes old package versions from the archive directory. It implements ``on_result`` and, after each build, compares available versions for updated packages and removes older ones, keeping the version currently published in the repository plus the last N other versions as configured by ``keep_built_packages`` option.
``ahriman.core.housekeeping.LogsRotationTrigger`` ``ahriman.core.housekeeping.LogsRotationTrigger``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -143,9 +143,9 @@ export default function PackageTable(): React.JSX.Element {
.then(() => table.setSelectionModel([])), .then(() => table.setSelectionModel([])),
}} }}
isAuthorized={table.isAuthorized} isAuthorized={table.isAuthorized}
hasSelection={table.selectionModel.length > 0}
onSearchChange={table.setSearchText} onSearchChange={table.setSearchText}
searchText={table.searchText} searchText={table.searchText}
selectedPackageCount={table.selectionModel.length}
status={table.status} status={table.status}
/> />
@@ -48,19 +48,19 @@ export interface ToolbarActions {
interface PackageTableToolbarProps { interface PackageTableToolbarProps {
actions: ToolbarActions; actions: ToolbarActions;
hasSelection: boolean;
isAuthorized: boolean; isAuthorized: boolean;
onSearchChange: (text: string) => void; onSearchChange: (text: string) => void;
searchText: string; searchText: string;
selectedPackageCount: number;
status?: BuildStatus; status?: BuildStatus;
} }
export default function PackageTableToolbar({ export default function PackageTableToolbar({
actions, actions,
hasSelection,
isAuthorized, isAuthorized,
onSearchChange, onSearchChange,
searchText, searchText,
selectedPackageCount,
status, status,
}: PackageTableToolbarProps): React.JSX.Element { }: PackageTableToolbarProps): React.JSX.Element {
const [packagesAnchorEl, setPackagesAnchorEl] = useState<HTMLElement | null>(null); const [packagesAnchorEl, setPackagesAnchorEl] = useState<HTMLElement | null>(null);
@@ -103,7 +103,8 @@ export default function PackageTableToolbar({
<MenuItem onClick={() => { <MenuItem onClick={() => {
setPackagesAnchorEl(null); actions.onUpdateClick(); setPackagesAnchorEl(null); actions.onUpdateClick();
}}> }}>
<PlayArrowIcon fontSize="small" sx={{ mr: 1 }} /> update <PlayArrowIcon fontSize="small" sx={{ mr: 1 }} />
{selectedPackageCount === 0 ? "update all packages" : `update selected packages (${selectedPackageCount})`}
</MenuItem> </MenuItem>
<MenuItem onClick={() => { <MenuItem onClick={() => {
setPackagesAnchorEl(null); actions.onRefreshDatabaseClick(); setPackagesAnchorEl(null); actions.onRefreshDatabaseClick();
@@ -116,7 +117,7 @@ export default function PackageTableToolbar({
<ReplayIcon fontSize="small" sx={{ mr: 1 }} /> rebuild <ReplayIcon fontSize="small" sx={{ mr: 1 }} /> rebuild
</MenuItem> </MenuItem>
<Divider /> <Divider />
<MenuItem disabled={!hasSelection} onClick={() => { <MenuItem disabled={selectedPackageCount === 0} onClick={() => {
setPackagesAnchorEl(null); actions.onRemoveClick(); setPackagesAnchorEl(null); actions.onRemoveClick();
}}> }}>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> remove <DeleteIcon fontSize="small" sx={{ mr: 1 }} /> remove
+6 -3
View File
@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import PauseCircleIcon from "@mui/icons-material/PauseCircle"; import PauseCircleIcon from "@mui/icons-material/PauseCircle";
import { Chip } from "@mui/material"; import { Chip, Tooltip } from "@mui/material";
import type { BuildStatus } from "models/BuildStatus"; import type { BuildStatus } from "models/BuildStatus";
import type React from "react"; import type React from "react";
import { StatusColors } from "theme/StatusColors"; import { StatusColors } from "theme/StatusColors";
@@ -29,7 +29,9 @@ interface StatusCellProps {
} }
export default function StatusCell({ isHeld, status }: StatusCellProps): React.JSX.Element { export default function StatusCell({ isHeld, status }: StatusCellProps): React.JSX.Element {
return <Chip return <Tooltip title={isHeld ? "Held: automatic updates are skipped" : ""}>
<Chip
aria-label={isHeld ? `${status}; held: automatic updates are skipped` : status}
icon={isHeld ? <PauseCircleIcon /> : undefined} icon={isHeld ? <PauseCircleIcon /> : undefined}
label={status} label={status}
size="small" size="small"
@@ -38,5 +40,6 @@ export default function StatusCell({ isHeld, status }: StatusCellProps): React.J
color: "common.white", color: "common.white",
fontWeight: 500, fontWeight: 500,
}} }}
/>; />
</Tooltip>;
} }
+9
View File
@@ -18,6 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { createTheme, type Theme } from "@mui/material/styles"; import { createTheme, type Theme } from "@mui/material/styles";
import type {} from "@mui/x-data-grid/themeAugmentation";
export function createAppTheme(mode: "light" | "dark"): Theme { export function createAppTheme(mode: "light" | "dark"): Theme {
return createTheme({ return createTheme({
@@ -30,6 +31,14 @@ export function createAppTheme(mode: "light" | "dark"): Theme {
}, },
}, },
}, },
MuiDataGrid: {
styleOverrides: {
cell: {
alignItems: "center",
display: "flex",
},
},
},
MuiDialog: { MuiDialog: {
defaultProps: { defaultProps: {
fullWidth: true, fullWidth: true,