mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-04-07 02:53:38 +00:00
Compare commits
21 Commits
2.20.0rc7
...
3ad2c494af
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ad2c494af | |||
| 34014d1cdd | |||
| 93ed2b864b | |||
| cca931ccd0 | |||
| 5e090cebdb | |||
| d7984c12f0 | |||
| 96684d7ddd | |||
| 2e9837b70d | |||
| ac4a8fb2cd | |||
| 1db8eb0ac4 | |||
| dc394f7df9 | |||
| 058f784b05 | |||
| f688768ca7 | |||
| a09ad7617d | |||
| 81aeb56ba3 | |||
| 2cd4ef5e86 | |||
| 998ed48dde | |||
| 021d88dc4c | |||
| 9012ee7144 | |||
| 945ddb2942 | |||
| 9cd0926588 |
@@ -12,6 +12,14 @@ ahriman.application.handlers.add module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.application.handlers.archives module
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.application.handlers.archives
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.application.handlers.backup module
|
ahriman.application.handlers.backup module
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
|
|
||||||
@@ -76,6 +84,14 @@ ahriman.application.handlers.help module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.application.handlers.hold module
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.application.handlers.hold
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.application.handlers.key\_import module
|
ahriman.application.handlers.key\_import module
|
||||||
-----------------------------------------------
|
-----------------------------------------------
|
||||||
|
|
||||||
@@ -92,6 +108,14 @@ ahriman.application.handlers.patch module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.application.handlers.pkgbuild module
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.application.handlers.pkgbuild
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.application.handlers.rebuild module
|
ahriman.application.handlers.rebuild module
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
|
|
||||||
@@ -140,6 +164,14 @@ ahriman.application.handlers.restore module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.application.handlers.rollback module
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.application.handlers.rollback
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.application.handlers.run module
|
ahriman.application.handlers.run module
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ ahriman.core.alpm.pacman\_database module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.alpm.pacman\_handle module
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.alpm.pacman_handle
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.core.alpm.pkgbuild\_parser module
|
ahriman.core.alpm.pkgbuild\_parser module
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,22 @@ ahriman.core.database.migrations.m016\_archive module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.database.migrations.m017\_pkgbuild module
|
||||||
|
------------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.database.migrations.m017_pkgbuild
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.database.migrations.m018\_package\_hold module
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.database.migrations.m018_package_hold
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
Module contents
|
Module contents
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,14 @@ ahriman.core.formatters.patch\_printer module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.formatters.pkgbuild\_printer module
|
||||||
|
------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.formatters.pkgbuild_printer
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.core.formatters.printer module
|
ahriman.core.formatters.printer module
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,14 @@ ahriman.web.schemas.file\_schema module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.schemas.hold\_schema module
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.schemas.hold_schema
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.schemas.info\_schema module
|
ahriman.web.schemas.info\_schema module
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
|
||||||
@@ -244,6 +252,14 @@ ahriman.web.schemas.package\_version\_schema module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.schemas.packager\_schema module
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.schemas.packager_schema
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.schemas.pagination\_schema module
|
ahriman.web.schemas.pagination\_schema module
|
||||||
---------------------------------------------
|
---------------------------------------------
|
||||||
|
|
||||||
@@ -324,6 +340,14 @@ ahriman.web.schemas.repository\_stats\_schema module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.schemas.rollback\_schema module
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.schemas.rollback_schema
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.schemas.search\_schema module
|
ahriman.web.schemas.search\_schema module
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ ahriman.web.views.v1.packages package
|
|||||||
Submodules
|
Submodules
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
ahriman.web.views.v1.packages.archives module
|
||||||
|
---------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.views.v1.packages.archives
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.views.v1.packages.changes module
|
ahriman.web.views.v1.packages.changes module
|
||||||
--------------------------------------------
|
--------------------------------------------
|
||||||
|
|
||||||
@@ -20,6 +28,14 @@ ahriman.web.views.v1.packages.dependencies module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.views.v1.packages.hold module
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.views.v1.packages.hold
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.views.v1.packages.logs module
|
ahriman.web.views.v1.packages.logs module
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,14 @@ ahriman.web.views.v1.service.request module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.views.v1.service.rollback module
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.views.v1.service.rollback
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.views.v1.service.search module
|
ahriman.web.views.v1.service.search module
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -235,6 +235,18 @@ Remove packages
|
|||||||
|
|
||||||
This flow removes package from filesystem, updates repository database and also runs synchronization and reporting methods.
|
This flow removes package from filesystem, updates repository database and also runs synchronization and reporting methods.
|
||||||
|
|
||||||
|
Rollback packages
|
||||||
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
This flow restores a package to a previously built version:
|
||||||
|
|
||||||
|
#. Load the current package definition from the repository database.
|
||||||
|
#. Replace its version with the requested rollback target.
|
||||||
|
#. Search the archive directory for built artifacts (packages and signatures) matching the target version.
|
||||||
|
#. Add the found artifacts to the repository via the same path as ``package-add`` with ``PackageSource.Archive``.
|
||||||
|
#. Trigger an immediate update to process the added packages.
|
||||||
|
#. If ``--hold`` is enabled (the default), mark the package as held in the database to prevent automatic updates from overriding the rollback.
|
||||||
|
|
||||||
Check outdated packages
|
Check outdated packages
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
@@ -425,6 +437,15 @@ The PKGBUILD class also provides some additional functions on top of that:
|
|||||||
* Ability to extract fields defined inside ``package*()`` functions, which are in particular used for the multi-packages.
|
* Ability to extract fields defined inside ``package*()`` functions, which are in particular used for the multi-packages.
|
||||||
* Shell substitution, which supports constructions ``$var`` (including ``${var}``), ``${var#(#)pattern}``, ``${var%(%)pattern}`` and ``${var/(/)pattern/replacement}`` (including ``#pattern`` and ``%pattern``).
|
* Shell substitution, which supports constructions ``$var`` (including ``${var}``), ``${var#(#)pattern}``, ``${var%(%)pattern}`` and ``${var/(/)pattern/replacement}`` (including ``#pattern`` and ``%pattern``).
|
||||||
|
|
||||||
|
HTTP client
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
The ``ahriman.core.http`` package provides a HTTP client built on top of the ``requests`` library.
|
||||||
|
|
||||||
|
The base class ``ahriman.core.http.SyncHttpClient`` wraps ``requests.Session`` and provides common features for all HTTP interactions: configurable timeouts, retry policies with exponential backoff (using ``urllib3.util.retry.Retry``), basic authentication, custom User-Agent header, error processing, and ``make_request`` method. The session is lazily created (via ``cached_property``).
|
||||||
|
|
||||||
|
On top of that, ``ahriman.core.http.SyncAhrimanClient`` extends the base client for communication with the ahriman web service specifically. It adds automatic login on session creation (using configured credentials), ``X-Request-ID`` header injection and Unix socket transport support (via ``requests-unixsocket2``) if required.
|
||||||
|
|
||||||
Additional features
|
Additional features
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|||||||
@@ -33,3 +33,28 @@ The service provides several commands aim to do easy repository backup and resto
|
|||||||
.. code-block:: shell
|
.. code-block:: shell
|
||||||
|
|
||||||
sudo -u ahriman ahriman repo-rebuild --from-database
|
sudo -u ahriman ahriman repo-rebuild --from-database
|
||||||
|
|
||||||
|
Package rollback
|
||||||
|
================
|
||||||
|
|
||||||
|
If the ``archive.keep_built_packages`` option is enabled, the service keeps previously built package files in the archive directory. These archives can be used to rollback a package to a previous successfully built version.
|
||||||
|
|
||||||
|
#.
|
||||||
|
List available archive versions for a package:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
ahriman package-archives ahriman
|
||||||
|
|
||||||
|
#.
|
||||||
|
Rollback the package to the desired version:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sudo -u ahriman ahriman package-rollback ahriman 2.19.0-1
|
||||||
|
|
||||||
|
By default, the ``--hold`` flag is enabled, which prevents the package from being automatically updated on subsequent ``repo-update`` runs. To rollback without holding the package use:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sudo -u ahriman ahriman package-rollback ahriman 2.19.0-1 --no-hold
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import js from "@eslint/js";
|
import js from "@eslint/js";
|
||||||
import stylistic from "@stylistic/eslint-plugin";
|
import stylistic from "@stylistic/eslint-plugin";
|
||||||
|
import react from "eslint-plugin-react";
|
||||||
import reactHooks from "eslint-plugin-react-hooks";
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
import reactRefresh from "eslint-plugin-react-refresh";
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
import simpleImportSort from "eslint-plugin-simple-import-sort";
|
import simpleImportSort from "eslint-plugin-simple-import-sort";
|
||||||
@@ -8,7 +9,11 @@ import tseslint from "typescript-eslint";
|
|||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{ ignores: ["dist"] },
|
{ ignores: ["dist"] },
|
||||||
{
|
{
|
||||||
extends: [js.configs.recommended, ...tseslint.configs.recommendedTypeChecked],
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
react.configs.flat.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
],
|
||||||
files: ["src/**/*.{ts,tsx}"],
|
files: ["src/**/*.{ts,tsx}"],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
@@ -17,13 +22,14 @@ export default tseslint.config(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
|
"@stylistic": stylistic,
|
||||||
"react-hooks": reactHooks,
|
"react-hooks": reactHooks,
|
||||||
"react-refresh": reactRefresh,
|
"react-refresh": reactRefresh,
|
||||||
"simple-import-sort": simpleImportSort,
|
"simple-import-sort": simpleImportSort,
|
||||||
"@stylistic": stylistic,
|
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...reactHooks.configs.recommended.rules,
|
...reactHooks.configs.recommended.rules,
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||||
|
|
||||||
// imports
|
// imports
|
||||||
@@ -33,7 +39,7 @@ export default tseslint.config(
|
|||||||
// core
|
// core
|
||||||
"curly": "error",
|
"curly": "error",
|
||||||
"eqeqeq": "error",
|
"eqeqeq": "error",
|
||||||
"no-console": "error",
|
"no-console": ["warn", { allow: ["warn", "error"] }],
|
||||||
"no-eval": "error",
|
"no-eval": "error",
|
||||||
|
|
||||||
// stylistic
|
// stylistic
|
||||||
@@ -68,6 +74,7 @@ export default tseslint.config(
|
|||||||
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
|
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
|
||||||
"@typescript-eslint/explicit-function-return-type": ["error", { allowExpressions: true }],
|
"@typescript-eslint/explicit-function-return-type": ["error", { allowExpressions: true }],
|
||||||
"@typescript-eslint/no-deprecated": "error",
|
"@typescript-eslint/no-deprecated": "error",
|
||||||
|
"@typescript-eslint/no-floating-promises": "error",
|
||||||
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
||||||
"@typescript-eslint/prefer-nullish-coalescing": "error",
|
"@typescript-eslint/prefer-nullish-coalescing": "error",
|
||||||
"@typescript-eslint/prefer-optional-chain": "error",
|
"@typescript-eslint/prefer-optional-chain": "error",
|
||||||
|
|||||||
@@ -1,8 +1,35 @@
|
|||||||
{
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": ">=11.14.0 <11.15.0",
|
||||||
|
"@emotion/styled": ">=11.14.0 <11.15.0",
|
||||||
|
"@mui/icons-material": ">=7.3.0 <7.4.0",
|
||||||
|
"@mui/material": ">=7.3.0 <7.4.0",
|
||||||
|
"@mui/x-data-grid": ">=8.28.0 <8.29.0",
|
||||||
|
"@tanstack/react-query": ">=5.94.0 <5.95.0",
|
||||||
|
"chart.js": ">=4.5.0 <4.6.0",
|
||||||
|
"react": ">=19.2.0 <19.3.0",
|
||||||
|
"react-chartjs-2": ">=5.3.0 <5.4.0",
|
||||||
|
"react-dom": ">=19.2.0 <19.3.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-syntax-highlighter": ">=15.5.0 <15.6.0",
|
||||||
|
"@vitejs/plugin-react": ">=6.0.0 <6.1.0",
|
||||||
|
"eslint": ">=9.39.0 <9.40.0",
|
||||||
|
"eslint-plugin-react": ">=7.37.0 <7.38.0",
|
||||||
|
"eslint-plugin-react-hooks": ">=7.0.0 <7.1.0",
|
||||||
|
"eslint-plugin-react-refresh": ">=0.5.0 <0.6.0",
|
||||||
|
"eslint-plugin-simple-import-sort": ">=12.1.0 <12.2.0",
|
||||||
|
"typescript": ">=5.9.0 <5.10.0",
|
||||||
|
"typescript-eslint": ">=8.57.0 <8.58.0",
|
||||||
|
"vite": ">=8.0.0 <8.1.0"
|
||||||
|
},
|
||||||
"name": "ahriman-frontend",
|
"name": "ahriman-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
|
||||||
"version": "2.20.0-rc7",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -10,33 +37,6 @@
|
|||||||
"lint:fix": "eslint --fix src/",
|
"lint:fix": "eslint --fix src/",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"type": "module",
|
||||||
"@emotion/react": "^11.14.0",
|
"version": "2.20.0"
|
||||||
"@emotion/styled": "^11.14.1",
|
|
||||||
"@mui/icons-material": "^7.3.9",
|
|
||||||
"@mui/material": "^7.3.9",
|
|
||||||
"@mui/x-data-grid": "^8.27.4",
|
|
||||||
"@tanstack/react-query": "^5.90.21",
|
|
||||||
"chart.js": "^4.5.1",
|
|
||||||
"react": "^19.2.4",
|
|
||||||
"react-chartjs-2": "^5.3.1",
|
|
||||||
"react-dom": "^19.2.4",
|
|
||||||
"react-syntax-highlighter": "^16.1.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@eslint/js": "^9.39.3",
|
|
||||||
"@stylistic/eslint-plugin": "^5.10.0",
|
|
||||||
"@types/react": "^19.2.14",
|
|
||||||
"@types/react-dom": "^19.2.3",
|
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
|
||||||
"eslint": "^9.39.3",
|
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
||||||
"typescript": "^5.9.3",
|
|
||||||
"typescript-eslint": "^8.56.1",
|
|
||||||
"vite": "^7.3.1",
|
|
||||||
"vite-tsconfig-paths": "^6.1.1"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,30 +17,27 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* 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 CssBaseline from "@mui/material/CssBaseline";
|
|
||||||
import { ThemeProvider } from "@mui/material/styles";
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import AppLayout from "components/layout/AppLayout";
|
import AppLayout from "components/layout/AppLayout";
|
||||||
import { AuthProvider } from "contexts/AuthProvider";
|
import { AuthProvider } from "contexts/AuthProvider";
|
||||||
import { ClientProvider } from "contexts/ClientProvider";
|
import { ClientProvider } from "contexts/ClientProvider";
|
||||||
import { NotificationProvider } from "contexts/NotificationProvider";
|
import { NotificationProvider } from "contexts/NotificationProvider";
|
||||||
import { RepositoryProvider } from "contexts/RepositoryProvider";
|
import { RepositoryProvider } from "contexts/RepositoryProvider";
|
||||||
|
import { ThemeProvider } from "contexts/ThemeProvider";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import Theme from "theme/Theme";
|
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
staleTime: 30_000,
|
|
||||||
retry: 1,
|
retry: 1,
|
||||||
|
staleTime: 30_000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function App(): React.JSX.Element {
|
export default function App(): React.JSX.Element {
|
||||||
return <QueryClientProvider client={queryClient}>
|
return <QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider theme={Theme}>
|
<ThemeProvider>
|
||||||
<CssBaseline />
|
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<ClientProvider>
|
<ClientProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
|||||||
@@ -18,9 +18,10 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
export class ApiError extends Error {
|
export class ApiError extends Error {
|
||||||
|
|
||||||
|
body: string;
|
||||||
status: number;
|
status: number;
|
||||||
statusText: string;
|
statusText: string;
|
||||||
body: string;
|
|
||||||
|
|
||||||
constructor(status: number, statusText: string, body: string) {
|
constructor(status: number, statusText: string, body: string) {
|
||||||
super(`${status} ${statusText}`);
|
super(`${status} ${statusText}`);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export class Client {
|
|||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"X-Request-ID": crypto.randomUUID(),
|
"X-Request-ID": crypto.randomUUID?.() ?? Date.now().toString(),
|
||||||
};
|
};
|
||||||
if (json !== undefined) {
|
if (json !== undefined) {
|
||||||
headers["Content-Type"] = "application/json";
|
headers["Content-Type"] = "application/json";
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import type { Event } from "models/Event";
|
|||||||
import type { InfoResponse } from "models/InfoResponse";
|
import type { InfoResponse } from "models/InfoResponse";
|
||||||
import type { InternalStatus } from "models/InternalStatus";
|
import type { InternalStatus } from "models/InternalStatus";
|
||||||
import type { LogRecord } from "models/LogRecord";
|
import type { LogRecord } from "models/LogRecord";
|
||||||
|
import type { Package } from "models/Package";
|
||||||
import type { PackageStatus } from "models/PackageStatus";
|
import type { PackageStatus } from "models/PackageStatus";
|
||||||
import type { Patch } from "models/Patch";
|
import type { Patch } from "models/Patch";
|
||||||
import { RepositoryId } from "models/RepositoryId";
|
import { RepositoryId } from "models/RepositoryId";
|
||||||
@@ -42,6 +43,12 @@ export class FetchClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchPackageArtifacts(packageBase: string, repository: RepositoryId): Promise<Package[]> {
|
||||||
|
return this.client.request<Package[]>(`/api/v1/packages/${encodeURIComponent(packageBase)}/archives`, {
|
||||||
|
query: repository.toQuery(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async fetchPackageChanges(packageBase: string, repository: RepositoryId): Promise<Changes> {
|
async fetchPackageChanges(packageBase: string, repository: RepositoryId): Promise<Changes> {
|
||||||
return this.client.request<Changes>(`/api/v1/packages/${encodeURIComponent(packageBase)}/changes`, {
|
return this.client.request<Changes>(`/api/v1/packages/${encodeURIComponent(packageBase)}/changes`, {
|
||||||
query: repository.toQuery(),
|
query: repository.toQuery(),
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
export interface RequestOptions {
|
export interface RequestOptions {
|
||||||
|
json?: unknown;
|
||||||
method?: string;
|
method?: string;
|
||||||
query?: Record<string, string | number | boolean>;
|
query?: Record<string, string | number | boolean>;
|
||||||
json?: unknown;
|
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import type { PackageActionRequest } from "models/PackageActionRequest";
|
|||||||
import type { PGPKey } from "models/PGPKey";
|
import type { PGPKey } from "models/PGPKey";
|
||||||
import type { PGPKeyRequest } from "models/PGPKeyRequest";
|
import type { PGPKeyRequest } from "models/PGPKeyRequest";
|
||||||
import type { RepositoryId } from "models/RepositoryId";
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
import type { RollbackRequest } from "models/RollbackRequest";
|
||||||
|
|
||||||
export class ServiceClient {
|
export class ServiceClient {
|
||||||
|
|
||||||
@@ -36,6 +37,14 @@ export class ServiceClient {
|
|||||||
return this.client.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 servicePackageHold(packageBase: string, repository: RepositoryId, isHeld: boolean): Promise<void> {
|
||||||
|
return this.client.request(`/api/v1/packages/${encodeURIComponent(packageBase)}/hold`, {
|
||||||
|
method: "POST",
|
||||||
|
query: repository.toQuery(),
|
||||||
|
json: { is_held: isHeld },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async servicePackagePatchRemove(packageBase: string, key: string): Promise<void> {
|
async servicePackagePatchRemove(packageBase: string, key: string): Promise<void> {
|
||||||
return this.client.request(`/api/v1/packages/${encodeURIComponent(packageBase)}/patches/${encodeURIComponent(key)}`, {
|
return this.client.request(`/api/v1/packages/${encodeURIComponent(packageBase)}/patches/${encodeURIComponent(key)}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
@@ -58,6 +67,14 @@ export class ServiceClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async servicePackageRollback(repository: RepositoryId, data: RollbackRequest): Promise<void> {
|
||||||
|
return this.client.request("/api/v1/service/rollback", {
|
||||||
|
method: "POST",
|
||||||
|
query: repository.toQuery(),
|
||||||
|
json: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async servicePackageSearch(query: string): Promise<AURPackage[]> {
|
async servicePackageSearch(query: string): Promise<AURPackage[]> {
|
||||||
return this.client.request<AURPackage[]>("/api/v1/service/search", { query: { for: query } });
|
return this.client.request<AURPackage[]>("/api/v1/service/search", { query: { for: query } });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* 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 { blue } from "@mui/material/colors";
|
||||||
import type { Event } from "models/Event";
|
import type { Event } from "models/Event";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { Line } from "react-chartjs-2";
|
import { Line } from "react-chartjs-2";
|
||||||
@@ -31,9 +32,11 @@ export default function EventDurationLineChart({ events }: EventDurationLineChar
|
|||||||
labels: updateEvents.map(event => new Date(event.created * 1000).toISOStringShort()),
|
labels: updateEvents.map(event => new Date(event.created * 1000).toISOStringShort()),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: "update duration, s",
|
backgroundColor: blue[200],
|
||||||
data: updateEvents.map(event => event.data?.took ?? 0),
|
borderColor: blue[500],
|
||||||
cubicInterpolationMode: "monotone" as const,
|
cubicInterpolationMode: "monotone" as const,
|
||||||
|
data: updateEvents.map(event => event.data?.took ?? 0),
|
||||||
|
label: "update duration, s",
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -29,19 +29,19 @@ interface PackageCountBarChartProps {
|
|||||||
export default function PackageCountBarChart({ stats }: PackageCountBarChartProps): React.JSX.Element {
|
export default function PackageCountBarChart({ stats }: PackageCountBarChartProps): React.JSX.Element {
|
||||||
return <Bar
|
return <Bar
|
||||||
data={{
|
data={{
|
||||||
labels: ["packages"],
|
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: "bases",
|
|
||||||
data: [stats.bases ?? 0],
|
|
||||||
backgroundColor: indigo[300],
|
backgroundColor: indigo[300],
|
||||||
|
data: [stats.bases ?? 0],
|
||||||
|
label: "bases",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "archives",
|
|
||||||
data: [stats.packages ?? 0],
|
|
||||||
backgroundColor: blue[500],
|
backgroundColor: blue[500],
|
||||||
|
data: [stats.packages ?? 0],
|
||||||
|
label: "archives",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
labels: ["packages"],
|
||||||
}}
|
}}
|
||||||
options={{
|
options={{
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ interface StatusPieChartProps {
|
|||||||
export default function StatusPieChart({ counters }: StatusPieChartProps): React.JSX.Element {
|
export default function StatusPieChart({ counters }: StatusPieChartProps): React.JSX.Element {
|
||||||
const labels = ["unknown", "pending", "building", "failed", "success"] as BuildStatus[];
|
const labels = ["unknown", "pending", "building", "failed", "success"] as BuildStatus[];
|
||||||
const data = {
|
const data = {
|
||||||
labels: labels,
|
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: "packages in status",
|
|
||||||
data: labels.map(label => counters[label]),
|
|
||||||
backgroundColor: labels.map(label => StatusColors[label]),
|
backgroundColor: labels.map(label => StatusColors[label]),
|
||||||
|
data: labels.map(label => counters[label]),
|
||||||
|
label: "packages in status",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
labels: labels,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Pie data={data} options={{ responsive: true }} />;
|
return <Pie data={data} options={{ responsive: true }} />;
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
interface AutoRefreshControlProps {
|
interface AutoRefreshControlProps {
|
||||||
intervals: AutoRefreshInterval[];
|
|
||||||
currentInterval: number;
|
currentInterval: number;
|
||||||
|
intervals: AutoRefreshInterval[];
|
||||||
onIntervalChange: (interval: number) => void;
|
onIntervalChange: (interval: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutoRefreshControl({
|
export default function AutoRefreshControl({
|
||||||
intervals,
|
|
||||||
currentInterval,
|
currentInterval,
|
||||||
|
intervals,
|
||||||
onIntervalChange,
|
onIntervalChange,
|
||||||
}: AutoRefreshControlProps): React.JSX.Element | null {
|
}: AutoRefreshControlProps): React.JSX.Element | null {
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||||
@@ -46,24 +46,25 @@ export default function AutoRefreshControl({
|
|||||||
return <>
|
return <>
|
||||||
<Tooltip title="Auto-refresh">
|
<Tooltip title="Auto-refresh">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
aria-label="Auto-refresh"
|
||||||
onClick={event => setAnchorEl(event.currentTarget)}
|
|
||||||
color={enabled ? "primary" : "default"}
|
color={enabled ? "primary" : "default"}
|
||||||
|
onClick={event => setAnchorEl(event.currentTarget)}
|
||||||
|
size="small"
|
||||||
>
|
>
|
||||||
{enabled ? <TimerIcon fontSize="small" /> : <TimerOffIcon fontSize="small" />}
|
{enabled ? <TimerIcon fontSize="small" /> : <TimerOffIcon fontSize="small" />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
open={Boolean(anchorEl)}
|
|
||||||
onClose={() => setAnchorEl(null)}
|
onClose={() => setAnchorEl(null)}
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
>
|
>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
selected={!enabled}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onIntervalChange(0);
|
onIntervalChange(0);
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
}}
|
}}
|
||||||
|
selected={!enabled}
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
{!enabled && <CheckIcon fontSize="small" />}
|
{!enabled && <CheckIcon fontSize="small" />}
|
||||||
@@ -73,11 +74,11 @@ export default function AutoRefreshControl({
|
|||||||
{intervals.map(interval =>
|
{intervals.map(interval =>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={interval.interval}
|
key={interval.interval}
|
||||||
selected={enabled && interval.interval === currentInterval}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onIntervalChange(interval.interval);
|
onIntervalChange(interval.interval);
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
}}
|
}}
|
||||||
|
selected={enabled && interval.interval === currentInterval}
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
{enabled && interval.interval === currentInterval && <CheckIcon fontSize="small" />}
|
{enabled && interval.interval === currentInterval && <CheckIcon fontSize="small" />}
|
||||||
|
|||||||
@@ -17,47 +17,54 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* 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 { Box } from "@mui/material";
|
import "components/common/syntaxLanguages";
|
||||||
|
|
||||||
|
import { Box, useTheme } from "@mui/material";
|
||||||
import CopyButton from "components/common/CopyButton";
|
import CopyButton from "components/common/CopyButton";
|
||||||
|
import { useThemeMode } from "hooks/useThemeMode";
|
||||||
import React, { type RefObject } from "react";
|
import React, { type RefObject } from "react";
|
||||||
|
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
import { githubGist, vs2015 } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||||
|
|
||||||
interface CodeBlockProps {
|
interface CodeBlockProps {
|
||||||
preRef?: RefObject<HTMLElement | null>;
|
content: string;
|
||||||
getText: () => string;
|
|
||||||
height?: number | string;
|
height?: number | string;
|
||||||
|
language?: string;
|
||||||
onScroll?: () => void;
|
onScroll?: () => void;
|
||||||
wordBreak?: boolean;
|
preRef?: RefObject<HTMLElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CodeBlock({
|
export default function CodeBlock({
|
||||||
preRef,
|
content,
|
||||||
getText,
|
|
||||||
height,
|
height,
|
||||||
|
language = "text",
|
||||||
onScroll,
|
onScroll,
|
||||||
wordBreak,
|
preRef,
|
||||||
}: CodeBlockProps): React.JSX.Element {
|
}: CodeBlockProps): React.JSX.Element {
|
||||||
|
const { mode } = useThemeMode();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
return <Box sx={{ position: "relative" }}>
|
return <Box sx={{ position: "relative" }}>
|
||||||
<Box
|
<Box
|
||||||
ref={preRef}
|
|
||||||
component="pre"
|
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
sx={{
|
ref={preRef}
|
||||||
backgroundColor: "grey.100",
|
sx={{ overflow: "auto", height }}
|
||||||
p: 2,
|
|
||||||
borderRadius: 1,
|
|
||||||
overflow: "auto",
|
|
||||||
height,
|
|
||||||
fontSize: "0.8rem",
|
|
||||||
fontFamily: "monospace",
|
|
||||||
...wordBreak ? { whiteSpace: "pre-wrap", wordBreak: "break-all" } : {},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<code>
|
<SyntaxHighlighter
|
||||||
{getText()}
|
customStyle={{
|
||||||
</code>
|
borderRadius: `${theme.shape.borderRadius}px`,
|
||||||
</Box>
|
fontSize: "0.8rem",
|
||||||
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
padding: theme.spacing(2),
|
||||||
<CopyButton getText={getText} />
|
}}
|
||||||
|
language={language}
|
||||||
|
style={mode === "dark" ? vs2015 : githubGist}
|
||||||
|
wrapLongLines
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</SyntaxHighlighter>
|
||||||
</Box>
|
</Box>
|
||||||
|
{content && <Box sx={{ position: "absolute", right: 8, top: 8 }}>
|
||||||
|
<CopyButton text={content} />
|
||||||
|
</Box>}
|
||||||
</Box>;
|
</Box>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,24 +23,24 @@ import { IconButton, Tooltip } from "@mui/material";
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
interface CopyButtonProps {
|
interface CopyButtonProps {
|
||||||
getText: () => string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CopyButton({ getText }: CopyButtonProps): React.JSX.Element {
|
export default function CopyButton({ text }: CopyButtonProps): React.JSX.Element {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
|
||||||
useEffect(() => () => clearTimeout(timer.current), []);
|
useEffect(() => () => clearTimeout(timer.current), []);
|
||||||
|
|
||||||
const handleCopy: () => Promise<void> = async () => {
|
const handleCopy: () => Promise<void> = async () => {
|
||||||
await navigator.clipboard.writeText(getText());
|
await navigator.clipboard.writeText(text);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
clearTimeout(timer.current);
|
clearTimeout(timer.current);
|
||||||
timer.current = setTimeout(() => setCopied(false), 2000);
|
timer.current = setTimeout(() => setCopied(false), 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Tooltip title={copied ? "Copied!" : "Copy"}>
|
return <Tooltip title={copied ? "Copied!" : "Copy"}>
|
||||||
<IconButton size="small" aria-label={copied ? "Copied" : "Copy"} onClick={() => void handleCopy()}>
|
<IconButton aria-label={copied ? "Copied" : "Copy"} onClick={() => void handleCopy()} size="small">
|
||||||
{copied ? <CheckIcon fontSize="small" /> : <ContentCopyIcon fontSize="small" />}
|
{copied ? <CheckIcon fontSize="small" /> : <ContentCopyIcon fontSize="small" />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>;
|
</Tooltip>;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ interface DialogHeaderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DialogHeader({ children, onClose, sx }: DialogHeaderProps): React.JSX.Element {
|
export default function DialogHeader({ children, onClose, sx }: DialogHeaderProps): React.JSX.Element {
|
||||||
return <DialogTitle sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", ...sx }}>
|
return <DialogTitle sx={{ alignItems: "center", display: "flex", justifyContent: "space-between", ...sx }}>
|
||||||
{children}
|
{children}
|
||||||
<IconButton aria-label="Close" onClick={onClose} size="small" sx={{ color: "inherit" }}>
|
<IconButton aria-label="Close" onClick={onClose} size="small" sx={{ color: "inherit" }}>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
|
|||||||
@@ -35,12 +35,12 @@ export default function NotificationItem({ notification, onClose }: Notification
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slide direction="down" in={show} mountOnEnter unmountOnExit onExited={() => onClose(notification.id)}>
|
<Slide direction="down" in={show} mountOnEnter onExited={() => onClose(notification.id)} unmountOnExit>
|
||||||
<Alert
|
<Alert
|
||||||
onClose={() => setShow(false)}
|
onClose={() => setShow(false)}
|
||||||
severity={notification.severity}
|
severity={notification.severity}
|
||||||
variant="filled"
|
|
||||||
sx={{ width: "100%", pointerEvents: "auto" }}
|
sx={{ width: "100%", pointerEvents: "auto" }}
|
||||||
|
variant="filled"
|
||||||
>
|
>
|
||||||
<strong>{notification.title}</strong>
|
<strong>{notification.title}</strong>
|
||||||
{notification.message && ` - ${notification.message}`}
|
{notification.message && ` - ${notification.message}`}
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ import type React from "react";
|
|||||||
export default function RepositorySelect({
|
export default function RepositorySelect({
|
||||||
repositorySelect,
|
repositorySelect,
|
||||||
}: { repositorySelect: SelectedRepositoryResult }): React.JSX.Element {
|
}: { repositorySelect: SelectedRepositoryResult }): React.JSX.Element {
|
||||||
const { repositories, current } = useRepository();
|
const { repositories, currentRepository } = useRepository();
|
||||||
|
|
||||||
return <FormControl fullWidth margin="normal">
|
return <FormControl fullWidth margin="normal">
|
||||||
<InputLabel>repository</InputLabel>
|
<InputLabel>repository</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={repositorySelect.selectedKey || (current?.key ?? "")}
|
|
||||||
label="repository"
|
label="repository"
|
||||||
onChange={event => repositorySelect.setSelectedKey(event.target.value)}
|
onChange={event => repositorySelect.setSelectedKey(event.target.value)}
|
||||||
|
value={repositorySelect.selectedKey || (currentRepository?.key ?? "")}
|
||||||
>
|
>
|
||||||
{repositories.map(repository =>
|
{repositories.map(repository =>
|
||||||
<MenuItem key={repository.key} value={repository.key}>
|
<MenuItem key={repository.key} value={repository.key}>
|
||||||
|
|||||||
27
frontend/src/components/common/syntaxLanguages.ts
Normal file
27
frontend/src/components/common/syntaxLanguages.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Light as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
import bash from "react-syntax-highlighter/dist/esm/languages/hljs/bash";
|
||||||
|
import diff from "react-syntax-highlighter/dist/esm/languages/hljs/diff";
|
||||||
|
import plaintext from "react-syntax-highlighter/dist/esm/languages/hljs/plaintext";
|
||||||
|
|
||||||
|
SyntaxHighlighter.registerLanguage("bash", bash);
|
||||||
|
SyntaxHighlighter.registerLanguage("diff", diff);
|
||||||
|
SyntaxHighlighter.registerLanguage("text", plaintext);
|
||||||
@@ -30,23 +30,23 @@ import type React from "react";
|
|||||||
import { StatusHeaderStyles } from "theme/StatusColors";
|
import { StatusHeaderStyles } from "theme/StatusColors";
|
||||||
|
|
||||||
interface DashboardDialogProps {
|
interface DashboardDialogProps {
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
open: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DashboardDialog({ open, onClose }: DashboardDialogProps): React.JSX.Element {
|
export default function DashboardDialog({ onClose, open }: DashboardDialogProps): React.JSX.Element {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { current } = useRepository();
|
const { currentRepository } = useRepository();
|
||||||
|
|
||||||
const { data: status } = useQuery<InternalStatus>({
|
const { data: status } = useQuery<InternalStatus>({
|
||||||
queryKey: current ? QueryKeys.status(current) : ["status"],
|
|
||||||
queryFn: current ? () => client.fetch.fetchServerStatus(current) : skipToken,
|
|
||||||
enabled: open,
|
enabled: open,
|
||||||
|
queryFn: currentRepository ? () => client.fetch.fetchServerStatus(currentRepository) : skipToken,
|
||||||
|
queryKey: currentRepository ? QueryKeys.status(currentRepository) : ["status"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const headerStyle = status ? StatusHeaderStyles[status.status.status] : {};
|
const headerStyle = status ? StatusHeaderStyles[status.status.status] : {};
|
||||||
|
|
||||||
return <Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
|
return <Dialog fullWidth maxWidth="lg" onClose={onClose} open={open}>
|
||||||
<DialogHeader onClose={onClose} sx={headerStyle}>
|
<DialogHeader onClose={onClose} sx={headerStyle}>
|
||||||
System health
|
System health
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -55,43 +55,43 @@ export default function DashboardDialog({ open, onClose }: DashboardDialogProps)
|
|||||||
{status &&
|
{status &&
|
||||||
<>
|
<>
|
||||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||||
<Grid size={{ xs: 6, md: 3 }}>
|
<Grid size={{ md: 3, xs: 6 }}>
|
||||||
<Typography variant="body2" color="text.secondary" align="right">Repository name</Typography>
|
<Typography align="right" color="text.secondary" variant="body2">Repository name</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 6, md: 3 }}>
|
<Grid size={{ md: 3, xs: 6 }}>
|
||||||
<Typography variant="body2">{status.repository}</Typography>
|
<Typography variant="body2">{status.repository}</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 6, md: 3 }}>
|
<Grid size={{ md: 3, xs: 6 }}>
|
||||||
<Typography variant="body2" color="text.secondary" align="right">Repository architecture</Typography>
|
<Typography align="right" color="text.secondary" variant="body2">Repository architecture</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 6, md: 3 }}>
|
<Grid size={{ md: 3, xs: 6 }}>
|
||||||
<Typography variant="body2">{status.architecture}</Typography>
|
<Typography variant="body2">{status.architecture}</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||||
<Grid size={{ xs: 6, md: 3 }}>
|
<Grid size={{ md: 3, xs: 6 }}>
|
||||||
<Typography variant="body2" color="text.secondary" align="right">Current status</Typography>
|
<Typography align="right" color="text.secondary" variant="body2">Current status</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 6, md: 3 }}>
|
<Grid size={{ md: 3, xs: 6 }}>
|
||||||
<Typography variant="body2">{status.status.status}</Typography>
|
<Typography variant="body2">{status.status.status}</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 6, md: 3 }}>
|
<Grid size={{ md: 3, xs: 6 }}>
|
||||||
<Typography variant="body2" color="text.secondary" align="right">Updated at</Typography>
|
<Typography align="right" color="text.secondary" variant="body2">Updated at</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 6, md: 3 }}>
|
<Grid size={{ md: 3, xs: 6 }}>
|
||||||
<Typography variant="body2">{new Date(status.status.timestamp * 1000).toISOStringShort()}</Typography>
|
<Typography variant="body2">{new Date(status.status.timestamp * 1000).toISOStringShort()}</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid container spacing={2} sx={{ mt: 2 }}>
|
<Grid container spacing={2} sx={{ mt: 2 }}>
|
||||||
<Grid size={{ xs: 12, md: 6 }}>
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
<Box sx={{ height: 300 }}>
|
<Box sx={{ height: 300 }}>
|
||||||
<PackageCountBarChart stats={status.stats} />
|
<PackageCountBarChart stats={status.stats} />
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12, md: 6 }}>
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
<Box sx={{ height: 300, display: "flex", justifyContent: "center", alignItems: "center" }}>
|
<Box sx={{ alignItems: "center", display: "flex", height: 300, justifyContent: "center" }}>
|
||||||
<StatusPieChart counters={status.packages} />
|
<StatusPieChart counters={status.packages} />
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ import { useNotification } from "hooks/useNotification";
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
interface KeyImportDialogProps {
|
interface KeyImportDialogProps {
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
open: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function KeyImportDialog({ open, onClose }: KeyImportDialogProps): React.JSX.Element {
|
export default function KeyImportDialog({ onClose, open }: KeyImportDialogProps): React.JSX.Element {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { showSuccess, showError } = useNotification();
|
const { showSuccess, showError } = useNotification();
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ export default function KeyImportDialog({ open, onClose }: KeyImportDialogProps)
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFetch: () => Promise<void> = async () => {
|
const handleFetch = async (): Promise<void> => {
|
||||||
if (!fingerprint || !server) {
|
if (!fingerprint || !server) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ export default function KeyImportDialog({ open, onClose }: KeyImportDialogProps)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImport: () => Promise<void> = async () => {
|
const handleImport = async (): Promise<void> => {
|
||||||
if (!fingerprint || !server) {
|
if (!fingerprint || !server) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -81,38 +81,38 @@ export default function KeyImportDialog({ open, onClose }: KeyImportDialogProps)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Dialog open={open} onClose={handleClose} maxWidth="lg" fullWidth>
|
return <Dialog fullWidth maxWidth="lg" onClose={handleClose} open={open}>
|
||||||
<DialogHeader onClose={handleClose}>
|
<DialogHeader onClose={handleClose}>
|
||||||
Import key from PGP server
|
Import key from PGP server
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TextField
|
<TextField
|
||||||
label="fingerprint"
|
|
||||||
placeholder="PGP key fingerprint"
|
|
||||||
fullWidth
|
fullWidth
|
||||||
|
label="fingerprint"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
value={fingerprint}
|
|
||||||
onChange={event => setFingerprint(event.target.value)}
|
onChange={event => setFingerprint(event.target.value)}
|
||||||
|
placeholder="PGP key fingerprint"
|
||||||
|
value={fingerprint}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="key server"
|
|
||||||
placeholder="PGP key server"
|
|
||||||
fullWidth
|
fullWidth
|
||||||
|
label="key server"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
value={server}
|
|
||||||
onChange={event => setServer(event.target.value)}
|
onChange={event => setServer(event.target.value)}
|
||||||
|
placeholder="PGP key server"
|
||||||
|
value={server}
|
||||||
/>
|
/>
|
||||||
{keyBody &&
|
{keyBody &&
|
||||||
<Box sx={{ mt: 2 }}>
|
<Box sx={{ mt: 2 }}>
|
||||||
<CodeBlock getText={() => keyBody} height={300} />
|
<CodeBlock height={300} content={keyBody} />
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => void handleImport()} variant="contained" startIcon={<PlayArrowIcon />}>import</Button>
|
<Button onClick={() => void handleImport()} startIcon={<PlayArrowIcon />} variant="contained">import</Button>
|
||||||
<Button onClick={() => void handleFetch()} variant="contained" color="success" startIcon={<RefreshIcon />}>fetch</Button>
|
<Button color="success" onClick={() => void handleFetch()} startIcon={<RefreshIcon />} variant="contained">fetch</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>;
|
</Dialog>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ import { useNotification } from "hooks/useNotification";
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
interface LoginDialogProps {
|
interface LoginDialogProps {
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
open: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LoginDialog({ open, onClose }: LoginDialogProps): React.JSX.Element {
|
export default function LoginDialog({ onClose, open }: LoginDialogProps): React.JSX.Element {
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
@@ -54,7 +54,7 @@ export default function LoginDialog({ open, onClose }: LoginDialogProps): React.
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit: () => Promise<void> = async () => {
|
const handleSubmit = async (): Promise<void> => {
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -72,26 +72,24 @@ export default function LoginDialog({ open, onClose }: LoginDialogProps): React.
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Dialog open={open} onClose={handleClose} maxWidth="xs" fullWidth>
|
return <Dialog fullWidth maxWidth="xs" onClose={handleClose} open={open}>
|
||||||
<DialogHeader onClose={handleClose}>
|
<DialogHeader onClose={handleClose}>
|
||||||
Login
|
Login
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TextField
|
<TextField
|
||||||
label="username"
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
value={username}
|
|
||||||
onChange={event => setUsername(event.target.value)}
|
|
||||||
autoFocus
|
autoFocus
|
||||||
|
fullWidth
|
||||||
|
label="username"
|
||||||
|
margin="normal"
|
||||||
|
onChange={event => setUsername(event.target.value)}
|
||||||
|
value={username}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="password"
|
|
||||||
fullWidth
|
fullWidth
|
||||||
|
label="password"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
type={showPassword ? "text" : "password"}
|
|
||||||
value={password}
|
|
||||||
onChange={event => setPassword(event.target.value)}
|
onChange={event => setPassword(event.target.value)}
|
||||||
onKeyDown={event => {
|
onKeyDown={event => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
@@ -102,17 +100,24 @@ export default function LoginDialog({ open, onClose }: LoginDialogProps): React.
|
|||||||
input: {
|
input: {
|
||||||
endAdornment:
|
endAdornment:
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
<IconButton aria-label={showPassword ? "Hide password" : "Show password"} onClick={() => setShowPassword(!showPassword)} edge="end" size="small">
|
<IconButton
|
||||||
|
aria-label={showPassword ? "Hide password" : "Show password"}
|
||||||
|
edge="end"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
{showPassword ? <VisibilityOffIcon /> : <VisibilityIcon />}
|
{showPassword ? <VisibilityOffIcon /> : <VisibilityIcon />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>,
|
</InputAdornment>,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
value={password}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => void handleSubmit()} variant="contained" startIcon={<PersonIcon />}>login</Button>
|
<Button onClick={() => void handleSubmit()} startIcon={<PersonIcon />} variant="contained">login</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>;
|
</Dialog>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,11 +52,11 @@ interface EnvironmentVariable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface PackageAddDialogProps {
|
interface PackageAddDialogProps {
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
open: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PackageAddDialog({ open, onClose }: PackageAddDialogProps): React.JSX.Element {
|
export default function PackageAddDialog({ onClose, open }: PackageAddDialogProps): React.JSX.Element {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { showSuccess, showError } = useNotification();
|
const { showSuccess, showError } = useNotification();
|
||||||
const repositorySelect = useSelectedRepository();
|
const repositorySelect = useSelectedRepository();
|
||||||
@@ -77,9 +77,9 @@ export default function PackageAddDialog({ open, onClose }: PackageAddDialogProp
|
|||||||
const debouncedSearch = useDebounce(packageName, 500);
|
const debouncedSearch = useDebounce(packageName, 500);
|
||||||
|
|
||||||
const { data: searchResults = [] } = useQuery<AURPackage[]>({
|
const { data: searchResults = [] } = useQuery<AURPackage[]>({
|
||||||
queryKey: QueryKeys.search(debouncedSearch),
|
|
||||||
queryFn: () => client.service.servicePackageSearch(debouncedSearch),
|
|
||||||
enabled: debouncedSearch.length >= 3,
|
enabled: debouncedSearch.length >= 3,
|
||||||
|
queryFn: () => client.service.servicePackageSearch(debouncedSearch),
|
||||||
|
queryKey: QueryKeys.search(debouncedSearch),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = async (action: "add" | "request"): Promise<void> => {
|
const handleSubmit = async (action: "add" | "request"): Promise<void> => {
|
||||||
@@ -107,7 +107,7 @@ export default function PackageAddDialog({ open, onClose }: PackageAddDialogProp
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
|
return <Dialog fullWidth maxWidth="md" onClose={handleClose} open={open}>
|
||||||
<DialogHeader onClose={handleClose}>
|
<DialogHeader onClose={handleClose}>
|
||||||
Add new packages
|
Add new packages
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -117,20 +117,18 @@ export default function PackageAddDialog({ open, onClose }: PackageAddDialogProp
|
|||||||
|
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
freeSolo
|
freeSolo
|
||||||
options={searchResults.map(pkg => pkg.package)}
|
|
||||||
inputValue={packageName}
|
inputValue={packageName}
|
||||||
onInputChange={(_, value) => setPackageName(value)}
|
onInputChange={(_, value) => setPackageName(value)}
|
||||||
|
options={searchResults.map(pkg => pkg.package)}
|
||||||
|
renderInput={params =>
|
||||||
|
<TextField {...params} label="package" margin="normal" placeholder="AUR package" />
|
||||||
|
}
|
||||||
renderOption={(props, option) => {
|
renderOption={(props, option) => {
|
||||||
const pkg = searchResults.find(pkg => pkg.package === option);
|
const pkg = searchResults.find(pkg => pkg.package === option);
|
||||||
return (
|
return <li {...props} key={option}>
|
||||||
<li {...props} key={option}>
|
|
||||||
{option}{pkg ? ` (${pkg.description})` : ""}
|
{option}{pkg ? ` (${pkg.description})` : ""}
|
||||||
</li>
|
</li>;
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
renderInput={params =>
|
|
||||||
<TextField {...params} label="package" placeholder="AUR package" margin="normal" />
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
@@ -140,45 +138,50 @@ export default function PackageAddDialog({ open, onClose }: PackageAddDialogProp
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
|
||||||
startIcon={<AddIcon />}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const id = variableIdCounter.current++;
|
const id = variableIdCounter.current++;
|
||||||
setEnvironmentVariables(prev => [...prev, { id, key: "", value: "" }]);
|
setEnvironmentVariables(prev => [...prev, { id, key: "", value: "" }]);
|
||||||
}}
|
}}
|
||||||
|
startIcon={<AddIcon />}
|
||||||
sx={{ mt: 1 }}
|
sx={{ mt: 1 }}
|
||||||
|
variant="outlined"
|
||||||
>
|
>
|
||||||
add environment variable
|
add environment variable
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{environmentVariables.map(variable =>
|
{environmentVariables.map(variable =>
|
||||||
<Box key={variable.id} sx={{ display: "flex", gap: 1, mt: 1, alignItems: "center" }}>
|
<Box key={variable.id} sx={{ alignItems: "center", display: "flex", gap: 1, mt: 1 }}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
|
||||||
placeholder="name"
|
|
||||||
value={variable.key}
|
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
const newKey = event.target.value;
|
const newKey = event.target.value;
|
||||||
setEnvironmentVariables(prev =>
|
setEnvironmentVariables(prev =>
|
||||||
prev.map(entry => entry.id === variable.id ? { ...entry, key: newKey } : entry),
|
prev.map(entry => entry.id === variable.id ? { ...entry, key: newKey } : entry),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
placeholder="name"
|
||||||
|
size="small"
|
||||||
sx={{ flex: 1 }}
|
sx={{ flex: 1 }}
|
||||||
|
value={variable.key}
|
||||||
/>
|
/>
|
||||||
<Box>=</Box>
|
<Box>=</Box>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
|
||||||
placeholder="value"
|
placeholder="value"
|
||||||
value={variable.value}
|
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
const newValue = event.target.value;
|
const newValue = event.target.value;
|
||||||
setEnvironmentVariables(prev =>
|
setEnvironmentVariables(prev =>
|
||||||
prev.map(entry => entry.id === variable.id ? { ...entry, value: newValue } : entry),
|
prev.map(entry => entry.id === variable.id ? { ...entry, value: newValue } : entry),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
size="small"
|
||||||
sx={{ flex: 1 }}
|
sx={{ flex: 1 }}
|
||||||
|
value={variable.value}
|
||||||
/>
|
/>
|
||||||
<IconButton size="small" color="error" aria-label="Remove variable" onClick={() => setEnvironmentVariables(prev => prev.filter(entry => entry.id !== variable.id))}>
|
<IconButton
|
||||||
|
aria-label="Remove variable"
|
||||||
|
color="error"
|
||||||
|
onClick={() => setEnvironmentVariables(prev => prev.filter(entry => entry.id !== variable.id))}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>,
|
</Box>,
|
||||||
@@ -186,8 +189,8 @@ export default function PackageAddDialog({ open, onClose }: PackageAddDialogProp
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => void handleSubmit("add")} variant="contained" startIcon={<PlayArrowIcon />}>add</Button>
|
<Button onClick={() => void handleSubmit("add")} startIcon={<PlayArrowIcon />} variant="contained">add</Button>
|
||||||
<Button onClick={() => void handleSubmit("request")} variant="contained" color="success" startIcon={<AddIcon />}>request</Button>
|
<Button color="success" onClick={() => void handleSubmit("request")} startIcon={<AddIcon />} variant="contained">request</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>;
|
</Dialog>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,15 @@ import { Box, Dialog, DialogContent, Tab, Tabs } from "@mui/material";
|
|||||||
import { skipToken, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { skipToken, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { ApiError } from "api/client/ApiError";
|
import { ApiError } from "api/client/ApiError";
|
||||||
import DialogHeader from "components/common/DialogHeader";
|
import DialogHeader from "components/common/DialogHeader";
|
||||||
|
import ArtifactsTab from "components/package/ArtifactsTab";
|
||||||
import BuildLogsTab from "components/package/BuildLogsTab";
|
import BuildLogsTab from "components/package/BuildLogsTab";
|
||||||
import ChangesTab from "components/package/ChangesTab";
|
import ChangesTab from "components/package/ChangesTab";
|
||||||
import EventsTab from "components/package/EventsTab";
|
import EventsTab from "components/package/EventsTab";
|
||||||
import PackageDetailsGrid from "components/package/PackageDetailsGrid";
|
import PackageDetailsGrid from "components/package/PackageDetailsGrid";
|
||||||
import PackageInfoActions from "components/package/PackageInfoActions";
|
import PackageInfoActions from "components/package/PackageInfoActions";
|
||||||
import PackagePatchesList from "components/package/PackagePatchesList";
|
import PackagePatchesList from "components/package/PackagePatchesList";
|
||||||
|
import PkgbuildTab from "components/package/PkgbuildTab";
|
||||||
|
import { type TabKey, tabs } from "components/package/TabKey";
|
||||||
import { QueryKeys } from "hooks/QueryKeys";
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
import { useAuth } from "hooks/useAuth";
|
import { useAuth } from "hooks/useAuth";
|
||||||
import { useAutoRefresh } from "hooks/useAutoRefresh";
|
import { useAutoRefresh } from "hooks/useAutoRefresh";
|
||||||
@@ -42,20 +45,20 @@ import { StatusHeaderStyles } from "theme/StatusColors";
|
|||||||
import { defaultInterval } from "utils";
|
import { defaultInterval } from "utils";
|
||||||
|
|
||||||
interface PackageInfoDialogProps {
|
interface PackageInfoDialogProps {
|
||||||
packageBase: string | null;
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
autoRefreshIntervals: AutoRefreshInterval[];
|
autoRefreshIntervals: AutoRefreshInterval[];
|
||||||
|
onClose: () => void;
|
||||||
|
open: boolean;
|
||||||
|
packageBase: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PackageInfoDialog({
|
export default function PackageInfoDialog({
|
||||||
packageBase,
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
autoRefreshIntervals,
|
autoRefreshIntervals,
|
||||||
|
onClose,
|
||||||
|
open,
|
||||||
|
packageBase,
|
||||||
}: PackageInfoDialogProps): React.JSX.Element {
|
}: PackageInfoDialogProps): React.JSX.Element {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { current } = useRepository();
|
const { currentRepository } = useRepository();
|
||||||
const { isAuthorized } = useAuth();
|
const { isAuthorized } = useAuth();
|
||||||
const { showSuccess, showError } = useNotification();
|
const { showSuccess, showError } = useNotification();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -65,11 +68,11 @@ export default function PackageInfoDialog({
|
|||||||
setLocalPackageBase(packageBase);
|
setLocalPackageBase(packageBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [tabIndex, setTabIndex] = useState(0);
|
const [activeTab, setActiveTab] = useState<TabKey>("logs");
|
||||||
const [refreshDatabase, setRefreshDatabase] = useState(true);
|
const [refreshDatabase, setRefreshDatabase] = useState(true);
|
||||||
|
|
||||||
const handleClose = (): void => {
|
const handleClose = (): void => {
|
||||||
setTabIndex(0);
|
setActiveTab("logs");
|
||||||
setRefreshDatabase(true);
|
setRefreshDatabase(true);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
@@ -77,48 +80,50 @@ export default function PackageInfoDialog({
|
|||||||
const autoRefresh = useAutoRefresh("package-info-autoreload-button", defaultInterval(autoRefreshIntervals));
|
const autoRefresh = useAutoRefresh("package-info-autoreload-button", defaultInterval(autoRefreshIntervals));
|
||||||
|
|
||||||
const { data: packageData } = useQuery<PackageStatus[]>({
|
const { data: packageData } = useQuery<PackageStatus[]>({
|
||||||
queryKey: localPackageBase && current ? QueryKeys.package(localPackageBase, current) : ["packages"],
|
|
||||||
queryFn: localPackageBase && current ? () => client.fetch.fetchPackage(localPackageBase, current) : skipToken,
|
|
||||||
enabled: open,
|
enabled: open,
|
||||||
|
queryFn: localPackageBase && currentRepository ?
|
||||||
|
() => client.fetch.fetchPackage(localPackageBase, currentRepository) : skipToken,
|
||||||
|
queryKey: localPackageBase && currentRepository ? QueryKeys.package(localPackageBase, currentRepository) : ["packages"],
|
||||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: dependencies } = useQuery<Dependencies>({
|
const { data: dependencies } = useQuery<Dependencies>({
|
||||||
queryKey: localPackageBase && current ? QueryKeys.dependencies(localPackageBase, current) : ["dependencies"],
|
|
||||||
queryFn: localPackageBase && current
|
|
||||||
? () => client.fetch.fetchPackageDependencies(localPackageBase, current) : skipToken,
|
|
||||||
enabled: open,
|
enabled: open,
|
||||||
|
queryFn: localPackageBase && currentRepository ?
|
||||||
|
() => client.fetch.fetchPackageDependencies(localPackageBase, currentRepository) : skipToken,
|
||||||
|
queryKey: localPackageBase && currentRepository ? QueryKeys.dependencies(localPackageBase, currentRepository) : ["dependencies"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: patches = [] } = useQuery<Patch[]>({
|
const { data: patches = [] } = useQuery<Patch[]>({
|
||||||
queryKey: localPackageBase ? QueryKeys.patches(localPackageBase) : ["patches"],
|
|
||||||
queryFn: localPackageBase ? () => client.fetch.fetchPackagePatches(localPackageBase) : skipToken,
|
|
||||||
enabled: open,
|
enabled: open,
|
||||||
|
queryFn: localPackageBase ? () => client.fetch.fetchPackagePatches(localPackageBase) : skipToken,
|
||||||
|
queryKey: localPackageBase ? QueryKeys.patches(localPackageBase) : ["patches"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const description: PackageStatus | undefined = packageData?.[0];
|
const description = packageData?.[0];
|
||||||
const pkg = description?.package;
|
const pkg = description?.package;
|
||||||
const status = description?.status;
|
const status = description?.status;
|
||||||
const headerStyle = status ? StatusHeaderStyles[status.status] : {};
|
const headerStyle = status ? StatusHeaderStyles[status.status] : {};
|
||||||
|
|
||||||
const handleUpdate: () => Promise<void> = async () => {
|
const handleUpdate = async (): Promise<void> => {
|
||||||
if (!localPackageBase || !current) {
|
if (!localPackageBase || !currentRepository) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await client.service.servicePackageAdd(current, { packages: [localPackageBase], refresh: refreshDatabase });
|
await client.service.servicePackageAdd(
|
||||||
|
currentRepository, { packages: [localPackageBase], refresh: refreshDatabase });
|
||||||
showSuccess("Success", `Run update for packages ${localPackageBase}`);
|
showSuccess("Success", `Run update for packages ${localPackageBase}`);
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
showError("Action failed", `Package update failed: ${ApiError.errorDetail(exception)}`);
|
showError("Action failed", `Package update failed: ${ApiError.errorDetail(exception)}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove: () => Promise<void> = async () => {
|
const handleRemove = async (): Promise<void> => {
|
||||||
if (!localPackageBase || !current) {
|
if (!localPackageBase || !currentRepository) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await client.service.servicePackageRemove(current, [localPackageBase]);
|
await client.service.servicePackageRemove(currentRepository, [localPackageBase]);
|
||||||
showSuccess("Success", `Packages ${localPackageBase} have been removed`);
|
showSuccess("Success", `Packages ${localPackageBase} have been removed`);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
@@ -126,7 +131,20 @@ export default function PackageInfoDialog({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeletePatch: (key: string) => Promise<void> = async key => {
|
const handleHoldToggle = async (): Promise<void> => {
|
||||||
|
if (!localPackageBase || !currentRepository) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const newHeldStatus = !(status?.is_held ?? false);
|
||||||
|
await client.service.servicePackageHold(localPackageBase, currentRepository, newHeldStatus);
|
||||||
|
void queryClient.invalidateQueries({ queryKey: QueryKeys.package(localPackageBase, currentRepository) });
|
||||||
|
} catch (exception) {
|
||||||
|
showError("Action failed", `Could not update hold status: ${ApiError.errorDetail(exception)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletePatch = async (key: string): Promise<void> => {
|
||||||
if (!localPackageBase) {
|
if (!localPackageBase) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -138,7 +156,7 @@ export default function PackageInfoDialog({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Dialog open={open} onClose={handleClose} maxWidth="lg" fullWidth>
|
return <Dialog fullWidth maxWidth="lg" onClose={handleClose} open={open}>
|
||||||
<DialogHeader onClose={handleClose} sx={headerStyle}>
|
<DialogHeader onClose={handleClose} sx={headerStyle}>
|
||||||
{pkg && status
|
{pkg && status
|
||||||
? `${pkg.base} ${status.status} at ${new Date(status.timestamp * 1000).toISOStringShort()}`
|
? `${pkg.base} ${status.status} at ${new Date(status.timestamp * 1000).toISOStringShort()}`
|
||||||
@@ -148,47 +166,57 @@ export default function PackageInfoDialog({
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
{pkg &&
|
{pkg &&
|
||||||
<>
|
<>
|
||||||
<PackageDetailsGrid pkg={pkg} dependencies={dependencies} />
|
<PackageDetailsGrid dependencies={dependencies} pkg={pkg} />
|
||||||
<PackagePatchesList
|
<PackagePatchesList
|
||||||
patches={patches}
|
|
||||||
editable={isAuthorized}
|
editable={isAuthorized}
|
||||||
onDelete={key => void handleDeletePatch(key)}
|
onDelete={key => void handleDeletePatch(key)}
|
||||||
|
patches={patches}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box sx={{ borderBottom: 1, borderColor: "divider", mt: 2 }}>
|
<Box sx={{ borderBottom: 1, borderColor: "divider", mt: 2 }}>
|
||||||
<Tabs value={tabIndex} onChange={(_, index: number) => setTabIndex(index)}>
|
<Tabs onChange={(_, tab: TabKey) => setActiveTab(tab)} value={activeTab}>
|
||||||
<Tab label="Build logs" />
|
{tabs.map(({ key, label }) => <Tab key={key} label={label} value={key} />)}
|
||||||
<Tab label="Changes" />
|
|
||||||
<Tab label="Events" />
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{tabIndex === 0 && localPackageBase && current &&
|
{activeTab === "logs" && localPackageBase && currentRepository &&
|
||||||
<BuildLogsTab
|
<BuildLogsTab
|
||||||
packageBase={localPackageBase}
|
packageBase={localPackageBase}
|
||||||
repository={current}
|
|
||||||
refreshInterval={autoRefresh.interval}
|
refreshInterval={autoRefresh.interval}
|
||||||
|
repository={currentRepository}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{tabIndex === 1 && localPackageBase && current &&
|
{activeTab === "changes" && localPackageBase && currentRepository &&
|
||||||
<ChangesTab packageBase={localPackageBase} repository={current} />
|
<ChangesTab packageBase={localPackageBase} repository={currentRepository} />
|
||||||
}
|
}
|
||||||
{tabIndex === 2 && localPackageBase && current &&
|
{activeTab === "pkgbuild" && localPackageBase && currentRepository &&
|
||||||
<EventsTab packageBase={localPackageBase} repository={current} />
|
<PkgbuildTab packageBase={localPackageBase} repository={currentRepository} />
|
||||||
|
}
|
||||||
|
{activeTab === "events" && localPackageBase && currentRepository &&
|
||||||
|
<EventsTab packageBase={localPackageBase} repository={currentRepository} />
|
||||||
|
}
|
||||||
|
{activeTab === "artifacts" && localPackageBase && currentRepository &&
|
||||||
|
<ArtifactsTab
|
||||||
|
currentVersion={pkg.version}
|
||||||
|
packageBase={localPackageBase}
|
||||||
|
repository={currentRepository}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<PackageInfoActions
|
<PackageInfoActions
|
||||||
isAuthorized={isAuthorized}
|
|
||||||
refreshDatabase={refreshDatabase}
|
|
||||||
onRefreshDatabaseChange={setRefreshDatabase}
|
|
||||||
onUpdate={() => void handleUpdate()}
|
|
||||||
onRemove={() => void handleRemove()}
|
|
||||||
autoRefreshIntervals={autoRefreshIntervals}
|
|
||||||
autoRefreshInterval={autoRefresh.interval}
|
autoRefreshInterval={autoRefresh.interval}
|
||||||
|
autoRefreshIntervals={autoRefreshIntervals}
|
||||||
|
isAuthorized={isAuthorized}
|
||||||
|
isHeld={status?.is_held ?? false}
|
||||||
onAutoRefreshIntervalChange={autoRefresh.setInterval}
|
onAutoRefreshIntervalChange={autoRefresh.setInterval}
|
||||||
|
onHoldToggle={() => void handleHoldToggle()}
|
||||||
|
onRefreshDatabaseChange={setRefreshDatabase}
|
||||||
|
onRemove={() => void handleRemove()}
|
||||||
|
onUpdate={() => void handleUpdate()}
|
||||||
|
refreshDatabase={refreshDatabase}
|
||||||
/>
|
/>
|
||||||
</Dialog>;
|
</Dialog>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ import { useSelectedRepository } from "hooks/useSelectedRepository";
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
interface PackageRebuildDialogProps {
|
interface PackageRebuildDialogProps {
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
open: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PackageRebuildDialog({ open, onClose }: PackageRebuildDialogProps): React.JSX.Element {
|
export default function PackageRebuildDialog({ onClose, open }: PackageRebuildDialogProps): React.JSX.Element {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { showSuccess, showError } = useNotification();
|
const { showSuccess, showError } = useNotification();
|
||||||
const repositorySelect = useSelectedRepository();
|
const repositorySelect = useSelectedRepository();
|
||||||
@@ -45,7 +45,7 @@ export default function PackageRebuildDialog({ open, onClose }: PackageRebuildDi
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRebuild: () => Promise<void> = async () => {
|
const handleRebuild = async (): Promise<void> => {
|
||||||
if (!dependency) {
|
if (!dependency) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ export default function PackageRebuildDialog({ open, onClose }: PackageRebuildDi
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
|
return <Dialog fullWidth maxWidth="md" onClose={handleClose} open={open}>
|
||||||
<DialogHeader onClose={handleClose}>
|
<DialogHeader onClose={handleClose}>
|
||||||
Rebuild depending packages
|
Rebuild depending packages
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -72,17 +72,17 @@ export default function PackageRebuildDialog({ open, onClose }: PackageRebuildDi
|
|||||||
<RepositorySelect repositorySelect={repositorySelect} />
|
<RepositorySelect repositorySelect={repositorySelect} />
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label="dependency"
|
|
||||||
placeholder="packages dependency"
|
|
||||||
fullWidth
|
fullWidth
|
||||||
|
label="dependency"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
value={dependency}
|
placeholder="packages dependency"
|
||||||
onChange={event => setDependency(event.target.value)}
|
onChange={event => setDependency(event.target.value)}
|
||||||
|
value={dependency}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => void handleRebuild()} variant="contained" startIcon={<PlayArrowIcon />}>rebuild</Button>
|
<Button onClick={() => void handleRebuild()} startIcon={<PlayArrowIcon />} variant="contained">rebuild</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>;
|
</Dialog>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* 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 { Box, Container } from "@mui/material";
|
import Brightness4Icon from "@mui/icons-material/Brightness4";
|
||||||
|
import Brightness7Icon from "@mui/icons-material/Brightness7";
|
||||||
|
import { Box, Container, IconButton, Tooltip } from "@mui/material";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import LoginDialog from "components/dialogs/LoginDialog";
|
import LoginDialog from "components/dialogs/LoginDialog";
|
||||||
import Footer from "components/layout/Footer";
|
import Footer from "components/layout/Footer";
|
||||||
@@ -27,6 +29,7 @@ import { QueryKeys } from "hooks/QueryKeys";
|
|||||||
import { useAuth } from "hooks/useAuth";
|
import { useAuth } from "hooks/useAuth";
|
||||||
import { useClient } from "hooks/useClient";
|
import { useClient } from "hooks/useClient";
|
||||||
import { useRepository } from "hooks/useRepository";
|
import { useRepository } from "hooks/useRepository";
|
||||||
|
import { useThemeMode } from "hooks/useThemeMode";
|
||||||
import type { InfoResponse } from "models/InfoResponse";
|
import type { InfoResponse } from "models/InfoResponse";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
@@ -34,11 +37,12 @@ export default function AppLayout(): React.JSX.Element {
|
|||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { setAuthState } = useAuth();
|
const { setAuthState } = useAuth();
|
||||||
const { setRepositories } = useRepository();
|
const { setRepositories } = useRepository();
|
||||||
|
const { mode, toggleTheme } = useThemeMode();
|
||||||
const [loginOpen, setLoginOpen] = useState(false);
|
const [loginOpen, setLoginOpen] = useState(false);
|
||||||
|
|
||||||
const { data: info } = useQuery<InfoResponse>({
|
const { data: info } = useQuery<InfoResponse>({
|
||||||
queryKey: QueryKeys.info,
|
|
||||||
queryFn: () => client.fetch.fetchServerInfo(),
|
queryFn: () => client.fetch.fetchServerInfo(),
|
||||||
|
queryKey: QueryKeys.info,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,26 +55,29 @@ export default function AppLayout(): React.JSX.Element {
|
|||||||
}, [info, setAuthState, setRepositories]);
|
}, [info, setAuthState, setRepositories]);
|
||||||
|
|
||||||
return <Container maxWidth="xl">
|
return <Container maxWidth="xl">
|
||||||
<Box sx={{ display: "flex", alignItems: "center", py: 1, gap: 1 }}>
|
<Box sx={{ alignItems: "center", display: "flex", gap: 1, py: 1 }}>
|
||||||
<a href="https://ahriman.readthedocs.io/" title="logo">
|
<a href="https://ahriman.readthedocs.io/" title="logo">
|
||||||
<img src="/static/logo.svg" width={30} height={30} alt="" />
|
<img alt="" height={30} src="/static/logo.svg" width={30} />
|
||||||
</a>
|
</a>
|
||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
</Box>
|
</Box>
|
||||||
|
<Tooltip title="Toggle theme">
|
||||||
|
<IconButton aria-label="Toggle theme" onClick={toggleTheme}>
|
||||||
|
{mode === "dark" ? <Brightness7Icon /> : <Brightness4Icon />}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<PackageTable
|
<PackageTable autoRefreshIntervals={info?.autorefresh_intervals ?? []} />
|
||||||
autoRefreshIntervals={info?.autorefresh_intervals ?? []}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Footer
|
<Footer
|
||||||
version={info?.version ?? ""}
|
|
||||||
docsEnabled={info?.docs_enabled ?? false}
|
docsEnabled={info?.docs_enabled ?? false}
|
||||||
indexUrl={info?.index_url}
|
indexUrl={info?.index_url}
|
||||||
onLoginClick={() => info?.auth.external ? window.location.assign("/api/v1/login") : setLoginOpen(true)}
|
onLoginClick={() => info?.auth.external ? window.location.assign("/api/v1/login") : setLoginOpen(true)}
|
||||||
|
version={info?.version ?? ""}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LoginDialog open={loginOpen} onClose={() => setLoginOpen(false)} />
|
<LoginDialog onClose={() => setLoginOpen(false)} open={loginOpen} />
|
||||||
</Container>;
|
</Container>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,41 +26,41 @@ import { useAuth } from "hooks/useAuth";
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
interface FooterProps {
|
interface FooterProps {
|
||||||
version: string;
|
|
||||||
docsEnabled: boolean;
|
docsEnabled: boolean;
|
||||||
indexUrl?: string;
|
indexUrl?: string;
|
||||||
onLoginClick: () => void;
|
onLoginClick: () => void;
|
||||||
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Footer({ version, docsEnabled, indexUrl, onLoginClick }: FooterProps): React.JSX.Element {
|
export default function Footer({ docsEnabled, indexUrl, onLoginClick, version }: FooterProps): React.JSX.Element {
|
||||||
const { enabled: authEnabled, username, logout } = useAuth();
|
const { enabled: authEnabled, username, logout } = useAuth();
|
||||||
|
|
||||||
return <Box
|
return <Box
|
||||||
component="footer"
|
component="footer"
|
||||||
sx={{
|
sx={{
|
||||||
|
alignItems: "center",
|
||||||
|
borderColor: "divider",
|
||||||
|
borderTop: 1,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
|
||||||
borderTop: 1,
|
|
||||||
borderColor: "divider",
|
|
||||||
mt: 2,
|
mt: 2,
|
||||||
py: 1,
|
py: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: "flex", gap: 2, alignItems: "center" }}>
|
<Box sx={{ alignItems: "center", display: "flex", gap: 2 }}>
|
||||||
<Link href="https://github.com/arcan1s/ahriman" underline="hover" color="inherit" sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
<Link color="inherit" href="https://github.com/arcan1s/ahriman" sx={{ alignItems: "center", display: "flex", gap: 0.5 }} underline="hover">
|
||||||
<GitHubIcon fontSize="small" />
|
<GitHubIcon fontSize="small" />
|
||||||
<Typography variant="body2">ahriman {version}</Typography>
|
<Typography variant="body2">ahriman {version}</Typography>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="https://github.com/arcan1s/ahriman/releases" underline="hover" color="text.secondary" variant="body2">
|
<Link color="text.secondary" href="https://github.com/arcan1s/ahriman/releases" underline="hover" variant="body2">
|
||||||
releases
|
releases
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="https://github.com/arcan1s/ahriman/issues" underline="hover" color="text.secondary" variant="body2">
|
<Link color="text.secondary" href="https://github.com/arcan1s/ahriman/issues" underline="hover" variant="body2">
|
||||||
report a bug
|
report a bug
|
||||||
</Link>
|
</Link>
|
||||||
{docsEnabled &&
|
{docsEnabled &&
|
||||||
<Link href="/api-docs" underline="hover" color="text.secondary" variant="body2">
|
<Link color="text.secondary" href="/api-docs" underline="hover" variant="body2">
|
||||||
api
|
api
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ export default function Footer({ version, docsEnabled, indexUrl, onLoginClick }:
|
|||||||
|
|
||||||
{indexUrl &&
|
{indexUrl &&
|
||||||
<Box>
|
<Box>
|
||||||
<Link href={indexUrl} underline="hover" color="inherit" sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
<Link color="inherit" href={indexUrl} underline="hover" sx={{ alignItems: "center", display: "flex", gap: 0.5 }}>
|
||||||
<HomeIcon fontSize="small" />
|
<HomeIcon fontSize="small" />
|
||||||
<Typography variant="body2">repo index</Typography>
|
<Typography variant="body2">repo index</Typography>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -78,11 +78,11 @@ export default function Footer({ version, docsEnabled, indexUrl, onLoginClick }:
|
|||||||
{authEnabled &&
|
{authEnabled &&
|
||||||
<Box>
|
<Box>
|
||||||
{username ?
|
{username ?
|
||||||
<Button size="small" startIcon={<LogoutIcon />} onClick={() => void logout()} sx={{ textTransform: "none" }}>
|
<Button onClick={() => void logout()} size="small" startIcon={<LogoutIcon />} sx={{ textTransform: "none" }}>
|
||||||
logout ({username})
|
logout ({username})
|
||||||
</Button>
|
</Button>
|
||||||
:
|
:
|
||||||
<Button size="small" startIcon={<LoginIcon />} onClick={onLoginClick} sx={{ textTransform: "none" }}>
|
<Button onClick={onLoginClick} size="small" startIcon={<LoginIcon />} sx={{ textTransform: "none" }}>
|
||||||
login
|
login
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,27 +22,28 @@ import { useRepository } from "hooks/useRepository";
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
export default function Navbar(): React.JSX.Element | null {
|
export default function Navbar(): React.JSX.Element | null {
|
||||||
const { repositories, current, setCurrent } = useRepository();
|
const { repositories, currentRepository, setCurrentRepository } = useRepository();
|
||||||
|
|
||||||
if (repositories.length === 0 || !current) {
|
if (repositories.length === 0 || !currentRepository) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentIndex = repositories.findIndex(repository =>
|
const currentIndex = repositories.findIndex(repository =>
|
||||||
repository.architecture === current.architecture && repository.repository === current.repository,
|
repository.architecture === currentRepository.architecture &&
|
||||||
|
repository.repository === currentRepository.repository,
|
||||||
);
|
);
|
||||||
|
|
||||||
return <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
return <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={currentIndex >= 0 ? currentIndex : 0}
|
|
||||||
onChange={(_, newValue: number) => {
|
onChange={(_, newValue: number) => {
|
||||||
const repository = repositories[newValue];
|
const repository = repositories[newValue];
|
||||||
if (repository) {
|
if (repository) {
|
||||||
setCurrent(repository);
|
setCurrentRepository(repository);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
variant="scrollable"
|
|
||||||
scrollButtons="auto"
|
scrollButtons="auto"
|
||||||
|
value={currentIndex >= 0 ? currentIndex : 0}
|
||||||
|
variant="scrollable"
|
||||||
>
|
>
|
||||||
{repositories.map(repository =>
|
{repositories.map(repository =>
|
||||||
<Tab
|
<Tab
|
||||||
|
|||||||
119
frontend/src/components/package/ArtifactsTab.tsx
Normal file
119
frontend/src/components/package/ArtifactsTab.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* 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 RestoreIcon from "@mui/icons-material/Restore";
|
||||||
|
import { Box, IconButton, Tooltip } from "@mui/material";
|
||||||
|
import { DataGrid, type GridColDef } from "@mui/x-data-grid";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { ApiError } from "api/client/ApiError";
|
||||||
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
|
import { useAuth } from "hooks/useAuth";
|
||||||
|
import { useClient } from "hooks/useClient";
|
||||||
|
import { useNotification } from "hooks/useNotification";
|
||||||
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
import type React from "react";
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { DETAIL_TABLE_PROPS } from "utils";
|
||||||
|
|
||||||
|
interface ArtifactsTabProps {
|
||||||
|
currentVersion: string;
|
||||||
|
packageBase: string;
|
||||||
|
repository: RepositoryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArtifactRow {
|
||||||
|
id: string;
|
||||||
|
packager: string;
|
||||||
|
packages: string[];
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const staticColumns: GridColDef<ArtifactRow>[] = [
|
||||||
|
{ align: "right", field: "version", flex: 1, headerAlign: "right", headerName: "version" },
|
||||||
|
{
|
||||||
|
field: "packages",
|
||||||
|
flex: 2,
|
||||||
|
headerName: "packages",
|
||||||
|
renderCell: params =>
|
||||||
|
<Box sx={{ whiteSpace: "pre-line" }}>{params.row.packages.join("\n")}</Box>,
|
||||||
|
},
|
||||||
|
{ field: "packager", flex: 1, headerName: "packager" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ArtifactsTab({
|
||||||
|
currentVersion,
|
||||||
|
packageBase,
|
||||||
|
repository,
|
||||||
|
}: ArtifactsTabProps): React.JSX.Element {
|
||||||
|
const client = useClient();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { isAuthorized } = useAuth();
|
||||||
|
const { showSuccess, showError } = useNotification();
|
||||||
|
|
||||||
|
const { data: rows = [] } = useQuery<ArtifactRow[]>({
|
||||||
|
enabled: !!packageBase,
|
||||||
|
queryFn: async () => {
|
||||||
|
const packages = await client.fetch.fetchPackageArtifacts(packageBase, repository);
|
||||||
|
return packages.map(artifact => ({
|
||||||
|
id: artifact.version,
|
||||||
|
packager: artifact.packager ?? "",
|
||||||
|
packages: Object.keys(artifact.packages).sort(),
|
||||||
|
version: artifact.version,
|
||||||
|
})).reverse();
|
||||||
|
},
|
||||||
|
queryKey: QueryKeys.artifacts(packageBase, repository),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRollback = useCallback(async (version: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await client.service.servicePackageRollback(repository, { package: packageBase, version });
|
||||||
|
void queryClient.invalidateQueries({ queryKey: QueryKeys.artifacts(packageBase, repository) });
|
||||||
|
void queryClient.invalidateQueries({ queryKey: QueryKeys.package(packageBase, repository) });
|
||||||
|
showSuccess("Success", `Rollback ${packageBase} to ${version} has been started`);
|
||||||
|
} catch (exception) {
|
||||||
|
showError("Action failed", `Rollback failed: ${ApiError.errorDetail(exception)}`);
|
||||||
|
}
|
||||||
|
}, [client, repository, packageBase, queryClient, showSuccess, showError]);
|
||||||
|
|
||||||
|
const columns = useMemo<GridColDef<ArtifactRow>[]>(() => [
|
||||||
|
...staticColumns,
|
||||||
|
...isAuthorized ? [{
|
||||||
|
field: "actions",
|
||||||
|
filterable: false,
|
||||||
|
headerName: "",
|
||||||
|
renderCell: params =>
|
||||||
|
<Tooltip title={params.row.version === currentVersion ? "Current version" : "Rollback to this version"}>
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
disabled={params.row.version === currentVersion}
|
||||||
|
onClick={() => void handleRollback(params.row.version)}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<RestoreIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>,
|
||||||
|
width: 60,
|
||||||
|
} satisfies GridColDef<ArtifactRow>] : [],
|
||||||
|
], [isAuthorized, currentVersion, handleRollback]);
|
||||||
|
|
||||||
|
return <Box sx={{ mt: 1 }}>
|
||||||
|
<DataGrid columns={columns} getRowHeight={() => "auto"} rows={rows} {...DETAIL_TABLE_PROPS} />
|
||||||
|
</Box>;
|
||||||
|
}
|
||||||
@@ -29,16 +29,16 @@ import type { RepositoryId } from "models/RepositoryId";
|
|||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
interface Logs {
|
interface Logs {
|
||||||
version: string;
|
|
||||||
processId: string;
|
|
||||||
created: number;
|
created: number;
|
||||||
logs: string;
|
logs: string;
|
||||||
|
processId: string;
|
||||||
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BuildLogsTabProps {
|
interface BuildLogsTabProps {
|
||||||
packageBase: string;
|
packageBase: string;
|
||||||
repository: RepositoryId;
|
|
||||||
refreshInterval: number;
|
refreshInterval: number;
|
||||||
|
repository: RepositoryId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertLogs(records: LogRecord[], filter?: (record: LogRecord) => boolean): string {
|
function convertLogs(records: LogRecord[], filter?: (record: LogRecord) => boolean): string {
|
||||||
@@ -50,17 +50,17 @@ function convertLogs(records: LogRecord[], filter?: (record: LogRecord) => boole
|
|||||||
|
|
||||||
export default function BuildLogsTab({
|
export default function BuildLogsTab({
|
||||||
packageBase,
|
packageBase,
|
||||||
repository,
|
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
|
repository,
|
||||||
}: BuildLogsTabProps): React.JSX.Element {
|
}: BuildLogsTabProps): React.JSX.Element {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const [selectedVersionKey, setSelectedVersionKey] = useState<string | null>(null);
|
const [selectedVersionKey, setSelectedVersionKey] = useState<string | null>(null);
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
const { data: allLogs } = useQuery<LogRecord[]>({
|
const { data: allLogs } = useQuery<LogRecord[]>({
|
||||||
queryKey: QueryKeys.logs(packageBase, repository),
|
|
||||||
queryFn: () => client.fetch.fetchPackageLogs(packageBase, repository),
|
|
||||||
enabled: !!packageBase,
|
enabled: !!packageBase,
|
||||||
|
queryFn: () => client.fetch.fetchPackageLogs(packageBase, repository),
|
||||||
|
queryKey: QueryKeys.logs(packageBase, repository),
|
||||||
refetchInterval: refreshInterval > 0 ? refreshInterval : false,
|
refetchInterval: refreshInterval > 0 ? refreshInterval : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,13 +84,13 @@ export default function BuildLogsTab({
|
|||||||
return Object.values(grouped)
|
return Object.values(grouped)
|
||||||
.sort((left, right) => right.minCreated - left.minCreated)
|
.sort((left, right) => right.minCreated - left.minCreated)
|
||||||
.map(record => ({
|
.map(record => ({
|
||||||
version: record.version,
|
|
||||||
processId: record.process_id,
|
|
||||||
created: record.minCreated,
|
created: record.minCreated,
|
||||||
logs: convertLogs(
|
logs: convertLogs(
|
||||||
allLogs,
|
allLogs,
|
||||||
right => record.version === right.version && record.process_id === right.process_id,
|
right => record.version === right.version && record.process_id === right.process_id,
|
||||||
),
|
),
|
||||||
|
processId: record.process_id,
|
||||||
|
version: record.version,
|
||||||
}));
|
}));
|
||||||
}, [allLogs]);
|
}, [allLogs]);
|
||||||
|
|
||||||
@@ -110,13 +110,13 @@ export default function BuildLogsTab({
|
|||||||
|
|
||||||
// Refresh active version logs
|
// Refresh active version logs
|
||||||
const { data: versionLogs } = useQuery<LogRecord[]>({
|
const { data: versionLogs } = useQuery<LogRecord[]>({
|
||||||
queryKey: QueryKeys.logsVersion(packageBase, repository, activeVersion?.version ?? "", activeVersion?.processId ?? ""),
|
placeholderData: keepPreviousData,
|
||||||
queryFn: activeVersion
|
queryFn: activeVersion
|
||||||
? () => client.fetch.fetchPackageLogs(
|
? () => client.fetch.fetchPackageLogs(
|
||||||
packageBase, repository, activeVersion.version, activeVersion.processId,
|
packageBase, repository, activeVersion.version, activeVersion.processId,
|
||||||
)
|
)
|
||||||
: skipToken,
|
: skipToken,
|
||||||
placeholderData: keepPreviousData,
|
queryKey: QueryKeys.logsVersion(packageBase, repository, activeVersion?.version ?? "", activeVersion?.processId ?? ""),
|
||||||
refetchInterval: refreshInterval > 0 ? refreshInterval : false,
|
refetchInterval: refreshInterval > 0 ? refreshInterval : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -143,25 +143,25 @@ export default function BuildLogsTab({
|
|||||||
return <Box sx={{ display: "flex", gap: 1, mt: 1 }}>
|
return <Box sx={{ display: "flex", gap: 1, mt: 1 }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
|
||||||
aria-label="Select version"
|
aria-label="Select version"
|
||||||
startIcon={<ListIcon />}
|
|
||||||
onClick={event => setAnchorEl(event.currentTarget)}
|
onClick={event => setAnchorEl(event.currentTarget)}
|
||||||
|
size="small"
|
||||||
|
startIcon={<ListIcon />}
|
||||||
/>
|
/>
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
open={Boolean(anchorEl)}
|
|
||||||
onClose={() => setAnchorEl(null)}
|
onClose={() => setAnchorEl(null)}
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
>
|
>
|
||||||
{versions.map((logs, index) =>
|
{versions.map((logs, index) =>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={`${logs.version}-${logs.processId}`}
|
key={`${logs.version}-${logs.processId}`}
|
||||||
selected={index === activeIndex}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedVersionKey(`${logs.version}-${logs.processId}`);
|
setSelectedVersionKey(`${logs.version}-${logs.processId}`);
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
resetScroll();
|
resetScroll();
|
||||||
}}
|
}}
|
||||||
|
selected={index === activeIndex}
|
||||||
>
|
>
|
||||||
<Typography variant="body2">{new Date(logs.created * 1000).toISOStringShort()}</Typography>
|
<Typography variant="body2">{new Date(logs.created * 1000).toISOStringShort()}</Typography>
|
||||||
</MenuItem>,
|
</MenuItem>,
|
||||||
@@ -174,11 +174,10 @@ export default function BuildLogsTab({
|
|||||||
|
|
||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
preRef={preRef}
|
content={displayedLogs}
|
||||||
getText={() => displayedLogs}
|
|
||||||
height={400}
|
height={400}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
wordBreak
|
preRef={preRef}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>;
|
</Box>;
|
||||||
|
|||||||
@@ -17,19 +17,10 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* 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 { Box } from "@mui/material";
|
import CodeBlock from "components/common/CodeBlock";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { usePackageChanges } from "hooks/usePackageChanges";
|
||||||
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 type { RepositoryId } from "models/RepositoryId";
|
||||||
import React 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";
|
|
||||||
|
|
||||||
SyntaxHighlighter.registerLanguage("diff", diff);
|
|
||||||
|
|
||||||
interface ChangesTabProps {
|
interface ChangesTabProps {
|
||||||
packageBase: string;
|
packageBase: string;
|
||||||
@@ -37,34 +28,7 @@ interface ChangesTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ChangesTab({ packageBase, repository }: ChangesTabProps): React.JSX.Element {
|
export default function ChangesTab({ packageBase, repository }: ChangesTabProps): React.JSX.Element {
|
||||||
const client = useClient();
|
const data = usePackageChanges(packageBase, repository);
|
||||||
|
|
||||||
const { data } = useQuery<Changes>({
|
return <CodeBlock content={data?.changes ?? ""} height={400} language="diff" />;
|
||||||
queryKey: QueryKeys.changes(packageBase, repository),
|
|
||||||
queryFn: () => client.fetch.fetchPackageChanges(packageBase, repository),
|
|
||||||
enabled: !!packageBase,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changesText = data?.changes ?? "";
|
|
||||||
|
|
||||||
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>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import type { Event } from "models/Event";
|
|||||||
import type { RepositoryId } from "models/RepositoryId";
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { DETAIL_TABLE_PROPS } from "utils";
|
||||||
|
|
||||||
interface EventsTabProps {
|
interface EventsTabProps {
|
||||||
packageBase: string;
|
packageBase: string;
|
||||||
@@ -34,46 +35,36 @@ interface EventsTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface EventRow {
|
interface EventRow {
|
||||||
id: number;
|
|
||||||
timestamp: string;
|
|
||||||
event: string;
|
event: string;
|
||||||
|
id: number;
|
||||||
message: string;
|
message: string;
|
||||||
|
timestamp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: GridColDef<EventRow>[] = [
|
const columns: GridColDef<EventRow>[] = [
|
||||||
{ field: "timestamp", headerName: "date", width: 180, align: "right", headerAlign: "right" },
|
{ align: "right", field: "timestamp", headerAlign: "right", headerName: "date", width: 180 },
|
||||||
{ field: "event", headerName: "event", flex: 1 },
|
{ field: "event", flex: 1, headerName: "event" },
|
||||||
{ field: "message", headerName: "description", flex: 2 },
|
{ field: "message", flex: 2, headerName: "description" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function EventsTab({ packageBase, repository }: EventsTabProps): React.JSX.Element {
|
export default function EventsTab({ packageBase, repository }: EventsTabProps): React.JSX.Element {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
const { data: events = [] } = useQuery<Event[]>({
|
const { data: events = [] } = useQuery<Event[]>({
|
||||||
queryKey: QueryKeys.events(repository, packageBase),
|
|
||||||
queryFn: () => client.fetch.fetchPackageEvents(repository, packageBase, 30),
|
|
||||||
enabled: !!packageBase,
|
enabled: !!packageBase,
|
||||||
|
queryFn: () => client.fetch.fetchPackageEvents(repository, packageBase, 30),
|
||||||
|
queryKey: QueryKeys.events(repository, packageBase),
|
||||||
});
|
});
|
||||||
|
|
||||||
const rows = useMemo<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,
|
event: event.event,
|
||||||
|
id: index,
|
||||||
message: event.message ?? "",
|
message: event.message ?? "",
|
||||||
|
timestamp: new Date(event.created * 1000).toISOStringShort(),
|
||||||
})), [events]);
|
})), [events]);
|
||||||
|
|
||||||
return <Box sx={{ mt: 1 }}>
|
return <Box sx={{ mt: 1 }}>
|
||||||
<EventDurationLineChart events={events} />
|
<EventDurationLineChart events={events} />
|
||||||
<DataGrid
|
<DataGrid columns={columns} rows={rows} {...DETAIL_TABLE_PROPS} />
|
||||||
rows={rows}
|
|
||||||
columns={columns}
|
|
||||||
density="compact"
|
|
||||||
initialState={{
|
|
||||||
sorting: { sortModel: [{ field: "timestamp", sort: "desc" }] },
|
|
||||||
}}
|
|
||||||
pageSizeOptions={[10, 25]}
|
|
||||||
sx={{ height: 400, mt: 1 }}
|
|
||||||
disableRowSelectionOnClick
|
|
||||||
/>
|
|
||||||
</Box>;
|
</Box>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ import type { Package } from "models/Package";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
interface PackageDetailsGridProps {
|
interface PackageDetailsGridProps {
|
||||||
pkg: Package;
|
|
||||||
dependencies?: Dependencies;
|
dependencies?: Dependencies;
|
||||||
|
pkg: Package;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PackageDetailsGrid({ pkg, dependencies }: PackageDetailsGridProps): React.JSX.Element {
|
export default function PackageDetailsGrid({ dependencies, pkg }: PackageDetailsGridProps): React.JSX.Element {
|
||||||
const packagesList = Object.entries(pkg.packages)
|
const packagesList = Object.entries(pkg.packages)
|
||||||
.map(([name, properties]) => `${name}${properties.description ? ` (${properties.description})` : ""}`);
|
.map(([name, properties]) => `${name}${properties.description ? ` (${properties.description})` : ""}`);
|
||||||
|
|
||||||
@@ -65,50 +65,50 @@ export default function PackageDetailsGrid({ pkg, dependencies }: PackageDetails
|
|||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Grid container spacing={1} sx={{ mt: 1 }}>
|
<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={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">packages</Typography></Grid>
|
||||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{packagesList.unique().join("\n")}</Typography></Grid>
|
<Grid size={{ md: 5, xs: 8 }}><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={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">version</Typography></Grid>
|
||||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2">{pkg.version}</Typography></Grid>
|
<Grid size={{ md: 5, xs: 8 }}><Typography variant="body2">{pkg.version}</Typography></Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid container spacing={1} sx={{ mt: 0.5 }}>
|
<Grid container spacing={1} sx={{ mt: 0.5 }}>
|
||||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">packager</Typography></Grid>
|
<Grid size={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">packager</Typography></Grid>
|
||||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2">{pkg.packager ?? ""}</Typography></Grid>
|
<Grid size={{ md: 5, xs: 8 }}><Typography variant="body2">{pkg.packager ?? ""}</Typography></Grid>
|
||||||
<Grid size={{ xs: 4, md: 1 }} />
|
<Grid size={{ md: 1, xs: 4 }} />
|
||||||
<Grid size={{ xs: 8, md: 5 }} />
|
<Grid size={{ md: 5, xs: 8 }} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid container spacing={1} sx={{ mt: 0.5 }}>
|
<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={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">groups</Typography></Grid>
|
||||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{groups.unique().join("\n")}</Typography></Grid>
|
<Grid size={{ md: 5, xs: 8 }}><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={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">licenses</Typography></Grid>
|
||||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{licenses.unique().join("\n")}</Typography></Grid>
|
<Grid size={{ md: 5, xs: 8 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{licenses.unique().join("\n")}</Typography></Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid container spacing={1} sx={{ mt: 0.5 }}>
|
<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={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">upstream</Typography></Grid>
|
||||||
<Grid size={{ xs: 8, md: 5 }}>
|
<Grid size={{ md: 5, xs: 8 }}>
|
||||||
{upstreamUrls.map(url =>
|
{upstreamUrls.map(url =>
|
||||||
<Link key={url} href={url} target="_blank" rel="noopener noreferrer" underline="hover" display="block" variant="body2">
|
<Link display="block" href={url} key={url} rel="noopener noreferrer" target="_blank" underline="hover" variant="body2">
|
||||||
{url}
|
{url}
|
||||||
</Link>,
|
</Link>,
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">AUR</Typography></Grid>
|
<Grid size={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">AUR</Typography></Grid>
|
||||||
<Grid size={{ xs: 8, md: 5 }}>
|
<Grid size={{ md: 5, xs: 8 }}>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
{aurUrl &&
|
{aurUrl &&
|
||||||
<Link href={aurUrl} target="_blank" rel="noopener noreferrer" underline="hover">AUR link</Link>
|
<Link href={aurUrl} rel="noopener noreferrer" target="_blank" underline="hover">AUR link</Link>
|
||||||
}
|
}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid container spacing={1} sx={{ mt: 0.5 }}>
|
<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={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">depends</Typography></Grid>
|
||||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{allDepends.join("\n")}</Typography></Grid>
|
<Grid size={{ md: 5, xs: 8 }}><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={{ md: 1, xs: 4 }}><Typography align="right" color="text.secondary" variant="body2">implicitly depends</Typography></Grid>
|
||||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{implicitDepends.unique().join("\n")}</Typography></Grid>
|
<Grid size={{ md: 5, xs: 8 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{implicitDepends.unique().join("\n")}</Typography></Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,32 +18,38 @@
|
|||||||
* 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 DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import PauseCircleIcon from "@mui/icons-material/PauseCircle";
|
||||||
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||||
|
import PlayCircleIcon from "@mui/icons-material/PlayCircle";
|
||||||
import { Button, Checkbox, DialogActions, FormControlLabel } from "@mui/material";
|
import { Button, Checkbox, DialogActions, FormControlLabel } from "@mui/material";
|
||||||
import AutoRefreshControl from "components/common/AutoRefreshControl";
|
import AutoRefreshControl from "components/common/AutoRefreshControl";
|
||||||
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
interface PackageInfoActionsProps {
|
interface PackageInfoActionsProps {
|
||||||
isAuthorized: boolean;
|
|
||||||
refreshDatabase: boolean;
|
|
||||||
onRefreshDatabaseChange: (checked: boolean) => void;
|
|
||||||
onUpdate: () => void;
|
|
||||||
onRemove: () => void;
|
|
||||||
autoRefreshIntervals: AutoRefreshInterval[];
|
|
||||||
autoRefreshInterval: number;
|
autoRefreshInterval: number;
|
||||||
|
autoRefreshIntervals: AutoRefreshInterval[];
|
||||||
|
isAuthorized: boolean;
|
||||||
|
isHeld: boolean;
|
||||||
onAutoRefreshIntervalChange: (interval: number) => void;
|
onAutoRefreshIntervalChange: (interval: number) => void;
|
||||||
|
onHoldToggle: () => void;
|
||||||
|
onRefreshDatabaseChange: (checked: boolean) => void;
|
||||||
|
onRemove: () => void;
|
||||||
|
onUpdate: () => void;
|
||||||
|
refreshDatabase: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PackageInfoActions({
|
export default function PackageInfoActions({
|
||||||
isAuthorized,
|
|
||||||
refreshDatabase,
|
|
||||||
onRefreshDatabaseChange,
|
|
||||||
onUpdate,
|
|
||||||
onRemove,
|
|
||||||
autoRefreshIntervals,
|
|
||||||
autoRefreshInterval,
|
autoRefreshInterval,
|
||||||
|
autoRefreshIntervals,
|
||||||
|
isAuthorized,
|
||||||
|
isHeld,
|
||||||
onAutoRefreshIntervalChange,
|
onAutoRefreshIntervalChange,
|
||||||
|
onHoldToggle,
|
||||||
|
onRefreshDatabaseChange,
|
||||||
|
onRemove,
|
||||||
|
onUpdate,
|
||||||
|
refreshDatabase,
|
||||||
}: PackageInfoActionsProps): React.JSX.Element {
|
}: PackageInfoActionsProps): React.JSX.Element {
|
||||||
return <DialogActions sx={{ flexWrap: "wrap", gap: 1 }}>
|
return <DialogActions sx={{ flexWrap: "wrap", gap: 1 }}>
|
||||||
{isAuthorized &&
|
{isAuthorized &&
|
||||||
@@ -52,17 +58,20 @@ export default function PackageInfoActions({
|
|||||||
control={<Checkbox checked={refreshDatabase} onChange={(_, checked) => onRefreshDatabaseChange(checked)} size="small" />}
|
control={<Checkbox checked={refreshDatabase} onChange={(_, checked) => onRefreshDatabaseChange(checked)} size="small" />}
|
||||||
label="update pacman databases"
|
label="update pacman databases"
|
||||||
/>
|
/>
|
||||||
<Button onClick={onUpdate} variant="contained" color="success" startIcon={<PlayArrowIcon />} size="small">
|
<Button color="warning" onClick={onHoldToggle} size="small" startIcon={isHeld ? <PlayCircleIcon /> : <PauseCircleIcon />} variant="outlined">
|
||||||
|
{isHeld ? "unhold" : "hold"}
|
||||||
|
</Button>
|
||||||
|
<Button color="success" onClick={onUpdate} size="small" startIcon={<PlayArrowIcon />} variant="contained">
|
||||||
update
|
update
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onRemove} variant="contained" color="error" startIcon={<DeleteIcon />} size="small">
|
<Button color="error" onClick={onRemove} size="small" startIcon={<DeleteIcon />} variant="contained">
|
||||||
remove
|
remove
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
<AutoRefreshControl
|
<AutoRefreshControl
|
||||||
intervals={autoRefreshIntervals}
|
|
||||||
currentInterval={autoRefreshInterval}
|
currentInterval={autoRefreshInterval}
|
||||||
|
intervals={autoRefreshIntervals}
|
||||||
onIntervalChange={onAutoRefreshIntervalChange}
|
onIntervalChange={onAutoRefreshIntervalChange}
|
||||||
/>
|
/>
|
||||||
</DialogActions>;
|
</DialogActions>;
|
||||||
|
|||||||
@@ -23,39 +23,39 @@ import type { Patch } from "models/Patch";
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
interface PackagePatchesListProps {
|
interface PackagePatchesListProps {
|
||||||
patches: Patch[];
|
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
onDelete: (key: string) => void;
|
onDelete: (key: string) => void;
|
||||||
|
patches: Patch[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PackagePatchesList({
|
export default function PackagePatchesList({
|
||||||
patches,
|
|
||||||
editable,
|
editable,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
patches,
|
||||||
}: PackagePatchesListProps): React.JSX.Element | null {
|
}: PackagePatchesListProps): React.JSX.Element | null {
|
||||||
if (patches.length === 0) {
|
if (patches.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Box sx={{ mt: 2 }}>
|
return <Box sx={{ mt: 2 }}>
|
||||||
<Typography variant="h6" gutterBottom>Environment variables</Typography>
|
<Typography gutterBottom variant="h6">Environment variables</Typography>
|
||||||
{patches.map(patch =>
|
{patches.map(patch =>
|
||||||
<Box key={patch.key} sx={{ display: "flex", alignItems: "center", gap: 1, mb: 0.5 }}>
|
<Box key={patch.key} sx={{ alignItems: "center", display: "flex", gap: 1, mb: 0.5 }}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
|
||||||
value={patch.key}
|
|
||||||
disabled
|
disabled
|
||||||
|
size="small"
|
||||||
sx={{ flex: 1 }}
|
sx={{ flex: 1 }}
|
||||||
|
value={patch.key}
|
||||||
/>
|
/>
|
||||||
<Box>=</Box>
|
<Box>=</Box>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
|
||||||
value={JSON.stringify(patch.value)}
|
|
||||||
disabled
|
disabled
|
||||||
|
value={JSON.stringify(patch.value)}
|
||||||
|
size="small"
|
||||||
sx={{ flex: 1 }}
|
sx={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
{editable &&
|
{editable &&
|
||||||
<IconButton size="small" color="error" onClick={() => onDelete(patch.key)}>
|
<IconButton aria-label="Remove patch" color="error" onClick={() => onDelete(patch.key)} size="small">
|
||||||
<DeleteIcon fontSize="small" />
|
<DeleteIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
|
|||||||
34
frontend/src/components/package/PkgbuildTab.tsx
Normal file
34
frontend/src/components/package/PkgbuildTab.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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 CodeBlock from "components/common/CodeBlock";
|
||||||
|
import { usePackageChanges } from "hooks/usePackageChanges";
|
||||||
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface PkgbuildTabProps {
|
||||||
|
packageBase: string;
|
||||||
|
repository: RepositoryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PkgbuildTab({ packageBase, repository }: PkgbuildTabProps): React.JSX.Element {
|
||||||
|
const data = usePackageChanges(packageBase, repository);
|
||||||
|
|
||||||
|
return <CodeBlock content={data?.pkgbuild ?? ""} height={400} language="bash" />;
|
||||||
|
}
|
||||||
28
frontend/src/components/package/TabKey.ts
Normal file
28
frontend/src/components/package/TabKey.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
export type TabKey = "logs" | "changes" | "pkgbuild" | "events" | "artifacts";
|
||||||
|
|
||||||
|
export const tabs: { key: TabKey; label: string }[] = [
|
||||||
|
{ key: "logs", label: "Build logs" },
|
||||||
|
{ key: "changes", label: "Changes" },
|
||||||
|
{ key: "pkgbuild", label: "PKGBUILD" },
|
||||||
|
{ key: "events", label: "Events" },
|
||||||
|
{ key: "artifacts", label: "Artifacts" },
|
||||||
|
];
|
||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
GRID_CHECKBOX_SELECTION_COL_DEF,
|
GRID_CHECKBOX_SELECTION_COL_DEF,
|
||||||
type GridColDef,
|
type GridColDef,
|
||||||
type GridFilterModel,
|
type GridFilterModel,
|
||||||
type GridRenderCellParams,
|
|
||||||
type GridRowId,
|
type GridRowId,
|
||||||
useGridApiRef,
|
useGridApiRef,
|
||||||
} from "@mui/x-data-grid";
|
} from "@mui/x-data-grid";
|
||||||
@@ -44,8 +43,6 @@ interface PackageTableProps {
|
|||||||
autoRefreshIntervals: AutoRefreshInterval[];
|
autoRefreshIntervals: AutoRefreshInterval[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
|
|
||||||
|
|
||||||
function createListColumn(
|
function createListColumn(
|
||||||
field: keyof PackageRow,
|
field: keyof PackageRow,
|
||||||
headerName: string,
|
headerName: string,
|
||||||
@@ -55,10 +52,10 @@ function createListColumn(
|
|||||||
field,
|
field,
|
||||||
headerName,
|
headerName,
|
||||||
...options,
|
...options,
|
||||||
valueGetter: (value: string[]) => (value ?? []).join(" "),
|
renderCell: params =>
|
||||||
renderCell: (params: GridRenderCellParams<PackageRow>) =>
|
|
||||||
<Box sx={{ whiteSpace: "pre-line" }}>{((params.row[field] as string[]) ?? []).join("\n")}</Box>,
|
<Box sx={{ whiteSpace: "pre-line" }}>{((params.row[field] as string[]) ?? []).join("\n")}</Box>,
|
||||||
sortComparator: (left: string, right: string) => left.localeCompare(right),
|
sortComparator: (left: string, right: string) => left.localeCompare(right),
|
||||||
|
valueGetter: (value: string[]) => (value ?? []).join(" "),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,35 +76,30 @@ export default function PackageTable({ autoRefreshIntervals }: PackageTableProps
|
|||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
field: "base",
|
field: "base",
|
||||||
headerName: "package base",
|
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
headerName: "package base",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
renderCell: (params: GridRenderCellParams<PackageRow>) =>
|
renderCell: params =>
|
||||||
params.row.webUrl ?
|
params.row.webUrl ?
|
||||||
<Link href={params.row.webUrl} target="_blank" rel="noopener noreferrer" underline="hover">
|
<Link href={params.row.webUrl} rel="noopener noreferrer" target="_blank" underline="hover">
|
||||||
{params.value as string}
|
{params.value as string}
|
||||||
</Link>
|
</Link>
|
||||||
: params.value as string,
|
: params.value as string,
|
||||||
},
|
},
|
||||||
{ field: "version", headerName: "version", width: 180, align: "right", headerAlign: "right" },
|
{ align: "right", field: "version", headerAlign: "right", headerName: "version", width: 180 },
|
||||||
createListColumn("packages", "packages", { flex: 1, minWidth: 120 }),
|
createListColumn("packages", "packages", { flex: 1, minWidth: 120 }),
|
||||||
createListColumn("groups", "groups", { width: 150 }),
|
createListColumn("groups", "groups", { width: 150 }),
|
||||||
createListColumn("licenses", "licenses", { width: 150 }),
|
createListColumn("licenses", "licenses", { width: 150 }),
|
||||||
{ field: "packager", headerName: "packager", width: 150 },
|
{ field: "packager", headerName: "packager", width: 150 },
|
||||||
|
{ align: "right", field: "timestamp", headerName: "last update", headerAlign: "right", width: 180 },
|
||||||
{
|
{
|
||||||
field: "timestamp",
|
|
||||||
headerName: "last update",
|
|
||||||
width: 180,
|
|
||||||
align: "right",
|
|
||||||
headerAlign: "right",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "status",
|
|
||||||
headerName: "status",
|
|
||||||
width: 120,
|
|
||||||
align: "center",
|
align: "center",
|
||||||
|
field: "status",
|
||||||
headerAlign: "center",
|
headerAlign: "center",
|
||||||
renderCell: (params: GridRenderCellParams<PackageRow>) => <StatusCell status={params.row.status} />,
|
headerName: "status",
|
||||||
|
renderCell: params =>
|
||||||
|
<StatusCell isHeld={params.row.isHeld} status={params.row.status} />,
|
||||||
|
width: 120,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
@@ -115,56 +107,42 @@ export default function PackageTable({ autoRefreshIntervals }: PackageTableProps
|
|||||||
|
|
||||||
return <Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
return <Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||||
<PackageTableToolbar
|
<PackageTableToolbar
|
||||||
hasSelection={table.selectionModel.length > 0}
|
actions={{
|
||||||
isAuthorized={table.isAuthorized}
|
onAddClick: () => table.setDialogOpen("add"),
|
||||||
status={table.status}
|
onDashboardClick: () => table.setDialogOpen("dashboard"),
|
||||||
searchText={table.searchText}
|
onExportClick: () => apiRef.current?.exportDataAsCsv(),
|
||||||
onSearchChange={table.setSearchText}
|
onKeyImportClick: () => table.setDialogOpen("keyImport"),
|
||||||
|
onRebuildClick: () => table.setDialogOpen("rebuild"),
|
||||||
|
onRefreshDatabaseClick: () => void table.handleRefreshDatabase(),
|
||||||
|
onReloadClick: table.handleReload,
|
||||||
|
onRemoveClick: () => void table.handleRemove(),
|
||||||
|
onUpdateClick: () => void table.handleUpdate(),
|
||||||
|
}}
|
||||||
autoRefresh={{
|
autoRefresh={{
|
||||||
autoRefreshIntervals,
|
autoRefreshIntervals,
|
||||||
currentInterval: table.autoRefreshInterval,
|
currentInterval: table.autoRefreshInterval,
|
||||||
onIntervalChange: table.onAutoRefreshIntervalChange,
|
onIntervalChange: table.onAutoRefreshIntervalChange,
|
||||||
}}
|
}}
|
||||||
actions={{
|
isAuthorized={table.isAuthorized}
|
||||||
onDashboardClick: () => table.setDialogOpen("dashboard"),
|
hasSelection={table.selectionModel.length > 0}
|
||||||
onAddClick: () => table.setDialogOpen("add"),
|
onSearchChange={table.setSearchText}
|
||||||
onUpdateClick: () => void table.handleUpdate(),
|
searchText={table.searchText}
|
||||||
onRefreshDatabaseClick: () => void table.handleRefreshDatabase(),
|
status={table.status}
|
||||||
onRebuildClick: () => table.setDialogOpen("rebuild"),
|
|
||||||
onRemoveClick: () => void table.handleRemove(),
|
|
||||||
onKeyImportClick: () => table.setDialogOpen("keyImport"),
|
|
||||||
onReloadClick: table.handleReload,
|
|
||||||
onExportClick: () => apiRef.current?.exportDataAsCsv(),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
apiRef={apiRef}
|
apiRef={apiRef}
|
||||||
rows={table.rows}
|
|
||||||
columns={columns}
|
|
||||||
loading={table.isLoading}
|
|
||||||
getRowHeight={() => "auto"}
|
|
||||||
checkboxSelection
|
checkboxSelection
|
||||||
disableRowSelectionOnClick
|
|
||||||
rowSelectionModel={{ type: "include", ids: new Set<GridRowId>(table.selectionModel) }}
|
|
||||||
onRowSelectionModelChange={model => {
|
|
||||||
if (model.type === "exclude") {
|
|
||||||
const excludeIds = new Set([...model.ids].map(String));
|
|
||||||
table.setSelectionModel(table.rows.map(row => row.id).filter(id => !excludeIds.has(id)));
|
|
||||||
} else {
|
|
||||||
table.setSelectionModel([...model.ids].map(String));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
paginationModel={table.paginationModel}
|
|
||||||
onPaginationModelChange={table.setPaginationModel}
|
|
||||||
pageSizeOptions={PAGE_SIZE_OPTIONS}
|
|
||||||
columnVisibilityModel={table.columnVisibility}
|
columnVisibilityModel={table.columnVisibility}
|
||||||
onColumnVisibilityModelChange={table.setColumnVisibility}
|
columns={columns}
|
||||||
|
density="compact"
|
||||||
|
disableRowSelectionOnClick
|
||||||
filterModel={effectiveFilterModel}
|
filterModel={effectiveFilterModel}
|
||||||
onFilterModelChange={table.setFilterModel}
|
getRowHeight={() => "auto"}
|
||||||
initialState={{
|
initialState={{
|
||||||
sorting: { sortModel: [{ field: "base", sort: "asc" }] },
|
sorting: { sortModel: [{ field: "base", sort: "asc" }] },
|
||||||
}}
|
}}
|
||||||
|
loading={table.isLoading}
|
||||||
onCellClick={(params, event) => {
|
onCellClick={(params, event) => {
|
||||||
// Don't open info dialog when clicking checkbox or link
|
// Don't open info dialog when clicking checkbox or link
|
||||||
if (params.field === GRID_CHECKBOX_SELECTION_COL_DEF.field) {
|
if (params.field === GRID_CHECKBOX_SELECTION_COL_DEF.field) {
|
||||||
@@ -175,22 +153,32 @@ export default function PackageTable({ autoRefreshIntervals }: PackageTableProps
|
|||||||
}
|
}
|
||||||
table.setSelectedPackage(String(params.id));
|
table.setSelectedPackage(String(params.id));
|
||||||
}}
|
}}
|
||||||
sx={{
|
onColumnVisibilityModelChange={table.setColumnVisibility}
|
||||||
flex: 1,
|
onFilterModelChange={table.setFilterModel}
|
||||||
"& .MuiDataGrid-row": { cursor: "pointer" },
|
onPaginationModelChange={table.setPaginationModel}
|
||||||
|
onRowSelectionModelChange={model => {
|
||||||
|
if (model.type === "exclude") {
|
||||||
|
const excludeIds = new Set([...model.ids].map(String));
|
||||||
|
table.setSelectionModel(table.rows.map(row => row.id).filter(id => !excludeIds.has(id)));
|
||||||
|
} else {
|
||||||
|
table.setSelectionModel([...model.ids].map(String));
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
density="compact"
|
paginationModel={table.paginationModel}
|
||||||
|
rowSelectionModel={{ type: "include", ids: new Set<GridRowId>(table.selectionModel) }}
|
||||||
|
rows={table.rows}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DashboardDialog open={table.dialogOpen === "dashboard"} onClose={() => table.setDialogOpen(null)} />
|
<DashboardDialog onClose={() => table.setDialogOpen(null)} open={table.dialogOpen === "dashboard"} />
|
||||||
<PackageAddDialog open={table.dialogOpen === "add"} onClose={() => table.setDialogOpen(null)} />
|
<PackageAddDialog onClose={() => table.setDialogOpen(null)} open={table.dialogOpen === "add"} />
|
||||||
<PackageRebuildDialog open={table.dialogOpen === "rebuild"} onClose={() => table.setDialogOpen(null)} />
|
<PackageRebuildDialog onClose={() => table.setDialogOpen(null)} open={table.dialogOpen === "rebuild"} />
|
||||||
<KeyImportDialog open={table.dialogOpen === "keyImport"} onClose={() => table.setDialogOpen(null)} />
|
<KeyImportDialog onClose={() => table.setDialogOpen(null)} open={table.dialogOpen === "keyImport"} />
|
||||||
<PackageInfoDialog
|
<PackageInfoDialog
|
||||||
packageBase={table.selectedPackage}
|
|
||||||
open={table.selectedPackage !== null}
|
|
||||||
onClose={() => table.setSelectedPackage(null)}
|
|
||||||
autoRefreshIntervals={autoRefreshIntervals}
|
autoRefreshIntervals={autoRefreshIntervals}
|
||||||
|
onClose={() => table.setSelectedPackage(null)}
|
||||||
|
open={table.selectedPackage !== null}
|
||||||
|
packageBase={table.selectedPackage}
|
||||||
/>
|
/>
|
||||||
</Box>;
|
</Box>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,47 +43,47 @@ export interface AutoRefreshProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolbarActions {
|
export interface ToolbarActions {
|
||||||
onDashboardClick: () => void;
|
|
||||||
onAddClick: () => void;
|
onAddClick: () => void;
|
||||||
onUpdateClick: () => void;
|
onDashboardClick: () => void;
|
||||||
onRefreshDatabaseClick: () => void;
|
|
||||||
onRebuildClick: () => void;
|
|
||||||
onRemoveClick: () => void;
|
|
||||||
onKeyImportClick: () => void;
|
|
||||||
onReloadClick: () => void;
|
|
||||||
onExportClick: () => void;
|
onExportClick: () => void;
|
||||||
|
onKeyImportClick: () => void;
|
||||||
|
onRebuildClick: () => void;
|
||||||
|
onRefreshDatabaseClick: () => void;
|
||||||
|
onReloadClick: () => void;
|
||||||
|
onRemoveClick: () => void;
|
||||||
|
onUpdateClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PackageTableToolbarProps {
|
interface PackageTableToolbarProps {
|
||||||
|
actions: ToolbarActions;
|
||||||
|
autoRefresh: AutoRefreshProps;
|
||||||
hasSelection: boolean;
|
hasSelection: boolean;
|
||||||
isAuthorized: boolean;
|
isAuthorized: boolean;
|
||||||
status?: BuildStatus;
|
|
||||||
searchText: string;
|
|
||||||
onSearchChange: (text: string) => void;
|
onSearchChange: (text: string) => void;
|
||||||
autoRefresh: AutoRefreshProps;
|
searchText: string;
|
||||||
actions: ToolbarActions;
|
status?: BuildStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PackageTableToolbar({
|
export default function PackageTableToolbar({
|
||||||
|
actions,
|
||||||
|
autoRefresh,
|
||||||
hasSelection,
|
hasSelection,
|
||||||
isAuthorized,
|
isAuthorized,
|
||||||
status,
|
|
||||||
searchText,
|
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
autoRefresh,
|
searchText,
|
||||||
actions,
|
status,
|
||||||
}: PackageTableToolbarProps): React.JSX.Element {
|
}: PackageTableToolbarProps): React.JSX.Element {
|
||||||
const [packagesAnchorEl, setPackagesAnchorEl] = useState<HTMLElement | null>(null);
|
const [packagesAnchorEl, setPackagesAnchorEl] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
return <Box sx={{ display: "flex", gap: 1, mb: 1, flexWrap: "wrap", alignItems: "center" }}>
|
return <Box sx={{ alignItems: "center", display: "flex", flexWrap: "wrap", gap: 1, mb: 1 }}>
|
||||||
<Tooltip title="System health">
|
<Tooltip title="System health">
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="System health"
|
aria-label="System health"
|
||||||
onClick={actions.onDashboardClick}
|
onClick={actions.onDashboardClick}
|
||||||
sx={{
|
sx={{
|
||||||
borderColor: status ? StatusColors[status] : undefined,
|
borderColor: status ? StatusColors[status] : undefined,
|
||||||
borderWidth: 1,
|
|
||||||
borderStyle: "solid",
|
borderStyle: "solid",
|
||||||
|
borderWidth: 1,
|
||||||
color: status ? StatusColors[status] : undefined,
|
color: status ? StatusColors[status] : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -94,16 +94,16 @@ export default function PackageTableToolbar({
|
|||||||
{isAuthorized &&
|
{isAuthorized &&
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
|
||||||
startIcon={<InventoryIcon />}
|
|
||||||
onClick={event => setPackagesAnchorEl(event.currentTarget)}
|
onClick={event => setPackagesAnchorEl(event.currentTarget)}
|
||||||
|
startIcon={<InventoryIcon />}
|
||||||
|
variant="contained"
|
||||||
>
|
>
|
||||||
packages
|
packages
|
||||||
</Button>
|
</Button>
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={packagesAnchorEl}
|
anchorEl={packagesAnchorEl}
|
||||||
open={Boolean(packagesAnchorEl)}
|
|
||||||
onClose={() => setPackagesAnchorEl(null)}
|
onClose={() => setPackagesAnchorEl(null)}
|
||||||
|
open={Boolean(packagesAnchorEl)}
|
||||||
>
|
>
|
||||||
<MenuItem onClick={() => {
|
<MenuItem onClick={() => {
|
||||||
setPackagesAnchorEl(null); actions.onAddClick();
|
setPackagesAnchorEl(null); actions.onAddClick();
|
||||||
@@ -126,58 +126,58 @@ export default function PackageTableToolbar({
|
|||||||
<ReplayIcon fontSize="small" sx={{ mr: 1 }} /> rebuild
|
<ReplayIcon fontSize="small" sx={{ mr: 1 }} /> rebuild
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
<MenuItem onClick={() => {
|
<MenuItem disabled={!hasSelection} onClick={() => {
|
||||||
setPackagesAnchorEl(null); actions.onRemoveClick();
|
setPackagesAnchorEl(null); actions.onRemoveClick();
|
||||||
}} disabled={!hasSelection}>
|
}}>
|
||||||
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> remove
|
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> remove
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
<Button variant="contained" color="info" startIcon={<VpnKeyIcon />} onClick={actions.onKeyImportClick}>
|
<Button color="info" onClick={actions.onKeyImportClick} startIcon={<VpnKeyIcon />} variant="contained">
|
||||||
import key
|
import key
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
<Button variant="outlined" color="secondary" startIcon={<RefreshIcon />} onClick={actions.onReloadClick}>
|
<Button color="secondary" onClick={actions.onReloadClick} startIcon={<RefreshIcon />} variant="outlined">
|
||||||
reload
|
reload
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<AutoRefreshControl
|
<AutoRefreshControl
|
||||||
intervals={autoRefresh.autoRefreshIntervals}
|
|
||||||
currentInterval={autoRefresh.currentInterval}
|
currentInterval={autoRefresh.currentInterval}
|
||||||
|
intervals={autoRefresh.autoRefreshIntervals}
|
||||||
onIntervalChange={autoRefresh.onIntervalChange}
|
onIntervalChange={autoRefresh.onIntervalChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box sx={{ flexGrow: 1 }} />
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
|
||||||
aria-label="Search packages"
|
aria-label="Search packages"
|
||||||
placeholder="search packages..."
|
|
||||||
value={searchText}
|
|
||||||
onChange={event => onSearchChange(event.target.value)}
|
onChange={event => onSearchChange(event.target.value)}
|
||||||
|
placeholder="search packages..."
|
||||||
|
size="small"
|
||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
|
endAdornment: searchText ?
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton aria-label="Clear search" onClick={() => onSearchChange("")} size="small">
|
||||||
|
<ClearIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
: undefined,
|
||||||
startAdornment:
|
startAdornment:
|
||||||
<InputAdornment position="start">
|
<InputAdornment position="start">
|
||||||
<SearchIcon fontSize="small" />
|
<SearchIcon fontSize="small" />
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
,
|
,
|
||||||
endAdornment: searchText ?
|
|
||||||
<InputAdornment position="end">
|
|
||||||
<IconButton size="small" aria-label="Clear search" onClick={() => onSearchChange("")}>
|
|
||||||
<ClearIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
sx={{ minWidth: 200 }}
|
sx={{ minWidth: 200 }}
|
||||||
|
value={searchText}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tooltip title="Export CSV">
|
<Tooltip title="Export CSV">
|
||||||
<IconButton size="small" aria-label="Export CSV" onClick={actions.onExportClick}>
|
<IconButton aria-label="Export CSV" onClick={actions.onExportClick} size="small">
|
||||||
<FileDownloadIcon fontSize="small" />
|
<FileDownloadIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -17,17 +17,20 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* 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 { Chip } from "@mui/material";
|
import { Chip } 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";
|
||||||
|
|
||||||
interface StatusCellProps {
|
interface StatusCellProps {
|
||||||
|
isHeld?: boolean;
|
||||||
status: BuildStatus;
|
status: BuildStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StatusCell({ status }: StatusCellProps): React.JSX.Element {
|
export default function StatusCell({ isHeld, status }: StatusCellProps): React.JSX.Element {
|
||||||
return <Chip
|
return <Chip
|
||||||
|
icon={isHeld ? <PauseCircleIcon /> : undefined}
|
||||||
label={status}
|
label={status}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ interface AuthState {
|
|||||||
|
|
||||||
export interface AuthContextValue extends AuthState {
|
export interface AuthContextValue extends AuthState {
|
||||||
isAuthorized: boolean;
|
isAuthorized: boolean;
|
||||||
setAuthState: (state: AuthState) => void;
|
|
||||||
login: (username: string, password: string) => Promise<void>;
|
login: (username: string, password: string) => Promise<void>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
|
setAuthState: (state: AuthState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthContext = createContext<AuthContextValue | null>(null);
|
export const AuthContext = createContext<AuthContextValue | null>(null);
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ import React, { type ReactNode, useMemo } from "react";
|
|||||||
export function ClientProvider({ children }: { children: ReactNode }): React.JSX.Element {
|
export function ClientProvider({ children }: { children: ReactNode }): React.JSX.Element {
|
||||||
const client = useMemo(() => new AhrimanClient(), []);
|
const client = useMemo(() => new AhrimanClient(), []);
|
||||||
|
|
||||||
return (
|
return <ClientContext.Provider value={client}>
|
||||||
<ClientContext.Provider value={client}>
|
|
||||||
{children}
|
{children}
|
||||||
</ClientContext.Provider>
|
</ClientContext.Provider>;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
import { createContext } from "react";
|
import { createContext } from "react";
|
||||||
|
|
||||||
export interface NotificationContextValue {
|
export interface NotificationContextValue {
|
||||||
showSuccess: (title: string, message: string) => void;
|
|
||||||
showError: (title: string, message: string) => void;
|
showError: (title: string, message: string) => void;
|
||||||
|
showSuccess: (title: string, message: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NotificationContext = createContext<NotificationContextValue | null>(null);
|
export const NotificationContext = createContext<NotificationContextValue | null>(null);
|
||||||
|
|||||||
@@ -55,17 +55,17 @@ export function NotificationProvider({ children }: { children: ReactNode }): Rea
|
|||||||
{children}
|
{children}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "fixed",
|
|
||||||
top: 16,
|
|
||||||
left: "50%",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
zIndex: theme => theme.zIndex.snackbar,
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: 1,
|
gap: 1,
|
||||||
|
left: "50%",
|
||||||
maxWidth: 500,
|
maxWidth: 500,
|
||||||
width: "100%",
|
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
|
position: "fixed",
|
||||||
|
top: 16,
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
width: "100%",
|
||||||
|
zIndex: theme => theme.zIndex.snackbar,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{notifications.map(notification =>
|
{notifications.map(notification =>
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ import type { RepositoryId } from "models/RepositoryId";
|
|||||||
import { createContext } from "react";
|
import { createContext } from "react";
|
||||||
|
|
||||||
export interface RepositoryContextValue {
|
export interface RepositoryContextValue {
|
||||||
|
currentRepository: RepositoryId | null;
|
||||||
repositories: RepositoryId[];
|
repositories: RepositoryId[];
|
||||||
current: RepositoryId | null;
|
setCurrentRepository: (repository: RepositoryId) => void;
|
||||||
setRepositories: (repositories: RepositoryId[]) => void;
|
setRepositories: (repositories: RepositoryId[]) => void;
|
||||||
setCurrent: (repository: RepositoryId) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RepositoryContext = createContext<RepositoryContextValue | null>(null);
|
export const RepositoryContext = createContext<RepositoryContextValue | null>(null);
|
||||||
|
|||||||
@@ -34,20 +34,20 @@ export function RepositoryProvider({ children }: { children: ReactNode }): React
|
|||||||
const [repositories, setRepositories] = useState<RepositoryId[]>([]);
|
const [repositories, setRepositories] = useState<RepositoryId[]>([]);
|
||||||
const hash = useSyncExternalStore(subscribeToHash, getHashSnapshot);
|
const hash = useSyncExternalStore(subscribeToHash, getHashSnapshot);
|
||||||
|
|
||||||
const current = useMemo(() => {
|
const currentRepository = useMemo(() => {
|
||||||
if (repositories.length === 0) {
|
if (repositories.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return repositories.find(repository => repository.key === hash) ?? repositories[0] ?? null;
|
return repositories.find(repository => repository.key === hash) ?? repositories[0] ?? null;
|
||||||
}, [repositories, hash]);
|
}, [repositories, hash]);
|
||||||
|
|
||||||
const setCurrent = useCallback((repository: RepositoryId) => {
|
const setCurrentRepository = useCallback((repository: RepositoryId) => {
|
||||||
window.location.hash = repository.key;
|
window.location.hash = repository.key;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const value = useMemo(() => ({
|
const value = useMemo(() => ({
|
||||||
repositories, current, setRepositories, setCurrent,
|
repositories, currentRepository, setRepositories, setCurrentRepository,
|
||||||
}), [repositories, current, setCurrent]);
|
}), [repositories, currentRepository, setCurrentRepository]);
|
||||||
|
|
||||||
return <RepositoryContext.Provider value={value}>{children}</RepositoryContext.Provider>;
|
return <RepositoryContext.Provider value={value}>{children}</RepositoryContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|||||||
27
frontend/src/contexts/ThemeContext.ts
Normal file
27
frontend/src/contexts/ThemeContext.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* 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 { createContext } from "react";
|
||||||
|
|
||||||
|
export interface ThemeContextValue {
|
||||||
|
mode: "light" | "dark";
|
||||||
|
toggleTheme: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||||
54
frontend/src/contexts/ThemeProvider.tsx
Normal file
54
frontend/src/contexts/ThemeProvider.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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 CssBaseline from "@mui/material/CssBaseline";
|
||||||
|
import { ThemeProvider as MuiThemeProvider } from "@mui/material/styles";
|
||||||
|
import { defaults as chartDefaults } from "chart.js";
|
||||||
|
import { ThemeContext } from "contexts/ThemeContext";
|
||||||
|
import { useLocalStorage } from "hooks/useLocalStorage";
|
||||||
|
import React, { useCallback, useEffect, useMemo } from "react";
|
||||||
|
import { createAppTheme } from "theme/Theme";
|
||||||
|
|
||||||
|
function systemPreference(): "light" | "dark" {
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }: { children: React.ReactNode }): React.JSX.Element {
|
||||||
|
const [mode, setMode] = useLocalStorage<"light" | "dark">("theme-mode", systemPreference());
|
||||||
|
|
||||||
|
const toggleTheme = useCallback(() => {
|
||||||
|
setMode(prev => prev === "light" ? "dark" : "light");
|
||||||
|
}, [setMode]);
|
||||||
|
|
||||||
|
const theme = useMemo(() => createAppTheme(mode), [mode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
chartDefaults.color = mode === "dark" ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.7)";
|
||||||
|
chartDefaults.borderColor = mode === "dark" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
|
const value = useMemo(() => ({ mode, toggleTheme }), [mode, toggleTheme]);
|
||||||
|
|
||||||
|
return <ThemeContext.Provider value={value}>
|
||||||
|
<MuiThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
{children}
|
||||||
|
</MuiThemeProvider>
|
||||||
|
</ThemeContext.Provider>;
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ import type { RepositoryId } from "models/RepositoryId";
|
|||||||
|
|
||||||
export const QueryKeys = {
|
export const QueryKeys = {
|
||||||
|
|
||||||
|
artifacts: (packageBase: string, repository: RepositoryId) => ["artifacts", repository.key, packageBase] as const,
|
||||||
|
|
||||||
changes: (packageBase: string, repository: RepositoryId) => ["changes", repository.key, packageBase] as const,
|
changes: (packageBase: string, repository: RepositoryId) => ["changes", repository.key, packageBase] as const,
|
||||||
|
|
||||||
dependencies: (packageBase: string, repository: RepositoryId) => ["dependencies", repository.key, packageBase] as const,
|
dependencies: (packageBase: string, repository: RepositoryId) => ["dependencies", repository.key, packageBase] as const,
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
import { type RefObject, useCallback, useRef } from "react";
|
import { type RefObject, useCallback, useRef } from "react";
|
||||||
|
|
||||||
interface UseAutoScrollResult {
|
interface UseAutoScrollResult {
|
||||||
preRef: RefObject<HTMLElement | null>;
|
|
||||||
handleScroll: () => void;
|
handleScroll: () => void;
|
||||||
scrollToBottom: () => void;
|
preRef: RefObject<HTMLElement | null>;
|
||||||
resetScroll: () => void;
|
resetScroll: () => void;
|
||||||
|
scrollToBottom: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAutoScroll(): UseAutoScrollResult {
|
export function useAutoScroll(): UseAutoScrollResult {
|
||||||
@@ -59,5 +59,5 @@ export function useAutoScroll(): UseAutoScrollResult {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { preRef, handleScroll, scrollToBottom, resetScroll };
|
return { handleScroll, preRef, resetScroll, scrollToBottom };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ import { useRepository } from "hooks/useRepository";
|
|||||||
import type { RepositoryId } from "models/RepositoryId";
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
|
||||||
export interface UsePackageActionsResult {
|
export interface UsePackageActionsResult {
|
||||||
handleReload: () => void;
|
|
||||||
handleUpdate: () => Promise<void>;
|
|
||||||
handleRefreshDatabase: () => Promise<void>;
|
handleRefreshDatabase: () => Promise<void>;
|
||||||
|
handleReload: () => void;
|
||||||
handleRemove: () => Promise<void>;
|
handleRemove: () => Promise<void>;
|
||||||
|
handleUpdate: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePackageActions(
|
export function usePackageActions(
|
||||||
@@ -37,7 +37,7 @@ export function usePackageActions(
|
|||||||
setSelectionModel: (model: string[]) => void,
|
setSelectionModel: (model: string[]) => void,
|
||||||
): UsePackageActionsResult {
|
): UsePackageActionsResult {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { current } = useRepository();
|
const { currentRepository } = useRepository();
|
||||||
const { showSuccess, showError } = useNotification();
|
const { showSuccess, showError } = useNotification();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -50,22 +50,22 @@ export function usePackageActions(
|
|||||||
action: (repository: RepositoryId) => Promise<string>,
|
action: (repository: RepositoryId) => Promise<string>,
|
||||||
errorMessage: string,
|
errorMessage: string,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (!current) {
|
if (!currentRepository) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const successMessage = await action(current);
|
const successMessage = await action(currentRepository);
|
||||||
showSuccess("Success", successMessage);
|
showSuccess("Success", successMessage);
|
||||||
invalidate(current);
|
invalidate(currentRepository);
|
||||||
setSelectionModel([]);
|
setSelectionModel([]);
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
showError("Action failed", `${errorMessage}: ${ApiError.errorDetail(exception)}`);
|
showError("Action failed", `${errorMessage}: ${ApiError.errorDetail(exception)}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReload: () => void = () => {
|
const handleReload = (): void => {
|
||||||
if (current !== null) {
|
if (currentRepository !== null) {
|
||||||
invalidate(current);
|
invalidate(currentRepository);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -80,11 +80,11 @@ export function usePackageActions(
|
|||||||
|
|
||||||
const handleRefreshDatabase = (): Promise<void> => performAction(async (repository): Promise<string> => {
|
const handleRefreshDatabase = (): Promise<void> => performAction(async (repository): Promise<string> => {
|
||||||
await client.service.servicePackageUpdate(repository, {
|
await client.service.servicePackageUpdate(repository, {
|
||||||
packages: [],
|
|
||||||
refresh: true,
|
|
||||||
aur: false,
|
aur: false,
|
||||||
local: false,
|
local: false,
|
||||||
manual: false,
|
manual: false,
|
||||||
|
packages: [],
|
||||||
|
refresh: true,
|
||||||
});
|
});
|
||||||
return "Pacman database update has been requested";
|
return "Pacman database update has been requested";
|
||||||
}, "Could not update pacman databases");
|
}, "Could not update pacman databases");
|
||||||
@@ -100,9 +100,9 @@ export function usePackageActions(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleReload,
|
|
||||||
handleUpdate,
|
|
||||||
handleRefreshDatabase,
|
handleRefreshDatabase,
|
||||||
|
handleReload,
|
||||||
handleRemove,
|
handleRemove,
|
||||||
|
handleUpdate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
36
frontend/src/hooks/usePackageChanges.ts
Normal file
36
frontend/src/hooks/usePackageChanges.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useQuery } from "@tanstack/react-query";
|
||||||
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
|
import { useClient } from "hooks/useClient";
|
||||||
|
import type { Changes } from "models/Changes";
|
||||||
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
|
||||||
|
export function usePackageChanges(packageBase: string, repository: RepositoryId): Changes | undefined {
|
||||||
|
const client = useClient();
|
||||||
|
|
||||||
|
const { data } = useQuery<Changes>({
|
||||||
|
enabled: !!packageBase,
|
||||||
|
queryFn: () => client.fetch.fetchPackageChanges(packageBase, repository),
|
||||||
|
queryKey: QueryKeys.changes(packageBase, repository),
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -30,39 +30,39 @@ import { useMemo } from "react";
|
|||||||
import { defaultInterval } from "utils";
|
import { defaultInterval } from "utils";
|
||||||
|
|
||||||
export interface UsePackageDataResult {
|
export interface UsePackageDataResult {
|
||||||
rows: PackageRow[];
|
|
||||||
isLoading: boolean;
|
|
||||||
isAuthorized: boolean;
|
|
||||||
status: BuildStatus | undefined;
|
|
||||||
autoRefresh: ReturnType<typeof useAutoRefresh>;
|
autoRefresh: ReturnType<typeof useAutoRefresh>;
|
||||||
|
isAuthorized: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
rows: PackageRow[];
|
||||||
|
status: BuildStatus | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePackageData(autoRefreshIntervals: AutoRefreshInterval[]): UsePackageDataResult {
|
export function usePackageData(autoRefreshIntervals: AutoRefreshInterval[]): UsePackageDataResult {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { current } = useRepository();
|
const { currentRepository } = useRepository();
|
||||||
const { isAuthorized } = useAuth();
|
const { isAuthorized } = useAuth();
|
||||||
|
|
||||||
const autoRefresh = useAutoRefresh("table-autoreload-button", defaultInterval(autoRefreshIntervals));
|
const autoRefresh = useAutoRefresh("table-autoreload-button", defaultInterval(autoRefreshIntervals));
|
||||||
|
|
||||||
const { data: packages = [], isLoading } = useQuery({
|
const { data: packages = [], isLoading } = useQuery({
|
||||||
queryKey: current ? QueryKeys.packages(current) : ["packages"],
|
queryFn: currentRepository ? () => client.fetch.fetchPackages(currentRepository) : skipToken,
|
||||||
queryFn: current ? () => client.fetch.fetchPackages(current) : skipToken,
|
queryKey: currentRepository ? QueryKeys.packages(currentRepository) : ["packages"],
|
||||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: status } = useQuery({
|
const { data: status } = useQuery({
|
||||||
queryKey: current ? QueryKeys.status(current) : ["status"],
|
queryFn: currentRepository ? () => client.fetch.fetchServerStatus(currentRepository) : skipToken,
|
||||||
queryFn: current ? () => client.fetch.fetchServerStatus(current) : skipToken,
|
queryKey: currentRepository ? QueryKeys.status(currentRepository) : ["status"],
|
||||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rows = useMemo(() => packages.map(descriptor => new PackageRow(descriptor)), [packages]);
|
const rows = useMemo(() => packages.map(descriptor => new PackageRow(descriptor)), [packages]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows,
|
autoRefresh,
|
||||||
isLoading,
|
isLoading,
|
||||||
isAuthorized,
|
isAuthorized,
|
||||||
|
rows,
|
||||||
status: status?.status.status,
|
status: status?.status.status,
|
||||||
autoRefresh,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,35 +27,30 @@ import type { PackageRow } from "models/PackageRow";
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export interface UsePackageTableResult {
|
export interface UsePackageTableResult {
|
||||||
rows: PackageRow[];
|
|
||||||
isLoading: boolean;
|
|
||||||
isAuthorized: boolean;
|
|
||||||
status: BuildStatus | undefined;
|
|
||||||
|
|
||||||
selectionModel: string[];
|
|
||||||
setSelectionModel: (model: string[]) => void;
|
|
||||||
|
|
||||||
dialogOpen: "dashboard" | "add" | "rebuild" | "keyImport" | null;
|
|
||||||
setDialogOpen: (dialog: "dashboard" | "add" | "rebuild" | "keyImport" | null) => void;
|
|
||||||
selectedPackage: string | null;
|
|
||||||
setSelectedPackage: (base: string | null) => void;
|
|
||||||
|
|
||||||
paginationModel: { pageSize: number; page: number };
|
|
||||||
setPaginationModel: (model: { pageSize: number; page: number }) => void;
|
|
||||||
columnVisibility: Record<string, boolean>;
|
|
||||||
setColumnVisibility: (model: Record<string, boolean>) => void;
|
|
||||||
filterModel: GridFilterModel;
|
|
||||||
setFilterModel: (model: GridFilterModel) => void;
|
|
||||||
searchText: string;
|
|
||||||
setSearchText: (text: string) => void;
|
|
||||||
|
|
||||||
autoRefreshInterval: number;
|
autoRefreshInterval: number;
|
||||||
onAutoRefreshIntervalChange: (interval: number) => void;
|
columnVisibility: Record<string, boolean>;
|
||||||
|
dialogOpen: "dashboard" | "add" | "rebuild" | "keyImport" | null;
|
||||||
handleReload: () => void;
|
filterModel: GridFilterModel;
|
||||||
handleUpdate: () => Promise<void>;
|
|
||||||
handleRefreshDatabase: () => Promise<void>;
|
handleRefreshDatabase: () => Promise<void>;
|
||||||
|
handleReload: () => void;
|
||||||
handleRemove: () => Promise<void>;
|
handleRemove: () => Promise<void>;
|
||||||
|
handleUpdate: () => Promise<void>;
|
||||||
|
isAuthorized: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
onAutoRefreshIntervalChange: (interval: number) => void;
|
||||||
|
paginationModel: { page: number; pageSize: number };
|
||||||
|
rows: PackageRow[];
|
||||||
|
searchText: string;
|
||||||
|
selectedPackage: string | null;
|
||||||
|
selectionModel: string[];
|
||||||
|
setColumnVisibility: (model: Record<string, boolean>) => void;
|
||||||
|
setDialogOpen: (dialog: "dashboard" | "add" | "rebuild" | "keyImport" | null) => void;
|
||||||
|
setFilterModel: (model: GridFilterModel) => void;
|
||||||
|
setPaginationModel: (model: { page: number; pageSize: number }) => void;
|
||||||
|
setSearchText: (text: string) => void;
|
||||||
|
setSelectedPackage: (base: string | null) => void;
|
||||||
|
setSelectionModel: (model: string[]) => void;
|
||||||
|
status: BuildStatus | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePackageTable(autoRefreshIntervals: AutoRefreshInterval[]): UsePackageTableResult {
|
export function usePackageTable(autoRefreshIntervals: AutoRefreshInterval[]): UsePackageTableResult {
|
||||||
@@ -71,16 +66,13 @@ export function usePackageTable(autoRefreshIntervals: AutoRefreshInterval[]): Us
|
|||||||
}, [isDialogOpen, setPaused]);
|
}, [isDialogOpen, setPaused]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows,
|
autoRefreshInterval: autoRefresh.interval,
|
||||||
isLoading,
|
isLoading,
|
||||||
isAuthorized,
|
isAuthorized,
|
||||||
status,
|
|
||||||
|
|
||||||
...tableState,
|
|
||||||
|
|
||||||
autoRefreshInterval: autoRefresh.interval,
|
|
||||||
onAutoRefreshIntervalChange: autoRefresh.setInterval,
|
onAutoRefreshIntervalChange: autoRefresh.setInterval,
|
||||||
|
rows,
|
||||||
|
status,
|
||||||
...actions,
|
...actions,
|
||||||
|
...tableState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,17 +22,17 @@ import type { RepositoryId } from "models/RepositoryId";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export interface SelectedRepositoryResult {
|
export interface SelectedRepositoryResult {
|
||||||
selectedKey: string;
|
|
||||||
setSelectedKey: (key: string) => void;
|
|
||||||
selectedRepository: RepositoryId | null;
|
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
|
selectedKey: string;
|
||||||
|
selectedRepository: RepositoryId | null;
|
||||||
|
setSelectedKey: (key: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSelectedRepository(): SelectedRepositoryResult {
|
export function useSelectedRepository(): SelectedRepositoryResult {
|
||||||
const { repositories, current } = useRepository();
|
const { repositories, currentRepository } = useRepository();
|
||||||
const [selectedKey, setSelectedKey] = useState("");
|
const [selectedKey, setSelectedKey] = useState("");
|
||||||
|
|
||||||
let selectedRepository: RepositoryId | null = current;
|
let selectedRepository: RepositoryId | null = currentRepository;
|
||||||
if (selectedKey) {
|
if (selectedKey) {
|
||||||
const repository = repositories.find(repository => repository.key === selectedKey);
|
const repository = repositories.find(repository => repository.key === selectedKey);
|
||||||
if (repository) {
|
if (repository) {
|
||||||
@@ -40,9 +40,9 @@ export function useSelectedRepository(): SelectedRepositoryResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reset: () => void = () => {
|
const reset = (): void => {
|
||||||
setSelectedKey("");
|
setSelectedKey("");
|
||||||
};
|
};
|
||||||
|
|
||||||
return { selectedKey, setSelectedKey, selectedRepository, reset };
|
return { reset, selectedKey, selectedRepository, setSelectedKey };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,22 +24,20 @@ import { useState } from "react";
|
|||||||
export type DialogType = "dashboard" | "add" | "rebuild" | "keyImport";
|
export type DialogType = "dashboard" | "add" | "rebuild" | "keyImport";
|
||||||
|
|
||||||
export interface UseTableStateResult {
|
export interface UseTableStateResult {
|
||||||
selectionModel: string[];
|
|
||||||
setSelectionModel: (model: string[]) => void;
|
|
||||||
|
|
||||||
dialogOpen: DialogType | null;
|
|
||||||
setDialogOpen: (dialog: DialogType | null) => void;
|
|
||||||
selectedPackage: string | null;
|
|
||||||
setSelectedPackage: (base: string | null) => void;
|
|
||||||
|
|
||||||
paginationModel: { pageSize: number; page: number };
|
|
||||||
setPaginationModel: (model: { pageSize: number; page: number }) => void;
|
|
||||||
columnVisibility: Record<string, boolean>;
|
columnVisibility: Record<string, boolean>;
|
||||||
setColumnVisibility: (model: Record<string, boolean>) => void;
|
dialogOpen: DialogType | null;
|
||||||
filterModel: GridFilterModel;
|
filterModel: GridFilterModel;
|
||||||
setFilterModel: (model: GridFilterModel) => void;
|
paginationModel: { pageSize: number; page: number };
|
||||||
searchText: string;
|
searchText: string;
|
||||||
|
selectedPackage: string | null;
|
||||||
|
selectionModel: string[];
|
||||||
|
setColumnVisibility: (model: Record<string, boolean>) => void;
|
||||||
|
setDialogOpen: (dialog: DialogType | null) => void;
|
||||||
|
setFilterModel: (model: GridFilterModel) => void;
|
||||||
|
setPaginationModel: (model: { pageSize: number; page: number }) => void;
|
||||||
setSearchText: (text: string) => void;
|
setSearchText: (text: string) => void;
|
||||||
|
setSelectedPackage: (base: string | null) => void;
|
||||||
|
setSelectionModel: (model: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTableState(): UseTableStateResult {
|
export function useTableState(): UseTableStateResult {
|
||||||
@@ -49,8 +47,8 @@ export function useTableState(): UseTableStateResult {
|
|||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
|
|
||||||
const [paginationModel, setPaginationModel] = useLocalStorage("ahriman-packages-pagination", {
|
const [paginationModel, setPaginationModel] = useLocalStorage("ahriman-packages-pagination", {
|
||||||
pageSize: 10,
|
|
||||||
page: 0,
|
page: 0,
|
||||||
|
pageSize: 25,
|
||||||
});
|
});
|
||||||
const [columnVisibility, setColumnVisibility] = useLocalStorage<Record<string, boolean>>(
|
const [columnVisibility, setColumnVisibility] = useLocalStorage<Record<string, boolean>>(
|
||||||
"ahriman-packages-columns",
|
"ahriman-packages-columns",
|
||||||
@@ -62,21 +60,19 @@ export function useTableState(): UseTableStateResult {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectionModel,
|
|
||||||
setSelectionModel,
|
|
||||||
|
|
||||||
dialogOpen,
|
|
||||||
setDialogOpen,
|
|
||||||
selectedPackage,
|
|
||||||
setSelectedPackage,
|
|
||||||
|
|
||||||
paginationModel,
|
|
||||||
setPaginationModel,
|
|
||||||
columnVisibility,
|
columnVisibility,
|
||||||
setColumnVisibility,
|
dialogOpen,
|
||||||
filterModel,
|
filterModel,
|
||||||
setFilterModel,
|
paginationModel,
|
||||||
searchText,
|
searchText,
|
||||||
|
selectedPackage,
|
||||||
|
selectionModel,
|
||||||
|
setColumnVisibility,
|
||||||
|
setDialogOpen,
|
||||||
|
setFilterModel,
|
||||||
|
setPaginationModel,
|
||||||
setSearchText,
|
setSearchText,
|
||||||
|
setSelectedPackage,
|
||||||
|
setSelectionModel,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
25
frontend/src/hooks/useThemeMode.ts
Normal file
25
frontend/src/hooks/useThemeMode.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* 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 { ThemeContext, type ThemeContextValue } from "contexts/ThemeContext";
|
||||||
|
import { useContextNotNull } from "hooks/useContextNotNull";
|
||||||
|
|
||||||
|
export function useThemeMode(): ThemeContextValue {
|
||||||
|
return useContextNotNull(ThemeContext);
|
||||||
|
}
|
||||||
@@ -18,6 +18,6 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
export interface AURPackage {
|
export interface AURPackage {
|
||||||
package: string;
|
|
||||||
description: string;
|
description: string;
|
||||||
|
package: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,4 +17,4 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
export type BuildStatus = "unknown" | "pending" | "building" | "failed" | "success";
|
export type BuildStatus = "building" | "failed" | "pending" | "success" | "unknown";
|
||||||
|
|||||||
@@ -20,4 +20,5 @@
|
|||||||
export interface Changes {
|
export interface Changes {
|
||||||
changes?: string;
|
changes?: string;
|
||||||
last_commit_sha?: string;
|
last_commit_sha?: string;
|
||||||
|
pkgbuild?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ import type { RepositoryId } from "models/RepositoryId";
|
|||||||
|
|
||||||
export interface InfoResponse {
|
export interface InfoResponse {
|
||||||
auth: AuthInfo;
|
auth: AuthInfo;
|
||||||
repositories: RepositoryId[];
|
|
||||||
version: string;
|
|
||||||
autorefresh_intervals: AutoRefreshInterval[];
|
autorefresh_intervals: AutoRefreshInterval[];
|
||||||
docs_enabled: boolean;
|
docs_enabled: boolean;
|
||||||
index_url?: string;
|
index_url?: string;
|
||||||
|
repositories: RepositoryId[];
|
||||||
|
version: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ import type { Status } from "models/Status";
|
|||||||
|
|
||||||
export interface InternalStatus {
|
export interface InternalStatus {
|
||||||
architecture: string;
|
architecture: string;
|
||||||
repository: string;
|
|
||||||
packages: Counters;
|
packages: Counters;
|
||||||
|
repository: string;
|
||||||
stats: RepositoryStats;
|
stats: RepositoryStats;
|
||||||
status: Status;
|
status: Status;
|
||||||
version: string;
|
version: string;
|
||||||
|
|||||||
@@ -18,6 +18,6 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
export interface LoginRequest {
|
export interface LoginRequest {
|
||||||
username: string;
|
|
||||||
password: string;
|
password: string;
|
||||||
|
username: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import type { AlertColor } from "@mui/material";
|
|||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
|
||||||
message: string;
|
message: string;
|
||||||
severity: AlertColor;
|
severity: AlertColor;
|
||||||
|
title: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
import type { Patch } from "models/Patch";
|
import type { Patch } from "models/Patch";
|
||||||
|
|
||||||
export interface PackageActionRequest {
|
export interface PackageActionRequest {
|
||||||
packages: string[];
|
|
||||||
patches?: Patch[];
|
|
||||||
refresh?: boolean;
|
|
||||||
aur?: boolean;
|
aur?: boolean;
|
||||||
local?: boolean;
|
local?: boolean;
|
||||||
manual?: boolean;
|
manual?: boolean;
|
||||||
|
packages: string[];
|
||||||
|
patches?: Patch[];
|
||||||
|
refresh?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,30 +21,31 @@ import type { BuildStatus } from "models/BuildStatus";
|
|||||||
import type { PackageStatus } from "models/PackageStatus";
|
import type { PackageStatus } from "models/PackageStatus";
|
||||||
|
|
||||||
export class PackageRow {
|
export class PackageRow {
|
||||||
id: string;
|
|
||||||
base: string;
|
base: string;
|
||||||
webUrl?: string;
|
|
||||||
version: string;
|
|
||||||
packages: string[];
|
|
||||||
groups: string[];
|
groups: string[];
|
||||||
|
id: string;
|
||||||
|
isHeld: boolean;
|
||||||
licenses: string[];
|
licenses: string[];
|
||||||
packager: string;
|
packager: string;
|
||||||
timestamp: string;
|
packages: string[];
|
||||||
timestampValue: number;
|
|
||||||
status: BuildStatus;
|
status: BuildStatus;
|
||||||
|
timestamp: string;
|
||||||
|
version: string;
|
||||||
|
webUrl?: string;
|
||||||
|
|
||||||
constructor(descriptor: PackageStatus) {
|
constructor(descriptor: PackageStatus) {
|
||||||
this.id = descriptor.package.base;
|
|
||||||
this.base = descriptor.package.base;
|
this.base = descriptor.package.base;
|
||||||
this.webUrl = descriptor.package.remote.web_url ?? undefined;
|
|
||||||
this.version = descriptor.package.version;
|
|
||||||
this.packages = Object.keys(descriptor.package.packages).sort();
|
|
||||||
this.groups = PackageRow.extractListProperties(descriptor.package, "groups");
|
this.groups = PackageRow.extractListProperties(descriptor.package, "groups");
|
||||||
|
this.id = descriptor.package.base;
|
||||||
|
this.isHeld = descriptor.status.is_held ?? false;
|
||||||
this.licenses = PackageRow.extractListProperties(descriptor.package, "licenses");
|
this.licenses = PackageRow.extractListProperties(descriptor.package, "licenses");
|
||||||
this.packager = descriptor.package.packager ?? "";
|
this.packager = descriptor.package.packager ?? "";
|
||||||
this.timestamp = new Date(descriptor.status.timestamp * 1000).toISOStringShort();
|
this.packages = Object.keys(descriptor.package.packages).sort();
|
||||||
this.timestampValue = descriptor.status.timestamp;
|
|
||||||
this.status = descriptor.status.status;
|
this.status = descriptor.status.status;
|
||||||
|
this.timestamp = new Date(descriptor.status.timestamp * 1000).toISOStringShort();
|
||||||
|
this.version = descriptor.package.version;
|
||||||
|
this.webUrl = descriptor.package.remote.web_url ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static extractListProperties(pkg: PackageStatus["package"], property: "groups" | "licenses"): string[] {
|
private static extractListProperties(pkg: PackageStatus["package"], property: "groups" | "licenses"): string[] {
|
||||||
|
|||||||
@@ -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/>.
|
||||||
*/
|
*/
|
||||||
export class RepositoryId {
|
export class RepositoryId {
|
||||||
|
|
||||||
readonly architecture: string;
|
readonly architecture: string;
|
||||||
readonly repository: string;
|
readonly repository: string;
|
||||||
|
|
||||||
|
|||||||
23
frontend/src/models/RollbackRequest.ts
Normal file
23
frontend/src/models/RollbackRequest.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
export interface RollbackRequest {
|
||||||
|
package: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
import type { BuildStatus } from "models/BuildStatus";
|
import type { BuildStatus } from "models/BuildStatus";
|
||||||
|
|
||||||
export interface Status {
|
export interface Status {
|
||||||
|
is_held?: boolean;
|
||||||
status: BuildStatus;
|
status: BuildStatus;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,19 +21,19 @@ import { amber, green, grey, orange, red } from "@mui/material/colors";
|
|||||||
import type { BuildStatus } from "models/BuildStatus";
|
import type { BuildStatus } from "models/BuildStatus";
|
||||||
|
|
||||||
const base: Record<BuildStatus, string> = {
|
const base: Record<BuildStatus, string> = {
|
||||||
unknown: grey[800],
|
building: orange[800],
|
||||||
pending: amber[900],
|
failed: red[700],
|
||||||
building: orange[900],
|
pending: amber[700],
|
||||||
failed: red[900],
|
success: green[700],
|
||||||
success: green[800],
|
unknown: grey[600],
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerBase: Record<BuildStatus, string> = {
|
const headerBase: Record<BuildStatus, string> = {
|
||||||
unknown: grey[800],
|
|
||||||
pending: amber[700],
|
|
||||||
building: orange[600],
|
building: orange[600],
|
||||||
failed: red[500],
|
failed: red[500],
|
||||||
|
pending: amber[700],
|
||||||
success: green[600],
|
success: green[600],
|
||||||
|
unknown: grey[600],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StatusColors = base;
|
export const StatusColors = base;
|
||||||
|
|||||||
@@ -17,17 +17,28 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* 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 } from "@mui/material/styles";
|
import { createTheme, type Theme } from "@mui/material/styles";
|
||||||
|
|
||||||
const Theme = createTheme({
|
export function createAppTheme(mode: "light" | "dark"): Theme {
|
||||||
|
return createTheme({
|
||||||
components: {
|
components: {
|
||||||
|
MuiButton: {
|
||||||
|
styleOverrides: {
|
||||||
|
startIcon: {
|
||||||
|
alignItems: "center",
|
||||||
|
display: "flex",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
MuiDialog: {
|
MuiDialog: {
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
maxWidth: "lg",
|
|
||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
|
maxWidth: "lg",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
palette: {
|
||||||
|
mode,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
export default Theme;
|
|
||||||
|
|||||||
@@ -19,6 +19,14 @@
|
|||||||
*/
|
*/
|
||||||
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
||||||
|
|
||||||
|
export const DETAIL_TABLE_PROPS = {
|
||||||
|
density: "compact" as const,
|
||||||
|
disableColumnSorting: true,
|
||||||
|
disableRowSelectionOnClick: true,
|
||||||
|
paginationModel: { page: 0, pageSize: 25 },
|
||||||
|
sx: { height: 400, mt: 1 },
|
||||||
|
};
|
||||||
|
|
||||||
export function defaultInterval(intervals: AutoRefreshInterval[]): number {
|
export function defaultInterval(intervals: AutoRefreshInterval[]): number {
|
||||||
return intervals.find(interval => interval.is_active)?.interval ?? 0;
|
return intervals.find(interval => interval.is_active)?.interval ?? 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,18 @@
|
|||||||
"baseUrl": "src",
|
"baseUrl": "src",
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedIndexedAccess": true,
|
"noImplicitOverride": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"target": "ES2020",
|
"target": "ESNext",
|
||||||
"useDefineForClassFields": true
|
"useDefineForClassFields": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
|
|||||||
@@ -1,35 +1,37 @@
|
|||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { defineConfig, type Plugin } from "vite";
|
import { defineConfig, type Plugin } from "vite";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
|
||||||
|
|
||||||
function rename(oldName: string, newName: string): Plugin {
|
function rename(oldName: string, newName: string): Plugin {
|
||||||
return {
|
return {
|
||||||
name: "rename",
|
|
||||||
enforce: "post",
|
enforce: "post",
|
||||||
generateBundle(_, bundle) {
|
generateBundle(_, bundle) {
|
||||||
if (bundle[oldName]) {
|
if (bundle[oldName]) {
|
||||||
bundle[oldName].fileName = newName;
|
bundle[oldName].fileName = newName;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
name: "rename",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tsconfigPaths(), rename("index.html", "build-status.jinja2")],
|
|
||||||
base: "/",
|
base: "/",
|
||||||
build: {
|
build: {
|
||||||
chunkSizeWarningLimit: 10000,
|
chunkSizeWarningLimit: 10000,
|
||||||
emptyOutDir: false,
|
emptyOutDir: false,
|
||||||
outDir: path.resolve(__dirname, "../package/share/ahriman/templates"),
|
outDir: path.resolve(__dirname, "../package/share/ahriman/templates"),
|
||||||
rollupOptions: {
|
rolldownOptions: {
|
||||||
output: {
|
output: {
|
||||||
entryFileNames: "static/[name].js",
|
|
||||||
chunkFileNames: "static/[name].js",
|
|
||||||
assetFileNames: "static/[name].[ext]",
|
assetFileNames: "static/[name].[ext]",
|
||||||
|
chunkFileNames: "static/[name].js",
|
||||||
|
entryFileNames: "static/[name].js",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
plugins: [react(), rename("index.html", "build-status.jinja2")],
|
||||||
|
resolve: {
|
||||||
|
tsconfigPaths: true,
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": "http://localhost:8080",
|
"/api": "http://localhost:8080",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
pkgbase='ahriman'
|
pkgbase='ahriman'
|
||||||
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
|
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
|
||||||
pkgver=2.20.0rc7
|
pkgver=2.20.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="ArcH linux ReposItory MANager"
|
pkgdesc="ArcH linux ReposItory MANager"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.TH AHRIMAN "1" "2026\-03\-08" "ahriman 2.20.0rc7" "ArcH linux ReposItory MANager"
|
.TH AHRIMAN "1" "2026\-03\-08" "ahriman 2.20.0" "ArcH linux ReposItory MANager"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
ahriman \- ArcH linux ReposItory MANager
|
ahriman \- ArcH linux ReposItory MANager
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
|
|||||||
@@ -17,4 +17,4 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2.20.0rc7"
|
__version__ = "2.20.0"
|
||||||
|
|||||||
@@ -154,13 +154,13 @@ class Application(ApplicationPackages, ApplicationRepository):
|
|||||||
for package_name, packager in missing.items():
|
for package_name, packager in missing.items():
|
||||||
if (source_dir := self.repository.paths.cache_for(package_name)).is_dir():
|
if (source_dir := self.repository.paths.cache_for(package_name)).is_dir():
|
||||||
# there is local cache, load package from it
|
# there is local cache, load package from it
|
||||||
leaf = Package.from_build(source_dir, self.repository.architecture, packager)
|
leaf = Package.from_build(source_dir, self.repository.repository_id.architecture, packager)
|
||||||
else:
|
else:
|
||||||
leaf = Package.from_aur(package_name, packager, include_provides=True)
|
leaf = Package.from_aur(package_name, packager, include_provides=True)
|
||||||
portion[leaf.base] = leaf
|
portion[leaf.base] = leaf
|
||||||
|
|
||||||
# register package in the database
|
# register package in the database
|
||||||
self.repository.reporter.set_unknown(leaf)
|
self.reporter.set_unknown(leaf)
|
||||||
|
|
||||||
return portion
|
return portion
|
||||||
|
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ class ApplicationRepository(ApplicationProperties):
|
|||||||
if last_commit_sha is None:
|
if last_commit_sha is None:
|
||||||
continue # skip check in case if we can't calculate diff
|
continue # skip check in case if we can't calculate diff
|
||||||
|
|
||||||
changes = self.repository.package_changes(package, last_commit_sha)
|
if (changes := self.repository.package_changes(package, last_commit_sha)) is not None:
|
||||||
self.repository.reporter.package_changes_update(package.base, changes)
|
self.reporter.package_changes_update(package.base, changes)
|
||||||
|
|
||||||
def clean(self, *, cache: bool, chroot: bool, manual: bool, packages: bool, pacman: bool) -> None:
|
def clean(self, *, cache: bool, chroot: bool, manual: bool, packages: bool, pacman: bool) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -156,12 +156,15 @@ class ApplicationRepository(ApplicationProperties):
|
|||||||
result = Result()
|
result = Result()
|
||||||
|
|
||||||
# process already built packages if any
|
# process already built packages if any
|
||||||
built_packages = self.repository.packages_built()
|
if built_packages := self.repository.packages_built(): # speedup a bit
|
||||||
if built_packages: # speedup a bit
|
|
||||||
build_result = self.repository.process_update(built_packages, packagers)
|
build_result = self.repository.process_update(built_packages, packagers)
|
||||||
self.on_result(build_result)
|
self.on_result(build_result)
|
||||||
result.merge(build_result)
|
result.merge(build_result)
|
||||||
|
|
||||||
|
# filter packages which were prebuilt
|
||||||
|
succeeded = {package.base for package in build_result.success}
|
||||||
|
updates = [package for package in updates if package.base not in succeeded]
|
||||||
|
|
||||||
builder = Updater.load(self.repository_id, self.configuration, self.repository)
|
builder = Updater.load(self.repository_id, self.configuration, self.repository)
|
||||||
|
|
||||||
# ok so for now we split all packages into chunks and process each chunk accordingly
|
# ok so for now we split all packages into chunks and process each chunk accordingly
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ import argparse
|
|||||||
|
|
||||||
from ahriman.application.application import Application
|
from ahriman.application.application import Application
|
||||||
from ahriman.application.handlers.handler import Handler, SubParserAction
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.application.handlers.update import Update
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.utils import enum_values, extract_user
|
from ahriman.core.utils import enum_values, extract_user
|
||||||
from ahriman.models.package_source import PackageSource
|
from ahriman.models.package_source import PackageSource
|
||||||
from ahriman.models.packagers import Packagers
|
|
||||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
@@ -48,26 +48,7 @@ class Add(Handler):
|
|||||||
"""
|
"""
|
||||||
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
||||||
application.on_start()
|
application.on_start()
|
||||||
|
Add.perform_action(application, args)
|
||||||
application.add(args.package, args.source, args.username)
|
|
||||||
patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else []
|
|
||||||
for package in args.package: # for each requested package insert patch
|
|
||||||
for patch in patches:
|
|
||||||
application.reporter.package_patches_update(package, patch)
|
|
||||||
|
|
||||||
if not args.now:
|
|
||||||
return
|
|
||||||
|
|
||||||
packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False, check_files=False)
|
|
||||||
if args.changes: # generate changes if requested
|
|
||||||
application.changes(packages)
|
|
||||||
|
|
||||||
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
|
|
||||||
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
|
|
||||||
|
|
||||||
application.print_updates(packages, log_fn=application.logger.info)
|
|
||||||
result = application.update(packages, packagers, bump_pkgrel=args.increment)
|
|
||||||
Add.check_status(args.exit_code, not result.is_empty)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
@@ -103,14 +84,34 @@ class Add(Handler):
|
|||||||
parser.add_argument("--increment", help="increment package release (pkgrel) version on duplicate",
|
parser.add_argument("--increment", help="increment package release (pkgrel) version on duplicate",
|
||||||
action=argparse.BooleanOptionalAction, default=True)
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
|
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
|
||||||
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
|
|
||||||
"-yy to force refresh even if up to date",
|
|
||||||
action="count", default=False)
|
|
||||||
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
|
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=enum_values(PackageSource), default=PackageSource.Auto)
|
||||||
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
|
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",
|
parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build",
|
||||||
action="append")
|
action="append")
|
||||||
|
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
|
||||||
|
"-yy to force refresh even if up to date",
|
||||||
|
action="count", default=False)
|
||||||
|
parser.set_defaults(aur=False, check_files=False, dry_run=False, local=False, manual=True, vcs=False)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def perform_action(application: Application, args: argparse.Namespace) -> None:
|
||||||
|
"""
|
||||||
|
perform add action
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
"""
|
||||||
|
application.add(args.package, args.source, args.username)
|
||||||
|
patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else []
|
||||||
|
for package in args.package: # for each requested package insert patch
|
||||||
|
for patch in patches:
|
||||||
|
application.reporter.package_patches_update(package, patch)
|
||||||
|
|
||||||
|
if not args.now:
|
||||||
|
return
|
||||||
|
Update.perform_action(application, args)
|
||||||
|
|
||||||
arguments = [_set_package_add_parser]
|
arguments = [_set_package_add_parser]
|
||||||
|
|||||||
81
src/ahriman/application/handlers/archives.py
Normal file
81
src/ahriman/application/handlers/archives.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#
|
||||||
|
# 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 argparse
|
||||||
|
|
||||||
|
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 PackagePrinter
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Archives(Handler):
|
||||||
|
"""
|
||||||
|
package archives handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=True)
|
||||||
|
|
||||||
|
match args.action:
|
||||||
|
case Action.List:
|
||||||
|
archives = application.repository.package_archives(args.package)
|
||||||
|
for package in archives:
|
||||||
|
PackagePrinter(package, BuildStatus(BuildStatusEnum.Success))(verbose=args.info)
|
||||||
|
|
||||||
|
Archives.check_status(args.exit_code, bool(archives))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_archives_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package archives subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-archives", help="list package archive versions",
|
||||||
|
description="list available archive versions for the package")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--info", help="show additional package information",
|
||||||
|
action=argparse.BooleanOptionalAction, default=False)
|
||||||
|
parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_package_archives_parser]
|
||||||
@@ -47,14 +47,13 @@ class Change(Handler):
|
|||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
report(bool): force enable or disable reporting
|
report(bool): force enable or disable reporting
|
||||||
"""
|
"""
|
||||||
application = Application(repository_id, configuration, report=True)
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
client = application.repository.reporter
|
|
||||||
|
|
||||||
match args.action:
|
match args.action:
|
||||||
case Action.List:
|
case Action.List:
|
||||||
changes = client.package_changes_get(args.package)
|
changes = client.package_changes_get(args.package)
|
||||||
ChangesPrinter(changes)(verbose=True, separator="")
|
ChangesPrinter(changes)(verbose=True, separator="")
|
||||||
Change.check_status(args.exit_code, not changes.is_empty)
|
Change.check_status(args.exit_code, changes.changes is not None)
|
||||||
case Action.Remove:
|
case Action.Remove:
|
||||||
client.package_changes_update(args.package, Changes())
|
client.package_changes_update(args.package, Changes())
|
||||||
|
|
||||||
|
|||||||
93
src/ahriman/application/handlers/hold.py
Normal file
93
src/ahriman/application/handlers/hold.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#
|
||||||
|
# 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 argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Hold(Handler):
|
||||||
|
"""
|
||||||
|
package hold handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
|
|
||||||
|
match args.action:
|
||||||
|
case Action.Remove:
|
||||||
|
for package in args.package:
|
||||||
|
client.package_hold_update(package, enabled=False)
|
||||||
|
case Action.Update:
|
||||||
|
for package in args.package:
|
||||||
|
client.package_hold_update(package, enabled=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_hold_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for hold package subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-hold", help="hold package",
|
||||||
|
description="hold package from automatic updates")
|
||||||
|
parser.add_argument("package", help="package base", nargs="+")
|
||||||
|
parser.set_defaults(action=Action.Update, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_unhold_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for unhold package subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-unhold", help="unhold package",
|
||||||
|
description="remove package hold, allowing automatic updates")
|
||||||
|
parser.add_argument("package", help="package base", nargs="+")
|
||||||
|
parser.set_defaults(action=Action.Remove, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [
|
||||||
|
_set_package_hold_parser,
|
||||||
|
_set_package_unhold_parser,
|
||||||
|
]
|
||||||
100
src/ahriman/application/handlers/pkgbuild.py
Normal file
100
src/ahriman/application/handlers/pkgbuild.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
#
|
||||||
|
# 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 argparse
|
||||||
|
|
||||||
|
from dataclasses import replace
|
||||||
|
|
||||||
|
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 PkgbuildPrinter
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Pkgbuild(Handler):
|
||||||
|
"""
|
||||||
|
package pkgbuild handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
|
|
||||||
|
match args.action:
|
||||||
|
case Action.List:
|
||||||
|
changes = client.package_changes_get(args.package)
|
||||||
|
PkgbuildPrinter(changes)(verbose=True, separator="")
|
||||||
|
Pkgbuild.check_status(args.exit_code, changes.pkgbuild is not None)
|
||||||
|
case Action.Remove:
|
||||||
|
changes = client.package_changes_get(args.package)
|
||||||
|
client.package_changes_update(args.package, replace(changes, pkgbuild=None))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_pkgbuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package pkgbuild subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-pkgbuild", help="get package pkgbuild",
|
||||||
|
description="retrieve package PKGBUILD stored in database",
|
||||||
|
epilog="This command requests package status from the web interface "
|
||||||
|
"if it is available.")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_pkgbuild_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package pkgbuild remove subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-pkgbuild-remove", help="remove package pkgbuild",
|
||||||
|
description="remove the package PKGBUILD stored remotely")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.set_defaults(action=Action.Remove, exit_code=False, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_package_pkgbuild_parser, _set_package_pkgbuild_remove_parser]
|
||||||
@@ -44,8 +44,7 @@ class Reload(Handler):
|
|||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
report(bool): force enable or disable reporting
|
report(bool): force enable or disable reporting
|
||||||
"""
|
"""
|
||||||
application = Application(repository_id, configuration, report=True)
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
client = application.repository.reporter
|
|
||||||
client.configuration_reload()
|
client.configuration_reload()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
131
src/ahriman/application/handlers/rollback.py
Normal file
131
src/ahriman/application/handlers/rollback.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
#
|
||||||
|
# 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 argparse
|
||||||
|
|
||||||
|
from dataclasses import replace
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.add import Add
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.exceptions import UnknownPackageError
|
||||||
|
from ahriman.core.utils import extract_user
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.package_source import PackageSource
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Rollback(Handler):
|
||||||
|
"""
|
||||||
|
package rollback handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=report)
|
||||||
|
application.on_start()
|
||||||
|
|
||||||
|
package = Rollback.package_load(application, args.package, args.version)
|
||||||
|
artifacts = Rollback.package_artifacts(application, package)
|
||||||
|
|
||||||
|
args.package = [str(artifact) for artifact in artifacts]
|
||||||
|
Add.perform_action(application, args)
|
||||||
|
|
||||||
|
if args.hold:
|
||||||
|
application.reporter.package_hold_update(package.base, enabled=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_rollback_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package rollback subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-rollback", help="rollback package",
|
||||||
|
description="rollback package to specified version from archives")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.add_argument("version", help="package version")
|
||||||
|
parser.add_argument("--hold", help="hold package afterwards",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
|
||||||
|
parser.set_defaults(aur=False, changes=False, check_files=False, dependencies=False, dry_run=False,
|
||||||
|
exit_code=True, increment=False, now=True, local=False, manual=False, refresh=False,
|
||||||
|
source=PackageSource.Archive, variable=None, vcs=False)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def package_artifacts(application: Application, package: Package) -> list[Path]:
|
||||||
|
"""
|
||||||
|
look for requested package artifacts and return paths to them
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
package(Package): package descriptor
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Path]: paths to found artifacts
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UnknownPackageError: if artifacts do not exist
|
||||||
|
"""
|
||||||
|
# lookup for built artifacts
|
||||||
|
artifacts = application.repository.package_archives_lookup(package)
|
||||||
|
if not artifacts:
|
||||||
|
raise UnknownPackageError(package.base)
|
||||||
|
return artifacts
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def package_load(application: Application, package_base: str, version: str) -> Package:
|
||||||
|
"""
|
||||||
|
load package from repository, while setting requested version
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
package_base(str): package base
|
||||||
|
version(str): package version
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Package: loaded package
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UnknownPackageError: if package does not exist
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
package, _ = next(iter(application.reporter.package_get(package_base)))
|
||||||
|
return replace(package, version=version)
|
||||||
|
except StopIteration:
|
||||||
|
raise UnknownPackageError(package_base) from None
|
||||||
|
|
||||||
|
arguments = [_set_package_rollback_parser]
|
||||||
@@ -52,7 +52,7 @@ class Status(Handler):
|
|||||||
report(bool): force enable or disable reporting
|
report(bool): force enable or disable reporting
|
||||||
"""
|
"""
|
||||||
# we are using reporter here
|
# we are using reporter here
|
||||||
client = Application(repository_id, configuration, report=True).repository.reporter
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
if args.ahriman:
|
if args.ahriman:
|
||||||
service_status = client.status_get()
|
service_status = client.status_get()
|
||||||
StatusPrinter(service_status.status)(verbose=args.info)
|
StatusPrinter(service_status.status)(verbose=args.info)
|
||||||
|
|||||||
@@ -47,8 +47,7 @@ class StatusUpdate(Handler):
|
|||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
report(bool): force enable or disable reporting
|
report(bool): force enable or disable reporting
|
||||||
"""
|
"""
|
||||||
application = Application(repository_id, configuration, report=True)
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
client = application.repository.reporter
|
|
||||||
|
|
||||||
match args.action:
|
match args.action:
|
||||||
case Action.Update if args.package:
|
case Action.Update if args.package:
|
||||||
|
|||||||
@@ -48,22 +48,7 @@ class Update(Handler):
|
|||||||
"""
|
"""
|
||||||
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
||||||
application.on_start()
|
application.on_start()
|
||||||
|
Update.perform_action(application, args)
|
||||||
packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs,
|
|
||||||
check_files=args.check_files)
|
|
||||||
if args.changes: # generate changes if requested
|
|
||||||
application.changes(packages)
|
|
||||||
|
|
||||||
if args.dry_run: # exit from application if no build requested
|
|
||||||
Update.check_status(args.exit_code, packages) # status code check
|
|
||||||
return
|
|
||||||
|
|
||||||
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
|
|
||||||
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
|
|
||||||
|
|
||||||
application.print_updates(packages, log_fn=application.logger.info)
|
|
||||||
result = application.update(packages, packagers, bump_pkgrel=args.increment)
|
|
||||||
Update.check_status(args.exit_code, not result.is_empty)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
@@ -153,6 +138,31 @@ class Update(Handler):
|
|||||||
return print(line) if dry_run else application.logger.info(line) # pylint: disable=bad-builtin
|
return print(line) if dry_run else application.logger.info(line) # pylint: disable=bad-builtin
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def perform_action(application: Application, args: argparse.Namespace) -> None:
|
||||||
|
"""
|
||||||
|
perform update action
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
"""
|
||||||
|
packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs,
|
||||||
|
check_files=args.check_files)
|
||||||
|
if args.changes: # generate changes if requested
|
||||||
|
application.changes(packages)
|
||||||
|
|
||||||
|
if args.dry_run: # exit from application if no build requested
|
||||||
|
Update.check_status(args.exit_code, packages) # status code check
|
||||||
|
return
|
||||||
|
|
||||||
|
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
|
||||||
|
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
|
||||||
|
|
||||||
|
application.print_updates(packages, log_fn=application.logger.info)
|
||||||
|
result = application.update(packages, packagers, bump_pkgrel=args.increment)
|
||||||
|
Update.check_status(args.exit_code, not result.is_empty)
|
||||||
|
|
||||||
arguments = [
|
arguments = [
|
||||||
_set_repo_check_parser,
|
_set_repo_check_parser,
|
||||||
_set_repo_update_parser,
|
_set_repo_update_parser,
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ import tarfile
|
|||||||
from collections.abc import Iterable, Iterator
|
from collections.abc import Iterable, Iterator
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pyalpm import DB, Handle, Package, SIG_DATABASE_OPTIONAL, SIG_PACKAGE_OPTIONAL # type: ignore[import-not-found]
|
from pyalpm import DB, Package, SIG_DATABASE_OPTIONAL, SIG_PACKAGE_OPTIONAL # type: ignore[import-not-found]
|
||||||
from string import Template
|
from string import Template
|
||||||
|
|
||||||
from ahriman.core.alpm.pacman_database import PacmanDatabase
|
from ahriman.core.alpm.pacman_database import PacmanDatabase
|
||||||
|
from ahriman.core.alpm.pacman_handle import PacmanHandle
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.log import LazyLogging
|
from ahriman.core.log import LazyLogging
|
||||||
from ahriman.core.utils import trim_package
|
from ahriman.core.utils import trim_package
|
||||||
@@ -61,16 +62,16 @@ class Pacman(LazyLogging):
|
|||||||
self.refresh_database = refresh_database
|
self.refresh_database = refresh_database
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def handle(self) -> Handle:
|
def handle(self) -> PacmanHandle:
|
||||||
"""
|
"""
|
||||||
pyalpm handle
|
pyalpm handle
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Handle: generated pyalpm handle instance
|
PacmanHandle: generated pyalpm handle instance
|
||||||
"""
|
"""
|
||||||
return self.__create_handle(refresh_database=self.refresh_database)
|
return self.__create_handle(refresh_database=self.refresh_database)
|
||||||
|
|
||||||
def __create_handle(self, *, refresh_database: PacmanSynchronization) -> Handle:
|
def __create_handle(self, *, refresh_database: PacmanSynchronization) -> PacmanHandle:
|
||||||
"""
|
"""
|
||||||
create lazy handle function
|
create lazy handle function
|
||||||
|
|
||||||
@@ -78,14 +79,14 @@ class Pacman(LazyLogging):
|
|||||||
refresh_database(PacmanSynchronization): synchronize local cache to remote
|
refresh_database(PacmanSynchronization): synchronize local cache to remote
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Handle: fully initialized pacman handle
|
PacmanHandle: fully initialized pacman handle
|
||||||
"""
|
"""
|
||||||
pacman_root = self.configuration.getpath("alpm", "database")
|
pacman_root = self.configuration.getpath("alpm", "database")
|
||||||
use_ahriman_cache = self.configuration.getboolean("alpm", "use_ahriman_cache")
|
use_ahriman_cache = self.configuration.getboolean("alpm", "use_ahriman_cache")
|
||||||
|
|
||||||
database_path = self.repository_paths.pacman if use_ahriman_cache else pacman_root
|
database_path = self.repository_paths.pacman if use_ahriman_cache else pacman_root
|
||||||
root = self.configuration.getpath("alpm", "root")
|
root = self.configuration.getpath("alpm", "root")
|
||||||
handle = Handle(str(root), str(database_path))
|
handle = PacmanHandle(str(root), str(database_path))
|
||||||
|
|
||||||
for repository in self.configuration.getlist("alpm", "repositories"):
|
for repository in self.configuration.getlist("alpm", "repositories"):
|
||||||
database = self.database_init(handle, repository, self.repository_id.architecture)
|
database = self.database_init(handle, repository, self.repository_id.architecture)
|
||||||
@@ -99,12 +100,12 @@ class Pacman(LazyLogging):
|
|||||||
|
|
||||||
return handle
|
return handle
|
||||||
|
|
||||||
def database_copy(self, handle: Handle, database: DB, pacman_root: Path, *, use_ahriman_cache: bool) -> None:
|
def database_copy(self, handle: PacmanHandle, database: DB, pacman_root: Path, *, use_ahriman_cache: bool) -> None:
|
||||||
"""
|
"""
|
||||||
copy database from the operating system root to the ahriman local home
|
copy database from the operating system root to the ahriman local home
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
handle(Handle): pacman handle which will be used for database copying
|
handle(PacmanHandle): pacman handle which will be used for database copying
|
||||||
database(DB): pacman database instance to be copied
|
database(DB): pacman database instance to be copied
|
||||||
pacman_root(Path): operating system pacman root
|
pacman_root(Path): operating system pacman root
|
||||||
use_ahriman_cache(bool): use local ahriman cache instead of system one
|
use_ahriman_cache(bool): use local ahriman cache instead of system one
|
||||||
@@ -133,12 +134,12 @@ class Pacman(LazyLogging):
|
|||||||
with self.repository_paths.preserve_owner():
|
with self.repository_paths.preserve_owner():
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
|
|
||||||
def database_init(self, handle: Handle, repository: str, architecture: str) -> DB:
|
def database_init(self, handle: PacmanHandle, repository: str, architecture: str) -> DB:
|
||||||
"""
|
"""
|
||||||
create database instance from pacman handler and set its properties
|
create database instance from pacman handler and set its properties
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
handle(Handle): pacman handle which will be used for database initializing
|
handle(PacmanHandle): pacman handle which will be used for database initializing
|
||||||
repository(str): pacman repository name (e.g. core)
|
repository(str): pacman repository name (e.g. core)
|
||||||
architecture(str): repository architecture
|
architecture(str): repository architecture
|
||||||
|
|
||||||
@@ -164,12 +165,12 @@ class Pacman(LazyLogging):
|
|||||||
|
|
||||||
return database
|
return database
|
||||||
|
|
||||||
def database_sync(self, handle: Handle, *, force: bool) -> None:
|
def database_sync(self, handle: PacmanHandle, *, force: bool) -> None:
|
||||||
"""
|
"""
|
||||||
sync local database
|
sync local database
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
handle(Handle): pacman handle which will be used for database sync
|
handle(PacmanHandle): pacman handle which will be used for database sync
|
||||||
force(bool): force database synchronization (same as ``pacman -Syy``)
|
force(bool): force database synchronization (same as ``pacman -Syy``)
|
||||||
"""
|
"""
|
||||||
self.logger.info("refresh ahriman's home pacman database (force refresh %s)", force)
|
self.logger.info("refresh ahriman's home pacman database (force refresh %s)", force)
|
||||||
|
|||||||
81
src/ahriman/core/alpm/pacman_handle.py
Normal file
81
src/ahriman/core/alpm/pacman_handle.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
#
|
||||||
|
from pathlib import Path
|
||||||
|
from pyalpm import Handle, Package # type: ignore[import-not-found]
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
from typing import Any, ClassVar, Self
|
||||||
|
|
||||||
|
|
||||||
|
class PacmanHandle:
|
||||||
|
"""
|
||||||
|
lightweight wrapper for pacman handle to be used for direct alpm operations (e.g. package load)
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
handle(Handle): pyalpm handle instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ephemeral: ClassVar[Self | None] = None
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
*args(Any): positional arguments for :class:`pyalpm.Handle`
|
||||||
|
**kwargs(Any): keyword arguments for :class:`pyalpm.Handle`
|
||||||
|
"""
|
||||||
|
self.handle = Handle(*args, **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def ephemeral(cls) -> Self:
|
||||||
|
"""
|
||||||
|
create temporary instance with no access to real databases
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Self: loaded class
|
||||||
|
"""
|
||||||
|
if cls._ephemeral is None:
|
||||||
|
# handle creates alpm version file, but we don't use it
|
||||||
|
# so it is ok to just remove it
|
||||||
|
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
|
||||||
|
cls._ephemeral = cls("/", dir_name)
|
||||||
|
return cls._ephemeral
|
||||||
|
|
||||||
|
def package_load(self, path: Path) -> Package:
|
||||||
|
"""
|
||||||
|
load package from path to the archive
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path(Path): path to package archive
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Package: package instance
|
||||||
|
"""
|
||||||
|
return self.handle.load_pkg(str(path))
|
||||||
|
|
||||||
|
def __getattr__(self, item: str) -> Any:
|
||||||
|
"""
|
||||||
|
proxy methods for :class:`pyalpm.Handle`, because it doesn't allow subclassing
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item(str): property name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: attribute by its name
|
||||||
|
"""
|
||||||
|
return self.handle.__getattribute__(item)
|
||||||
@@ -104,7 +104,7 @@ class PkgbuildParser(shlex.shlex):
|
|||||||
|
|
||||||
# ignore substitution and extend bash symbols
|
# ignore substitution and extend bash symbols
|
||||||
self.wordchars += "${}#:+-@!"
|
self.wordchars += "${}#:+-@!"
|
||||||
# in case of default behaviour, it will ignore, for example, segment part of url outside of quotes
|
# in case of default behavior, it will ignore, for example, segment part of url outside of quotes
|
||||||
self.commenters = ""
|
self.commenters = ""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user