mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-03-04 17:29:48 +00:00
Compare commits
4 Commits
2.20.0rc4
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
| b8d9bef965 | |||
| db46147f0d | |||
| 49ebbc34fa | |||
| e376f1307f |
@@ -12,3 +12,6 @@ __pycache__/
|
|||||||
*.pyc
|
*.pyc
|
||||||
*.pyd
|
*.pyd
|
||||||
*.pyo
|
*.pyo
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
|||||||
2
.github/workflows/setup.sh
vendored
2
.github/workflows/setup.sh
vendored
@@ -10,7 +10,7 @@ echo -e '[arcanisrepo]\nServer = https://repo.arcanis.me/$arch\nSigLevel = Never
|
|||||||
# refresh the image
|
# refresh the image
|
||||||
pacman -Syyu --noconfirm
|
pacman -Syyu --noconfirm
|
||||||
# main dependencies
|
# main dependencies
|
||||||
pacman -S --noconfirm devtools git pyalpm python-bcrypt python-filelock python-inflection python-pyelftools python-requests python-systemd sudo
|
pacman -S --noconfirm devtools git npm pyalpm python-bcrypt python-filelock python-inflection python-pyelftools python-requests python-systemd sudo
|
||||||
# make dependencies
|
# make dependencies
|
||||||
pacman -S --noconfirm --asdeps base-devel python-build python-flit python-installer python-tox python-wheel
|
pacman -S --noconfirm --asdeps base-devel python-build python-flit python-installer python-tox python-wheel
|
||||||
# optional dependencies
|
# optional dependencies
|
||||||
|
|||||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- ${{ github.workspace }}:/build
|
- ${{ github.workspace }}:/build
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- run: pacman --noconfirm -Syu base-devel git python-tox
|
- run: pacman --noconfirm -Syu base-devel git npm python-tox
|
||||||
|
|
||||||
- run: git config --global --add safe.directory *
|
- run: git config --global --add safe.directory *
|
||||||
|
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -99,3 +99,9 @@ status_cache.json
|
|||||||
*.db
|
*.db
|
||||||
|
|
||||||
docs/html/
|
docs/html/
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
package/share/ahriman/templates/static/index.js
|
||||||
|
package/share/ahriman/templates/static/index.css
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ Again, the most checks can be performed by `tox` command, though some additional
|
|||||||
def __hash__(self) -> int: ... # basically any magic (or look-alike) method
|
def __hash__(self) -> int: ... # basically any magic (or look-alike) method
|
||||||
```
|
```
|
||||||
|
|
||||||
Methods inside one group should be ordered alphabetically, the only exceptions are `__init__` (`__post_init__` for dataclasses), `__new__` and `__del__` methods which should be defined first. For test methods it is recommended to follow the order in which functions are defined.
|
Methods inside one group should be ordered alphabetically, the only exceptions are `__init__` (`__post_init__` for dataclasses), `__new__` and `__del__` methods which should be defined first. For test methods it is recommended to follow the order in which functions are defined. Same idea applies to frontend classes.
|
||||||
|
|
||||||
Though, we would like to highlight abstract methods (i.e. ones which raise `NotImplementedError`), we still keep in global order at the moment.
|
Though, we would like to highlight abstract methods (i.e. ones which raise `NotImplementedError`), we still keep in global order at the moment.
|
||||||
|
|
||||||
@@ -172,8 +172,9 @@ Again, the most checks can be performed by `tox` command, though some additional
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
* One file should define only one class, exception is class satellites in case if file length remains less than 400 lines.
|
* Imports goes in alphabetical order, no relative imports allowed. Same rule applies to frontend classes.
|
||||||
* It is possible to create file which contains some functions (e.g. `ahriman.core.util`), but in this case you would need to define `__all__` attribute.
|
* One file should define only one class, exception is class satellites in case if file length remains less than 400 lines. Same rule applies to frontend classes.
|
||||||
|
* It is possible to create file which contains some functions (e.g. `ahriman.core.utils`), but in this case you would need to define `__all__` attribute.
|
||||||
* The file size mentioned above must be applicable in general. In case of big classes consider splitting them into traits. Note, however, that `pylint` includes comments and docstrings into counter, thus you need to check file size by other tools.
|
* The file size mentioned above must be applicable in general. In case of big classes consider splitting them into traits. Note, however, that `pylint` includes comments and docstrings into counter, thus you need to check file size by other tools.
|
||||||
* No global variable is allowed outside of `ahriman` module. `ahriman.core.context` is also special case.
|
* No global variable is allowed outside of `ahriman` module. `ahriman.core.context` is also special case.
|
||||||
* Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent.
|
* Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent.
|
||||||
@@ -226,6 +227,8 @@ Again, the most checks can be performed by `tox` command, though some additional
|
|||||||
|
|
||||||
The projects also uses typing checks (provided by `mypy`) and some linter checks provided by `pylint` and `bandit`. Those checks must be passed successfully for any open pull requests.
|
The projects also uses typing checks (provided by `mypy`) and some linter checks provided by `pylint` and `bandit`. Those checks must be passed successfully for any open pull requests.
|
||||||
|
|
||||||
|
Frontend checks normally are performed by `eslint` (e.g. `npx run eslint`).
|
||||||
|
|
||||||
## Developers how to
|
## Developers how to
|
||||||
|
|
||||||
### Run automated checks
|
### Run automated checks
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
|
|||||||
RUN pacman -S --noconfirm --asdeps \
|
RUN pacman -S --noconfirm --asdeps \
|
||||||
devtools \
|
devtools \
|
||||||
git \
|
git \
|
||||||
|
npm \
|
||||||
pyalpm \
|
pyalpm \
|
||||||
python-bcrypt \
|
python-bcrypt \
|
||||||
python-filelock \
|
python-filelock \
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ ahriman.web.routes module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.server\_info module
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.server_info
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.web module
|
ahriman.web.web module
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ ahriman.web.schemas.aur\_package\_schema module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.schemas.auth\_info\_schema module
|
||||||
|
---------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.schemas.auth_info_schema
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.schemas.auth\_schema module
|
ahriman.web.schemas.auth\_schema module
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
|
||||||
@@ -28,6 +36,14 @@ ahriman.web.schemas.auth\_schema module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.schemas.auto\_refresh\_interval\_schema module
|
||||||
|
----------------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.schemas.auto_refresh_interval_schema
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.schemas.build\_options\_schema module
|
ahriman.web.schemas.build\_options\_schema module
|
||||||
-------------------------------------------------
|
-------------------------------------------------
|
||||||
|
|
||||||
@@ -108,6 +124,14 @@ ahriman.web.schemas.info\_schema module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.schemas.info\_v2\_schema module
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.schemas.info_v2_schema
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.schemas.internal\_status\_schema module
|
ahriman.web.schemas.internal\_status\_schema module
|
||||||
---------------------------------------------------
|
---------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Subpackages
|
|||||||
:maxdepth: 4
|
:maxdepth: 4
|
||||||
|
|
||||||
ahriman.web.views.v2.packages
|
ahriman.web.views.v2.packages
|
||||||
|
ahriman.web.views.v2.status
|
||||||
|
|
||||||
Module contents
|
Module contents
|
||||||
---------------
|
---------------
|
||||||
|
|||||||
21
docs/ahriman.web.views.v2.status.rst
Normal file
21
docs/ahriman.web.views.v2.status.rst
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
ahriman.web.views.v2.status package
|
||||||
|
===================================
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
ahriman.web.views.v2.status.info module
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.views.v2.status.info
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.views.v2.status
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
@@ -118,7 +118,6 @@ Base authorization settings. ``OAuth`` provider requires ``aioauth-client`` libr
|
|||||||
* ``cookie_secret_key`` - secret key which will be used for cookies encryption, string, optional. It must be 32 bytes URL-safe base64-encoded and can be generated as following ``base64.urlsafe_b64encode(os.urandom(32)).decode("utf8")``. If not set, it will be generated automatically; note, however, that in this case, all sessions will be automatically invalidated during the service restart.
|
* ``cookie_secret_key`` - secret key which will be used for cookies encryption, string, optional. It must be 32 bytes URL-safe base64-encoded and can be generated as following ``base64.urlsafe_b64encode(os.urandom(32)).decode("utf8")``. If not set, it will be generated automatically; note, however, that in this case, all sessions will be automatically invalidated during the service restart.
|
||||||
* ``full_access_group`` - name of the secondary group (e.g. ``wheel``) to be used as admin group in the service, string, required in case if ``pam`` is used.
|
* ``full_access_group`` - name of the secondary group (e.g. ``wheel``) to be used as admin group in the service, string, required in case if ``pam`` is used.
|
||||||
* ``max_age`` - parameter which controls both cookie expiration and token expiration inside the service in seconds, integer, optional, default is 7 days.
|
* ``max_age`` - parameter which controls both cookie expiration and token expiration inside the service in seconds, integer, optional, default is 7 days.
|
||||||
* ``oauth_icon`` - OAuth2 login button icon, string, optional, default is ``google``. Must be valid `Bootstrap icon <https://icons.getbootstrap.com/>`__ name.
|
|
||||||
* ``oauth_provider`` - OAuth2 provider class name as is in ``aioauth-client`` (e.g. ``GoogleClient``, ``GithubClient`` etc), string, required in case if ``oauth`` is used.
|
* ``oauth_provider`` - OAuth2 provider class name as is in ``aioauth-client`` (e.g. ``GoogleClient``, ``GithubClient`` etc), string, required in case if ``oauth`` is used.
|
||||||
* ``oauth_scopes`` - scopes list for OAuth2 provider, which will allow retrieving user email (which is used for checking user permissions), e.g. ``https://www.googleapis.com/auth/userinfo.email`` for ``GoogleClient`` or ``user:email`` for ``GithubClient``, space separated list of strings, required in case if ``oauth`` is used.
|
* ``oauth_scopes`` - scopes list for OAuth2 provider, which will allow retrieving user email (which is used for checking user permissions), e.g. ``https://www.googleapis.com/auth/userinfo.email`` for ``GoogleClient`` or ``user:email`` for ``GithubClient``, space separated list of strings, required in case if ``oauth`` is used.
|
||||||
* ``permit_root_login`` - allow login as root user, boolean, optional, default ``no``.
|
* ``permit_root_login`` - allow login as root user, boolean, optional, default ``no``.
|
||||||
@@ -188,6 +187,7 @@ Web server settings. This feature requires ``aiohttp`` libraries to be installed
|
|||||||
* ``port`` - port to bind, integer, optional.
|
* ``port`` - port to bind, integer, optional.
|
||||||
* ``service_only`` - disable status routes (including logs), boolean, optional, default ``no``.
|
* ``service_only`` - disable status routes (including logs), boolean, optional, default ``no``.
|
||||||
* ``static_path`` - path to directory with static files, string, required.
|
* ``static_path`` - path to directory with static files, string, required.
|
||||||
|
* ``template`` - Jinja2 template name for the index page, string, optional, default ``build-status.jinja2``.
|
||||||
* ``templates`` - path to templates directories, space separated list of paths, required.
|
* ``templates`` - path to templates directories, space separated list of paths, required.
|
||||||
* ``unix_socket`` - path to the listening unix socket, string, optional. If set, server will create the socket on the specified address which can (and will) be used by application. Note, that unlike usual host/port configuration, unix socket allows to perform requests without authorization.
|
* ``unix_socket`` - path to the listening unix socket, string, optional. If set, server will create the socket on the specified address which can (and will) be used by application. Note, that unlike usual host/port configuration, unix socket allows to perform requests without authorization.
|
||||||
* ``unix_socket_unsafe`` - set unsafe (o+w) permissions to unix socket, boolean, optional, default ``yes``. This option is enabled by default, because it is supposed that unix socket is created in safe environment (only web service is supposed to be used in unsafe), but it can be disabled by configuration.
|
* ``unix_socket_unsafe`` - set unsafe (o+w) permissions to unix socket, boolean, optional, default ``yes``. This option is enabled by default, because it is supposed that unix socket is created in safe environment (only web service is supposed to be used in unsafe), but it can be disabled by configuration.
|
||||||
|
|||||||
67
frontend/eslint.config.js
Normal file
67
frontend/eslint.config.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import stylistic from "@stylistic/eslint-plugin";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
import simpleImportSort from "eslint-plugin-simple-import-sort";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ["dist"] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommendedTypeChecked],
|
||||||
|
files: ["src/**/*.{ts,tsx}"],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
"react-refresh": reactRefresh,
|
||||||
|
"simple-import-sort": simpleImportSort,
|
||||||
|
"@stylistic": stylistic,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||||
|
|
||||||
|
// imports
|
||||||
|
"simple-import-sort/imports": "error",
|
||||||
|
"simple-import-sort/exports": "error",
|
||||||
|
|
||||||
|
// brackets
|
||||||
|
"curly": "error",
|
||||||
|
"@stylistic/brace-style": ["error", "1tbs"],
|
||||||
|
|
||||||
|
// stylistic
|
||||||
|
"@stylistic/array-bracket-spacing": ["error", "never"],
|
||||||
|
"@stylistic/arrow-parens": ["error", "as-needed"],
|
||||||
|
"@stylistic/comma-dangle": ["error", "always-multiline"],
|
||||||
|
"@stylistic/comma-spacing": ["error", { before: false, after: true }],
|
||||||
|
"@stylistic/eol-last": ["error", "always"],
|
||||||
|
"@stylistic/indent": ["error", 4],
|
||||||
|
"@stylistic/jsx-quotes": ["error", "prefer-double"],
|
||||||
|
"@stylistic/max-len": ["error", {
|
||||||
|
code: 120,
|
||||||
|
ignoreComments: true,
|
||||||
|
ignoreStrings: true,
|
||||||
|
ignoreTemplateLiterals: true,
|
||||||
|
ignoreUrls: true,
|
||||||
|
}],
|
||||||
|
"@stylistic/no-extra-parens": ["error", "all"],
|
||||||
|
"@stylistic/no-multi-spaces": "error",
|
||||||
|
"@stylistic/no-multiple-empty-lines": ["error", { max: 1 }],
|
||||||
|
"@stylistic/no-trailing-spaces": "error",
|
||||||
|
"@stylistic/object-curly-spacing": ["error", "always"],
|
||||||
|
"@stylistic/quotes": ["error", "double"],
|
||||||
|
"@stylistic/semi": ["error", "always"],
|
||||||
|
|
||||||
|
// typescript
|
||||||
|
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
|
||||||
|
"@typescript-eslint/explicit-function-return-type": ["error", { allowExpressions: true }],
|
||||||
|
"@typescript-eslint/no-deprecated": "error",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
16
frontend/index.html
Normal file
16
frontend/index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>ahriman</title>
|
||||||
|
|
||||||
|
<link rel="icon" href="/static/favicon.ico" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
42
frontend/package.json
Normal file
42
frontend/package.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "ahriman-frontend",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"version": "2.20.0-rc4",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"dev": "vite",
|
||||||
|
"lint": "eslint src/",
|
||||||
|
"lint:fix": "eslint --fix src/",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.11.0",
|
||||||
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@mui/icons-material": "^7.3.8",
|
||||||
|
"@mui/material": "^7.3.8",
|
||||||
|
"@mui/x-data-grid": "^8.27.3",
|
||||||
|
"@tanstack/react-query": "^5.0.0",
|
||||||
|
"chart.js": "^4.5.0",
|
||||||
|
"react": "^19.2.4",
|
||||||
|
"react-chartjs-2": "^5.2.0",
|
||||||
|
"react-dom": "^19.2.4",
|
||||||
|
"react-syntax-highlighter": "^16.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.3",
|
||||||
|
"@stylistic/eslint-plugin": "^5.9.0",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
"eslint": "^9.39.3",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
|
"typescript": "^5.3.0",
|
||||||
|
"typescript-eslint": "^8.56.1",
|
||||||
|
"vite": "^7.3.1",
|
||||||
|
"vite-tsconfig-paths": "^6.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
57
frontend/src/App.tsx
Normal file
57
frontend/src/App.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* 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 } from "@mui/material/styles";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import AppLayout from "components/layout/AppLayout";
|
||||||
|
import { AuthProvider } from "contexts/AuthProvider";
|
||||||
|
import { ClientProvider } from "contexts/ClientProvider";
|
||||||
|
import { NotificationProvider } from "contexts/NotificationProvider";
|
||||||
|
import { RepositoryProvider } from "contexts/RepositoryProvider";
|
||||||
|
import type React from "react";
|
||||||
|
import Theme from "theme/Theme";
|
||||||
|
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 30_000,
|
||||||
|
retry: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function App(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<ThemeProvider theme={Theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<ClientProvider>
|
||||||
|
<AuthProvider>
|
||||||
|
<RepositoryProvider>
|
||||||
|
<NotificationProvider>
|
||||||
|
<AppLayout />
|
||||||
|
</NotificationProvider>
|
||||||
|
</RepositoryProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
</ClientProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
frontend/src/api/client/AhrimanClient.ts
Normal file
39
frontend/src/api/client/AhrimanClient.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Client } from "api/client/Client";
|
||||||
|
import { FetchClient } from "api/client/FetchClient";
|
||||||
|
import { ServiceClient } from "api/client/ServiceClient";
|
||||||
|
import type { LoginRequest } from "models/LoginRequest";
|
||||||
|
|
||||||
|
export class AhrimanClient {
|
||||||
|
|
||||||
|
protected client = new Client();
|
||||||
|
|
||||||
|
readonly fetch = new FetchClient(this.client);
|
||||||
|
readonly service = new ServiceClient(this.client);
|
||||||
|
|
||||||
|
async login(data: LoginRequest): Promise<void> {
|
||||||
|
return this.client.request("/api/v1/login", { method: "POST", json: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout(): Promise<void> {
|
||||||
|
return this.client.request("/api/v1/logout", { method: "POST" });
|
||||||
|
}
|
||||||
|
}
|
||||||
44
frontend/src/api/client/ApiError.ts
Normal file
44
frontend/src/api/client/ApiError.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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 class ApiError extends Error {
|
||||||
|
status: number;
|
||||||
|
statusText: string;
|
||||||
|
body: string;
|
||||||
|
|
||||||
|
constructor(status: number, statusText: string, body: string) {
|
||||||
|
super(`${status} ${statusText}`);
|
||||||
|
this.status = status;
|
||||||
|
this.statusText = statusText;
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
get detail(): string {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(this.body) as Record<string, string>;
|
||||||
|
return parsed.error ?? (this.body || this.message);
|
||||||
|
} catch {
|
||||||
|
return this.body || this.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static errorDetail(exception: unknown): string {
|
||||||
|
return exception instanceof ApiError ? exception.detail : String(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
68
frontend/src/api/client/Client.ts
Normal file
68
frontend/src/api/client/Client.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* 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 { ApiError } from "api/client/ApiError";
|
||||||
|
import type { RequestOptions } from "api/client/RequestOptions";
|
||||||
|
|
||||||
|
export class Client {
|
||||||
|
|
||||||
|
async request<T>(url: string, options: RequestOptions = {}): Promise<T> {
|
||||||
|
const { method, query, json } = options;
|
||||||
|
|
||||||
|
let fullUrl = url;
|
||||||
|
if (query) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
for (const [key, value] of Object.entries(query)) {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
params.set(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fullUrl += `?${params.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
Accept: "application/json",
|
||||||
|
};
|
||||||
|
if (json !== undefined) {
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestInit: RequestInit = {
|
||||||
|
method: method || (json ? "POST" : "GET"),
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (json !== undefined) {
|
||||||
|
requestInit.body = JSON.stringify(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(fullUrl, requestInit);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const body = await response.text();
|
||||||
|
throw new ApiError(response.status, response.statusText, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = response.headers.get("Content-Type") ?? "";
|
||||||
|
if (contentType.includes("application/json")) {
|
||||||
|
return await response.json() as T;
|
||||||
|
}
|
||||||
|
return await response.text() as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
109
frontend/src/api/client/FetchClient.ts
Normal file
109
frontend/src/api/client/FetchClient.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* 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 type { Client } from "api/client/Client";
|
||||||
|
import type { Changes } from "models/Changes";
|
||||||
|
import type { Dependencies } from "models/Dependencies";
|
||||||
|
import type { Event } from "models/Event";
|
||||||
|
import type { InfoResponse } from "models/InfoResponse";
|
||||||
|
import type { InternalStatus } from "models/InternalStatus";
|
||||||
|
import type { LogRecord } from "models/LogRecord";
|
||||||
|
import type { PackageStatus } from "models/PackageStatus";
|
||||||
|
import type { Patch } from "models/Patch";
|
||||||
|
import { RepositoryId } from "models/RepositoryId";
|
||||||
|
|
||||||
|
export class FetchClient {
|
||||||
|
|
||||||
|
protected client: Client;
|
||||||
|
|
||||||
|
constructor(client: Client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchPackage(packageBase: string, repository: RepositoryId): Promise<PackageStatus[]> {
|
||||||
|
return this.client.request<PackageStatus[]>(`/api/v1/packages/${encodeURIComponent(packageBase)}`, {
|
||||||
|
query: repository.toQuery(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchPackageChanges(packageBase: string, repository: RepositoryId): Promise<Changes> {
|
||||||
|
return this.client.request<Changes>(`/api/v1/packages/${encodeURIComponent(packageBase)}/changes`, {
|
||||||
|
query: repository.toQuery(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchPackageDependencies(packageBase: string, repository: RepositoryId): Promise<Dependencies> {
|
||||||
|
return this.client.request<Dependencies>(`/api/v1/packages/${encodeURIComponent(packageBase)}/dependencies`, {
|
||||||
|
query: repository.toQuery(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchPackageEvents(repository: RepositoryId, objectId?: string, limit?: number): Promise<Event[]> {
|
||||||
|
const query: Record<string, string | number> = repository.toQuery();
|
||||||
|
if (objectId) {
|
||||||
|
query.object_id = objectId;
|
||||||
|
}
|
||||||
|
if (limit) {
|
||||||
|
query.limit = limit;
|
||||||
|
}
|
||||||
|
return this.client.request<Event[]>("/api/v1/events", { query });
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchPackageLogs(
|
||||||
|
packageBase: string,
|
||||||
|
repository: RepositoryId,
|
||||||
|
version?: string,
|
||||||
|
processId?: string,
|
||||||
|
head?: boolean,
|
||||||
|
): Promise<LogRecord[]> {
|
||||||
|
const query: Record<string, string | boolean> = { ...repository.toQuery() };
|
||||||
|
if (version) {
|
||||||
|
query.version = version;
|
||||||
|
}
|
||||||
|
if (processId) {
|
||||||
|
query.process_id = processId;
|
||||||
|
}
|
||||||
|
if (head) {
|
||||||
|
query.head = true;
|
||||||
|
}
|
||||||
|
return this.client.request<LogRecord[]>(`/api/v2/packages/${encodeURIComponent(packageBase)}/logs`, { query });
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchPackagePatches(packageBase: string): Promise<Patch[]> {
|
||||||
|
return this.client.request<Patch[]>(`/api/v1/packages/${encodeURIComponent(packageBase)}/patches`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchPackages(repository: RepositoryId): Promise<PackageStatus[]> {
|
||||||
|
return this.client.request<PackageStatus[]>("/api/v1/packages", { query: repository.toQuery() });
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchServerInfo(): Promise<InfoResponse> {
|
||||||
|
const info = await this.client.request<InfoResponse>("/api/v2/info");
|
||||||
|
return {
|
||||||
|
...info,
|
||||||
|
repositories: info.repositories.map(repo =>
|
||||||
|
new RepositoryId(repo.architecture, repo.repository),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchServerStatus(repository: RepositoryId): Promise<InternalStatus> {
|
||||||
|
return this.client.request<InternalStatus>("/api/v1/status", { query: repository.toQuery() });
|
||||||
|
}
|
||||||
|
}
|
||||||
24
frontend/src/api/client/RequestOptions.ts
Normal file
24
frontend/src/api/client/RequestOptions.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* 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 RequestOptions {
|
||||||
|
method?: string;
|
||||||
|
query?: Record<string, string | number | boolean>;
|
||||||
|
json?: unknown;
|
||||||
|
}
|
||||||
88
frontend/src/api/client/ServiceClient.ts
Normal file
88
frontend/src/api/client/ServiceClient.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* 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 type { Client } from "api/client/Client";
|
||||||
|
import type { AURPackage } from "models/AURPackage";
|
||||||
|
import type { PackageActionRequest } from "models/PackageActionRequest";
|
||||||
|
import type { PGPKey } from "models/PGPKey";
|
||||||
|
import type { PGPKeyRequest } from "models/PGPKeyRequest";
|
||||||
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
|
||||||
|
export class ServiceClient {
|
||||||
|
|
||||||
|
protected client: Client;
|
||||||
|
|
||||||
|
constructor(client: Client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async servicePackageAdd(repository: RepositoryId, data: PackageActionRequest): Promise<void> {
|
||||||
|
return this.client.request("/api/v1/service/add", { method: "POST", query: repository.toQuery(), json: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async servicePackagePatchRemove(packageBase: string, key: string): Promise<void> {
|
||||||
|
return this.client.request(`/api/v1/packages/${encodeURIComponent(packageBase)}/patches/${encodeURIComponent(key)}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async servicePackageRemove(repository: RepositoryId, packages: string[]): Promise<void> {
|
||||||
|
return this.client.request("/api/v1/service/remove", {
|
||||||
|
method: "POST",
|
||||||
|
query: repository.toQuery(),
|
||||||
|
json: { packages },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async servicePackageRequest(repository: RepositoryId, data: PackageActionRequest): Promise<void> {
|
||||||
|
return this.client.request("/api/v1/service/request", {
|
||||||
|
method: "POST",
|
||||||
|
query: repository.toQuery(),
|
||||||
|
json: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async servicePackageSearch(query: string): Promise<AURPackage[]> {
|
||||||
|
return this.client.request<AURPackage[]>("/api/v1/service/search", { query: { for: query } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async servicePackageUpdate(repository: RepositoryId, data: PackageActionRequest): Promise<void> {
|
||||||
|
return this.client.request("/api/v1/service/update", {
|
||||||
|
method: "POST",
|
||||||
|
query: repository.toQuery(),
|
||||||
|
json: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async servicePGPFetch(key: string, server: string): Promise<PGPKey> {
|
||||||
|
return this.client.request<PGPKey>("/api/v1/service/pgp", { query: { key, server } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async servicePGPImport(data: PGPKeyRequest): Promise<void> {
|
||||||
|
return this.client.request("/api/v1/service/pgp", { method: "POST", json: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async serviceRebuild(repository: RepositoryId, packages: string[]): Promise<void> {
|
||||||
|
return this.client.request("/api/v1/service/rebuild", {
|
||||||
|
method: "POST",
|
||||||
|
query: repository.toQuery(),
|
||||||
|
json: { packages },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
41
frontend/src/chartSetup.ts
Normal file
41
frontend/src/chartSetup.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
ArcElement,
|
||||||
|
BarElement,
|
||||||
|
CategoryScale,
|
||||||
|
Chart as ChartJS,
|
||||||
|
Legend,
|
||||||
|
LinearScale,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
Tooltip,
|
||||||
|
} from "chart.js";
|
||||||
|
|
||||||
|
ChartJS.register(
|
||||||
|
ArcElement,
|
||||||
|
BarElement,
|
||||||
|
CategoryScale,
|
||||||
|
Legend,
|
||||||
|
LinearScale,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
Tooltip,
|
||||||
|
);
|
||||||
43
frontend/src/components/charts/EventDurationLineChart.tsx
Normal file
43
frontend/src/components/charts/EventDurationLineChart.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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 type { Event } from "models/Event";
|
||||||
|
import type React from "react";
|
||||||
|
import { Line } from "react-chartjs-2";
|
||||||
|
|
||||||
|
interface EventDurationLineChartProps {
|
||||||
|
events: Event[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EventDurationLineChart({ events }: EventDurationLineChartProps): React.JSX.Element {
|
||||||
|
const updateEvents = events.filter(event => event.event === "package-updated");
|
||||||
|
const data = {
|
||||||
|
labels: updateEvents.map(event => new Date(event.created * 1000).toISOStringShort()),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "update duration, s",
|
||||||
|
data: updateEvents.map(event => event.data?.took ?? 0),
|
||||||
|
cubicInterpolationMode: "monotone" as const,
|
||||||
|
tension: 0.4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Line data={data} options={{ responsive: true }} />;
|
||||||
|
}
|
||||||
55
frontend/src/components/charts/PackageCountBarChart.tsx
Normal file
55
frontend/src/components/charts/PackageCountBarChart.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* 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 { blue, indigo } from "@mui/material/colors";
|
||||||
|
import type { RepositoryStats } from "models/RepositoryStats";
|
||||||
|
import type React from "react";
|
||||||
|
import { Bar } from "react-chartjs-2";
|
||||||
|
|
||||||
|
interface PackageCountBarChartProps {
|
||||||
|
stats: RepositoryStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PackageCountBarChart({ stats }: PackageCountBarChartProps): React.JSX.Element {
|
||||||
|
return <Bar
|
||||||
|
data={{
|
||||||
|
labels: ["packages"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "archives",
|
||||||
|
data: [stats.packages ?? 0],
|
||||||
|
backgroundColor: blue[500],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "bases",
|
||||||
|
data: [stats.bases ?? 0],
|
||||||
|
backgroundColor: indigo[300],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
x: { stacked: true },
|
||||||
|
y: { stacked: true },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
44
frontend/src/components/charts/StatusPieChart.tsx
Normal file
44
frontend/src/components/charts/StatusPieChart.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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 type { BuildStatus } from "models/BuildStatus.ts";
|
||||||
|
import type { Counters } from "models/Counters";
|
||||||
|
import type React from "react";
|
||||||
|
import { Pie } from "react-chartjs-2";
|
||||||
|
import { StatusColors } from "theme/StatusColors";
|
||||||
|
|
||||||
|
interface StatusPieChartProps {
|
||||||
|
counters: Counters;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StatusPieChart({ counters }: StatusPieChartProps): React.JSX.Element {
|
||||||
|
const labels = ["unknown", "pending", "building", "failed", "success"] as BuildStatus[];
|
||||||
|
const data = {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "packages in status",
|
||||||
|
data: labels.map(label => counters[label]),
|
||||||
|
backgroundColor: labels.map(label => StatusColors[label]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Pie data={data} options={{ responsive: true }} />;
|
||||||
|
}
|
||||||
90
frontend/src/components/common/AutoRefreshControl.tsx
Normal file
90
frontend/src/components/common/AutoRefreshControl.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* 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 CheckIcon from "@mui/icons-material/Check";
|
||||||
|
import TimerIcon from "@mui/icons-material/Timer";
|
||||||
|
import TimerOffIcon from "@mui/icons-material/TimerOff";
|
||||||
|
import { IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Tooltip } from "@mui/material";
|
||||||
|
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
interface AutoRefreshControlProps {
|
||||||
|
intervals: AutoRefreshInterval[];
|
||||||
|
currentInterval: number;
|
||||||
|
onIntervalChange: (interval: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AutoRefreshControl({
|
||||||
|
intervals,
|
||||||
|
currentInterval,
|
||||||
|
onIntervalChange,
|
||||||
|
}: AutoRefreshControlProps): React.JSX.Element | null {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
if (intervals.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabled = currentInterval > 0;
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Tooltip title="Auto-refresh">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={event => setAnchorEl(event.currentTarget)}
|
||||||
|
color={enabled ? "primary" : "default"}
|
||||||
|
>
|
||||||
|
{enabled ? <TimerIcon fontSize="small" /> : <TimerOffIcon fontSize="small" />}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
onClose={() => setAnchorEl(null)}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
selected={!enabled}
|
||||||
|
onClick={() => {
|
||||||
|
onIntervalChange(0);
|
||||||
|
setAnchorEl(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
{!enabled && <CheckIcon fontSize="small" />}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>Off</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
{intervals.map(interval =>
|
||||||
|
<MenuItem
|
||||||
|
key={interval.interval}
|
||||||
|
selected={enabled && interval.interval === currentInterval}
|
||||||
|
onClick={() => {
|
||||||
|
onIntervalChange(interval.interval);
|
||||||
|
setAnchorEl(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
{enabled && interval.interval === currentInterval && <CheckIcon fontSize="small" />}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>{interval.text}</ListItemText>
|
||||||
|
</MenuItem>,
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
63
frontend/src/components/common/CodeBlock.tsx
Normal file
63
frontend/src/components/common/CodeBlock.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Box } from "@mui/material";
|
||||||
|
import CopyButton from "components/common/CopyButton";
|
||||||
|
import React, { type RefObject } from "react";
|
||||||
|
|
||||||
|
interface CodeBlockProps {
|
||||||
|
preRef?: RefObject<HTMLElement | null>;
|
||||||
|
getText: () => string;
|
||||||
|
height?: number | string;
|
||||||
|
onScroll?: () => void;
|
||||||
|
wordBreak?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CodeBlock({
|
||||||
|
preRef,
|
||||||
|
getText,
|
||||||
|
height,
|
||||||
|
onScroll,
|
||||||
|
wordBreak,
|
||||||
|
}: CodeBlockProps): React.JSX.Element {
|
||||||
|
return <Box sx={{ position: "relative" }}>
|
||||||
|
<Box
|
||||||
|
ref={preRef}
|
||||||
|
component="pre"
|
||||||
|
onScroll={onScroll}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "grey.100",
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 1,
|
||||||
|
overflow: "auto",
|
||||||
|
height,
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
...wordBreak ? { whiteSpace: "pre-wrap", wordBreak: "break-all" } : {},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<code>
|
||||||
|
{getText()}
|
||||||
|
</code>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
||||||
|
<CopyButton getText={getText} />
|
||||||
|
</Box>
|
||||||
|
</Box>;
|
||||||
|
}
|
||||||
47
frontend/src/components/common/CopyButton.tsx
Normal file
47
frontend/src/components/common/CopyButton.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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 CheckIcon from "@mui/icons-material/Check";
|
||||||
|
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||||
|
import { IconButton, Tooltip } from "@mui/material";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
interface CopyButtonProps {
|
||||||
|
getText: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CopyButton({ getText }: CopyButtonProps): React.JSX.Element {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => () => clearTimeout(timer.current), []);
|
||||||
|
|
||||||
|
const handleCopy: () => Promise<void> = async () => {
|
||||||
|
await navigator.clipboard.writeText(getText());
|
||||||
|
setCopied(true);
|
||||||
|
clearTimeout(timer.current);
|
||||||
|
timer.current = setTimeout(() => setCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Tooltip title={copied ? "Copied!" : "Copy"}>
|
||||||
|
<IconButton size="small" aria-label={copied ? "Copied" : "Copy"} onClick={() => void handleCopy()}>
|
||||||
|
{copied ? <CheckIcon fontSize="small" /> : <ContentCopyIcon fontSize="small" />}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>;
|
||||||
|
}
|
||||||
37
frontend/src/components/common/DialogHeader.tsx
Normal file
37
frontend/src/components/common/DialogHeader.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* 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 CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import { DialogTitle, IconButton, type SxProps, type Theme } from "@mui/material";
|
||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
interface DialogHeaderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
onClose: () => void;
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DialogHeader({ children, onClose, sx }: DialogHeaderProps): React.JSX.Element {
|
||||||
|
return <DialogTitle sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", ...sx }}>
|
||||||
|
{children}
|
||||||
|
<IconButton aria-label="Close" onClick={onClose} size="small" sx={{ color: "inherit" }}>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</DialogTitle>;
|
||||||
|
}
|
||||||
44
frontend/src/components/common/RepositorySelect.tsx
Normal file
44
frontend/src/components/common/RepositorySelect.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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 { FormControl, InputLabel, MenuItem, Select } from "@mui/material";
|
||||||
|
import { useRepository } from "hooks/useRepository";
|
||||||
|
import type { SelectedRepositoryResult } from "hooks/useSelectedRepository";
|
||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
export default function RepositorySelect({
|
||||||
|
repositorySelect,
|
||||||
|
}: { repositorySelect: SelectedRepositoryResult }): React.JSX.Element {
|
||||||
|
const { repositories, current } = useRepository();
|
||||||
|
|
||||||
|
return <FormControl fullWidth margin="normal">
|
||||||
|
<InputLabel>repository</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={repositorySelect.selectedKey || (current?.key ?? "")}
|
||||||
|
label="repository"
|
||||||
|
onChange={event => repositorySelect.setSelectedKey(event.target.value)}
|
||||||
|
>
|
||||||
|
{repositories.map(repository =>
|
||||||
|
<MenuItem key={repository.key} value={repository.key}>
|
||||||
|
{repository.label}
|
||||||
|
</MenuItem>,
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>;
|
||||||
|
}
|
||||||
28
frontend/src/components/common/StringList.tsx
Normal file
28
frontend/src/components/common/StringList.tsx
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/>.
|
||||||
|
*/
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface StringListProps {
|
||||||
|
items: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StringList({ items }: StringListProps): React.JSX.Element {
|
||||||
|
return <>{items.join("\n")}</>;
|
||||||
|
}
|
||||||
103
frontend/src/components/dialogs/DashboardDialog.tsx
Normal file
103
frontend/src/components/dialogs/DashboardDialog.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Box, Dialog, DialogContent, Grid, Typography } from "@mui/material";
|
||||||
|
import { skipToken, useQuery } from "@tanstack/react-query";
|
||||||
|
import PackageCountBarChart from "components/charts/PackageCountBarChart";
|
||||||
|
import StatusPieChart from "components/charts/StatusPieChart";
|
||||||
|
import DialogHeader from "components/common/DialogHeader";
|
||||||
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
|
import { useClient } from "hooks/useClient";
|
||||||
|
import { useRepository } from "hooks/useRepository";
|
||||||
|
import type { InternalStatus } from "models/InternalStatus";
|
||||||
|
import type React from "react";
|
||||||
|
import { StatusHeaderStyles } from "theme/StatusColors";
|
||||||
|
|
||||||
|
interface DashboardDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardDialog({ open, onClose }: DashboardDialogProps): React.JSX.Element {
|
||||||
|
const client = useClient();
|
||||||
|
const { current } = useRepository();
|
||||||
|
|
||||||
|
const { data: status } = useQuery<InternalStatus>({
|
||||||
|
queryKey: current ? QueryKeys.status(current) : ["status"],
|
||||||
|
queryFn: current ? () => client.fetch.fetchServerStatus(current) : skipToken,
|
||||||
|
enabled: open,
|
||||||
|
});
|
||||||
|
|
||||||
|
const headerStyle = status ? StatusHeaderStyles[status.status.status] : {};
|
||||||
|
|
||||||
|
return <Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
|
||||||
|
<DialogHeader onClose={onClose} sx={headerStyle}>
|
||||||
|
System health
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
{status &&
|
||||||
|
<>
|
||||||
|
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||||
|
<Grid size={{ xs: 6, md: 3 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary" align="right">Repository name</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 6, md: 3 }}>
|
||||||
|
<Typography variant="body2">{status.repository}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 6, md: 3 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary" align="right">Repository architecture</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 6, md: 3 }}>
|
||||||
|
<Typography variant="body2">{status.architecture}</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||||
|
<Grid size={{ xs: 6, md: 3 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary" align="right">Current status</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 6, md: 3 }}>
|
||||||
|
<Typography variant="body2">{status.status.status}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 6, md: 3 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary" align="right">Updated at</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 6, md: 3 }}>
|
||||||
|
<Typography variant="body2">{new Date(status.status.timestamp * 1000).toISOStringShort()}</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid container spacing={2} sx={{ mt: 2 }}>
|
||||||
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
|
<Box sx={{ maxHeight: 300 }}>
|
||||||
|
<PackageCountBarChart stats={status.stats} />
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
|
<Box sx={{ maxHeight: 300, display: "flex", justifyContent: "center", alignItems: "center" }}>
|
||||||
|
<StatusPieChart counters={status.packages} />
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>;
|
||||||
|
}
|
||||||
118
frontend/src/components/dialogs/KeyImportDialog.tsx
Normal file
118
frontend/src/components/dialogs/KeyImportDialog.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* 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 PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||||
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { ApiError } from "api/client/ApiError";
|
||||||
|
import CodeBlock from "components/common/CodeBlock";
|
||||||
|
import DialogHeader from "components/common/DialogHeader";
|
||||||
|
import { useClient } from "hooks/useClient";
|
||||||
|
import { useNotification } from "hooks/useNotification";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
interface KeyImportDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function KeyImportDialog({ open, onClose }: KeyImportDialogProps): React.JSX.Element {
|
||||||
|
const client = useClient();
|
||||||
|
const { showSuccess, showError } = useNotification();
|
||||||
|
|
||||||
|
const [fingerprint, setFingerprint] = useState("");
|
||||||
|
const [server, setServer] = useState("keyserver.ubuntu.com");
|
||||||
|
const [keyBody, setKeyBody] = useState("");
|
||||||
|
|
||||||
|
const handleClose = (): void => {
|
||||||
|
setFingerprint("");
|
||||||
|
setServer("keyserver.ubuntu.com");
|
||||||
|
setKeyBody("");
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFetch: () => Promise<void> = async () => {
|
||||||
|
if (!fingerprint || !server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await client.service.servicePGPFetch(fingerprint, server);
|
||||||
|
setKeyBody(result.key);
|
||||||
|
} catch (exception) {
|
||||||
|
const detail = ApiError.errorDetail(exception);
|
||||||
|
showError("Action failed", `Could not fetch key: ${detail}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImport: () => Promise<void> = async () => {
|
||||||
|
if (!fingerprint || !server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await client.service.servicePGPImport({ key: fingerprint, server });
|
||||||
|
handleClose();
|
||||||
|
showSuccess("Success", `Key ${fingerprint} has been imported`);
|
||||||
|
} catch (exception) {
|
||||||
|
const detail = ApiError.errorDetail(exception);
|
||||||
|
showError("Action failed", `Could not import key ${fingerprint} from ${server}: ${detail}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Dialog open={open} onClose={handleClose} maxWidth="lg" fullWidth>
|
||||||
|
<DialogHeader onClose={handleClose}>
|
||||||
|
Import key from PGP server
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
label="fingerprint"
|
||||||
|
placeholder="PGP key fingerprint"
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
value={fingerprint}
|
||||||
|
onChange={event => setFingerprint(event.target.value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="key server"
|
||||||
|
placeholder="PGP key server"
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
value={server}
|
||||||
|
onChange={event => setServer(event.target.value)}
|
||||||
|
/>
|
||||||
|
{keyBody &&
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<CodeBlock getText={() => keyBody} height={300} />
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => void handleImport()} variant="contained" startIcon={<PlayArrowIcon />}>import</Button>
|
||||||
|
<Button onClick={() => void handleFetch()} variant="contained" color="success" startIcon={<RefreshIcon />}>fetch</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>;
|
||||||
|
}
|
||||||
118
frontend/src/components/dialogs/LoginDialog.tsx
Normal file
118
frontend/src/components/dialogs/LoginDialog.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* 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 PersonIcon from "@mui/icons-material/Person";
|
||||||
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
|
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
IconButton,
|
||||||
|
InputAdornment,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { ApiError } from "api/client/ApiError";
|
||||||
|
import DialogHeader from "components/common/DialogHeader";
|
||||||
|
import { useAuth } from "hooks/useAuth";
|
||||||
|
import { useNotification } from "hooks/useNotification";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
interface LoginDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LoginDialog({ open, onClose }: LoginDialogProps): React.JSX.Element {
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const { login } = useAuth();
|
||||||
|
const { showSuccess, showError } = useNotification();
|
||||||
|
|
||||||
|
const handleClose = (): void => {
|
||||||
|
setUsername("");
|
||||||
|
setPassword("");
|
||||||
|
setShowPassword(false);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit: () => Promise<void> = async () => {
|
||||||
|
if (!username || !password) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await login(username, password);
|
||||||
|
handleClose();
|
||||||
|
showSuccess("Logged in", `Successfully logged in as ${username}`);
|
||||||
|
} catch (exception) {
|
||||||
|
const detail = ApiError.errorDetail(exception);
|
||||||
|
if (username === "admin" && password === "admin") {
|
||||||
|
showError("Login error", "You've entered a password for user \"root\", did you make a typo in username?");
|
||||||
|
} else {
|
||||||
|
showError("Login error", `Could not login as ${username}: ${detail}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Dialog open={open} onClose={handleClose} maxWidth="xs" fullWidth>
|
||||||
|
<DialogHeader onClose={handleClose}>
|
||||||
|
Login
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
label="username"
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
value={username}
|
||||||
|
onChange={event => setUsername(event.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="password"
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
value={password}
|
||||||
|
onChange={event => setPassword(event.target.value)}
|
||||||
|
onKeyDown={event => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
void handleSubmit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
endAdornment:
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton aria-label={showPassword ? "Hide password" : "Show password"} onClick={() => setShowPassword(!showPassword)} edge="end" size="small">
|
||||||
|
{showPassword ? <VisibilityOffIcon /> : <VisibilityIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => void handleSubmit()} variant="contained" startIcon={<PersonIcon />}>login</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>;
|
||||||
|
}
|
||||||
193
frontend/src/components/dialogs/PackageAddDialog.tsx
Normal file
193
frontend/src/components/dialogs/PackageAddDialog.tsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
/*
|
||||||
|
* 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 AddIcon from "@mui/icons-material/Add";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||||
|
import {
|
||||||
|
Autocomplete,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
FormControlLabel,
|
||||||
|
IconButton,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { ApiError } from "api/client/ApiError";
|
||||||
|
import DialogHeader from "components/common/DialogHeader";
|
||||||
|
import RepositorySelect from "components/common/RepositorySelect";
|
||||||
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
|
import { useClient } from "hooks/useClient";
|
||||||
|
import { useDebounce } from "hooks/useDebounce";
|
||||||
|
import { useNotification } from "hooks/useNotification";
|
||||||
|
import { useSelectedRepository } from "hooks/useSelectedRepository";
|
||||||
|
import type { AURPackage } from "models/AURPackage";
|
||||||
|
import type { PackageActionRequest } from "models/PackageActionRequest";
|
||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
|
||||||
|
interface EnvironmentVariable {
|
||||||
|
id: number;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PackageAddDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PackageAddDialog({ open, onClose }: PackageAddDialogProps): React.JSX.Element {
|
||||||
|
const client = useClient();
|
||||||
|
const { showSuccess, showError } = useNotification();
|
||||||
|
const repositorySelect = useSelectedRepository();
|
||||||
|
|
||||||
|
const [packageName, setPackageName] = useState("");
|
||||||
|
const [refreshDatabase, setRefreshDatabase] = useState(true);
|
||||||
|
const [environmentVariables, setEnvironmentVariables] = useState<EnvironmentVariable[]>([]);
|
||||||
|
const variableIdCounter = useRef(0);
|
||||||
|
|
||||||
|
const handleClose = (): void => {
|
||||||
|
setPackageName("");
|
||||||
|
repositorySelect.reset();
|
||||||
|
setRefreshDatabase(true);
|
||||||
|
setEnvironmentVariables([]);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const debouncedSearch = useDebounce(packageName, 500);
|
||||||
|
|
||||||
|
const { data: searchResults = [] } = useQuery<AURPackage[]>({
|
||||||
|
queryKey: QueryKeys.search(debouncedSearch),
|
||||||
|
queryFn: () => client.service.servicePackageSearch(debouncedSearch),
|
||||||
|
enabled: debouncedSearch.length >= 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async (action: "add" | "request"): Promise<void> => {
|
||||||
|
if (!packageName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const repository = repositorySelect.selectedRepository;
|
||||||
|
if (!repository) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const patches = environmentVariables.filter(variable => variable.key);
|
||||||
|
const request: PackageActionRequest = { packages: [packageName], patches };
|
||||||
|
if (action === "add") {
|
||||||
|
request.refresh = refreshDatabase;
|
||||||
|
await client.service.servicePackageAdd(repository, request);
|
||||||
|
} else {
|
||||||
|
await client.service.servicePackageRequest(repository, request);
|
||||||
|
}
|
||||||
|
handleClose();
|
||||||
|
showSuccess("Success", `Packages ${packageName} have been ${action === "add" ? "added" : "requested"}`);
|
||||||
|
} catch (exception) {
|
||||||
|
const detail = ApiError.errorDetail(exception);
|
||||||
|
showError("Action failed", `Package ${action} failed: ${detail}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
|
||||||
|
<DialogHeader onClose={handleClose}>
|
||||||
|
Add new packages
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<RepositorySelect repositorySelect={repositorySelect} />
|
||||||
|
|
||||||
|
<Autocomplete
|
||||||
|
freeSolo
|
||||||
|
options={searchResults.map(pkg => pkg.package)}
|
||||||
|
inputValue={packageName}
|
||||||
|
onInputChange={(_, value) => setPackageName(value)}
|
||||||
|
renderOption={(props, option) => {
|
||||||
|
const pkg = searchResults.find(pkg => pkg.package === option);
|
||||||
|
return (
|
||||||
|
<li {...props} key={option}>
|
||||||
|
{option}{pkg ? ` (${pkg.description})` : ""}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
renderInput={params =>
|
||||||
|
<TextField {...params} label="package" placeholder="AUR package" margin="normal" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControlLabel
|
||||||
|
control={<Checkbox checked={refreshDatabase} onChange={(_, checked) => setRefreshDatabase(checked)} />}
|
||||||
|
label="update pacman databases"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
const id = variableIdCounter.current++;
|
||||||
|
setEnvironmentVariables(prev => [...prev, { id, key: "", value: "" }]);
|
||||||
|
}}
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
>
|
||||||
|
add environment variable
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{environmentVariables.map(variable =>
|
||||||
|
<Box key={variable.id} sx={{ display: "flex", gap: 1, mt: 1, alignItems: "center" }}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
placeholder="name"
|
||||||
|
value={variable.key}
|
||||||
|
onChange={event => {
|
||||||
|
const newKey = event.target.value;
|
||||||
|
setEnvironmentVariables(prev =>
|
||||||
|
prev.map(entry => entry.id === variable.id ? { ...entry, key: newKey } : entry),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<Box>=</Box>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
placeholder="value"
|
||||||
|
value={variable.value}
|
||||||
|
onChange={event => {
|
||||||
|
const newValue = event.target.value;
|
||||||
|
setEnvironmentVariables(prev =>
|
||||||
|
prev.map(entry => entry.id === variable.id ? { ...entry, value: newValue } : entry),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<IconButton size="small" color="error" aria-label="Remove variable" onClick={() => setEnvironmentVariables(prev => prev.filter(entry => entry.id !== variable.id))}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>,
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => void handleSubmit("add")} variant="contained" startIcon={<PlayArrowIcon />}>add</Button>
|
||||||
|
<Button onClick={() => void handleSubmit("request")} variant="contained" color="success" startIcon={<AddIcon />}>request</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>;
|
||||||
|
}
|
||||||
194
frontend/src/components/dialogs/PackageInfoDialog.tsx
Normal file
194
frontend/src/components/dialogs/PackageInfoDialog.tsx
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Box, Dialog, DialogContent, Tab, Tabs } from "@mui/material";
|
||||||
|
import { skipToken, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { ApiError } from "api/client/ApiError";
|
||||||
|
import DialogHeader from "components/common/DialogHeader";
|
||||||
|
import BuildLogsTab from "components/package/BuildLogsTab";
|
||||||
|
import ChangesTab from "components/package/ChangesTab";
|
||||||
|
import EventsTab from "components/package/EventsTab";
|
||||||
|
import PackageDetailsGrid from "components/package/PackageDetailsGrid";
|
||||||
|
import PackageInfoActions from "components/package/PackageInfoActions";
|
||||||
|
import PackagePatchesList from "components/package/PackagePatchesList";
|
||||||
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
|
import { useAuth } from "hooks/useAuth";
|
||||||
|
import { useAutoRefresh } from "hooks/useAutoRefresh";
|
||||||
|
import { useClient } from "hooks/useClient";
|
||||||
|
import { useNotification } from "hooks/useNotification";
|
||||||
|
import { useRepository } from "hooks/useRepository";
|
||||||
|
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
||||||
|
import type { Dependencies } from "models/Dependencies";
|
||||||
|
import type { PackageStatus } from "models/PackageStatus";
|
||||||
|
import type { Patch } from "models/Patch";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { StatusHeaderStyles } from "theme/StatusColors";
|
||||||
|
import { defaultInterval } from "utils";
|
||||||
|
|
||||||
|
interface PackageInfoDialogProps {
|
||||||
|
packageBase: string | null;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
autoRefreshIntervals: AutoRefreshInterval[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PackageInfoDialog({
|
||||||
|
packageBase,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
autoRefreshIntervals,
|
||||||
|
}: PackageInfoDialogProps): React.JSX.Element {
|
||||||
|
const client = useClient();
|
||||||
|
const { current } = useRepository();
|
||||||
|
const { isAuthorized } = useAuth();
|
||||||
|
const { showSuccess, showError } = useNotification();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const [localPackageBase, setLocalPackageBase] = useState(packageBase);
|
||||||
|
if (packageBase !== null && packageBase !== localPackageBase) {
|
||||||
|
setLocalPackageBase(packageBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [tabIndex, setTabIndex] = useState(0);
|
||||||
|
const [refreshDatabase, setRefreshDatabase] = useState(true);
|
||||||
|
|
||||||
|
const handleClose = (): void => {
|
||||||
|
setTabIndex(0);
|
||||||
|
setRefreshDatabase(true);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoRefresh = useAutoRefresh("package-info-autoreload-button", defaultInterval(autoRefreshIntervals));
|
||||||
|
|
||||||
|
const { data: packageData } = useQuery<PackageStatus[]>({
|
||||||
|
queryKey: localPackageBase && current ? QueryKeys.package(localPackageBase, current) : ["packages"],
|
||||||
|
queryFn: localPackageBase && current ? () => client.fetch.fetchPackage(localPackageBase, current) : skipToken,
|
||||||
|
enabled: open,
|
||||||
|
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: dependencies } = useQuery<Dependencies>({
|
||||||
|
queryKey: localPackageBase && current ? QueryKeys.dependencies(localPackageBase, current) : ["dependencies"],
|
||||||
|
queryFn: localPackageBase && current
|
||||||
|
? () => client.fetch.fetchPackageDependencies(localPackageBase, current) : skipToken,
|
||||||
|
enabled: open,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: patches = [] } = useQuery<Patch[]>({
|
||||||
|
queryKey: localPackageBase ? QueryKeys.patches(localPackageBase) : ["patches"],
|
||||||
|
queryFn: localPackageBase ? () => client.fetch.fetchPackagePatches(localPackageBase) : skipToken,
|
||||||
|
enabled: open,
|
||||||
|
});
|
||||||
|
|
||||||
|
const description: PackageStatus | undefined = packageData?.[0];
|
||||||
|
const pkg = description?.package;
|
||||||
|
const status = description?.status;
|
||||||
|
const headerStyle = status ? StatusHeaderStyles[status.status] : {};
|
||||||
|
|
||||||
|
const handleUpdate: () => Promise<void> = async () => {
|
||||||
|
if (!localPackageBase || !current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await client.service.servicePackageAdd(current, { packages: [localPackageBase], refresh: refreshDatabase });
|
||||||
|
showSuccess("Success", `Run update for packages ${localPackageBase}`);
|
||||||
|
} catch (exception) {
|
||||||
|
showError("Action failed", `Package update failed: ${ApiError.errorDetail(exception)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove: () => Promise<void> = async () => {
|
||||||
|
if (!localPackageBase || !current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await client.service.servicePackageRemove(current, [localPackageBase]);
|
||||||
|
showSuccess("Success", `Packages ${localPackageBase} have been removed`);
|
||||||
|
onClose();
|
||||||
|
} catch (exception) {
|
||||||
|
showError("Action failed", `Could not remove package: ${ApiError.errorDetail(exception)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletePatch: (key: string) => Promise<void> = async key => {
|
||||||
|
if (!localPackageBase) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await client.service.servicePackagePatchRemove(localPackageBase, key);
|
||||||
|
void queryClient.invalidateQueries({ queryKey: QueryKeys.patches(localPackageBase) });
|
||||||
|
} catch (exception) {
|
||||||
|
showError("Action failed", `Could not delete variable: ${ApiError.errorDetail(exception)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Dialog open={open} onClose={handleClose} maxWidth="lg" fullWidth>
|
||||||
|
<DialogHeader onClose={handleClose} sx={headerStyle}>
|
||||||
|
{pkg && status
|
||||||
|
? `${pkg.base} ${status.status} at ${new Date(status.timestamp * 1000).toISOStringShort()}`
|
||||||
|
: localPackageBase ?? ""}
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
{pkg &&
|
||||||
|
<>
|
||||||
|
<PackageDetailsGrid pkg={pkg} dependencies={dependencies} />
|
||||||
|
<PackagePatchesList
|
||||||
|
patches={patches}
|
||||||
|
editable={isAuthorized}
|
||||||
|
onDelete={key => void handleDeletePatch(key)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box sx={{ borderBottom: 1, borderColor: "divider", mt: 2 }}>
|
||||||
|
<Tabs value={tabIndex} onChange={(_, index: number) => setTabIndex(index)}>
|
||||||
|
<Tab label="Build logs" />
|
||||||
|
<Tab label="Changes" />
|
||||||
|
<Tab label="Events" />
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{tabIndex === 0 && localPackageBase && current &&
|
||||||
|
<BuildLogsTab
|
||||||
|
packageBase={localPackageBase}
|
||||||
|
repository={current}
|
||||||
|
refreshInterval={autoRefresh.interval}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{tabIndex === 1 && localPackageBase && current &&
|
||||||
|
<ChangesTab packageBase={localPackageBase} repository={current} />
|
||||||
|
}
|
||||||
|
{tabIndex === 2 && localPackageBase && current &&
|
||||||
|
<EventsTab packageBase={localPackageBase} repository={current} />
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<PackageInfoActions
|
||||||
|
isAuthorized={isAuthorized}
|
||||||
|
refreshDatabase={refreshDatabase}
|
||||||
|
onRefreshDatabaseChange={setRefreshDatabase}
|
||||||
|
onUpdate={() => void handleUpdate()}
|
||||||
|
onRemove={() => void handleRemove()}
|
||||||
|
autoRefreshIntervals={autoRefreshIntervals}
|
||||||
|
autoRefreshInterval={autoRefresh.interval}
|
||||||
|
onAutoRefreshIntervalChange={autoRefresh.setInterval}
|
||||||
|
/>
|
||||||
|
</Dialog>;
|
||||||
|
}
|
||||||
88
frontend/src/components/dialogs/PackageRebuildDialog.tsx
Normal file
88
frontend/src/components/dialogs/PackageRebuildDialog.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* 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 PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||||
|
import { Button, Dialog, DialogActions, DialogContent, TextField } from "@mui/material";
|
||||||
|
import { ApiError } from "api/client/ApiError";
|
||||||
|
import DialogHeader from "components/common/DialogHeader";
|
||||||
|
import RepositorySelect from "components/common/RepositorySelect";
|
||||||
|
import { useClient } from "hooks/useClient";
|
||||||
|
import { useNotification } from "hooks/useNotification";
|
||||||
|
import { useSelectedRepository } from "hooks/useSelectedRepository";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
interface PackageRebuildDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PackageRebuildDialog({ open, onClose }: PackageRebuildDialogProps): React.JSX.Element {
|
||||||
|
const client = useClient();
|
||||||
|
const { showSuccess, showError } = useNotification();
|
||||||
|
const repositorySelect = useSelectedRepository();
|
||||||
|
|
||||||
|
const [dependency, setDependency] = useState("");
|
||||||
|
|
||||||
|
const handleClose = (): void => {
|
||||||
|
setDependency("");
|
||||||
|
repositorySelect.reset();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRebuild: () => Promise<void> = async () => {
|
||||||
|
if (!dependency) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const repository = repositorySelect.selectedRepository;
|
||||||
|
if (!repository) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await client.service.serviceRebuild(repository, [dependency]);
|
||||||
|
handleClose();
|
||||||
|
showSuccess("Success", `Repository rebuild has been run for packages which depend on ${dependency}`);
|
||||||
|
} catch (exception) {
|
||||||
|
const detail = ApiError.errorDetail(exception);
|
||||||
|
showError("Action failed", `Repository rebuild failed: ${detail}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
|
||||||
|
<DialogHeader onClose={handleClose}>
|
||||||
|
Rebuild depending packages
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<RepositorySelect repositorySelect={repositorySelect} />
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="dependency"
|
||||||
|
placeholder="packages dependency"
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
value={dependency}
|
||||||
|
onChange={event => setDependency(event.target.value)}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => void handleRebuild()} variant="contained" startIcon={<PlayArrowIcon />}>rebuild</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>;
|
||||||
|
}
|
||||||
76
frontend/src/components/layout/AppLayout.tsx
Normal file
76
frontend/src/components/layout/AppLayout.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Box, Container } from "@mui/material";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import LoginDialog from "components/dialogs/LoginDialog";
|
||||||
|
import Footer from "components/layout/Footer";
|
||||||
|
import Navbar from "components/layout/Navbar";
|
||||||
|
import PackageTable from "components/table/PackageTable";
|
||||||
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
|
import { useAuth } from "hooks/useAuth";
|
||||||
|
import { useClient } from "hooks/useClient";
|
||||||
|
import { useRepository } from "hooks/useRepository";
|
||||||
|
import type { InfoResponse } from "models/InfoResponse";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function AppLayout(): React.JSX.Element {
|
||||||
|
const client = useClient();
|
||||||
|
const { setAuthState } = useAuth();
|
||||||
|
const { setRepositories } = useRepository();
|
||||||
|
const [loginOpen, setLoginOpen] = useState(false);
|
||||||
|
|
||||||
|
const { data: info } = useQuery<InfoResponse>({
|
||||||
|
queryKey: QueryKeys.info,
|
||||||
|
queryFn: () => client.fetch.fetchServerInfo(),
|
||||||
|
staleTime: Infinity,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync info to contexts when loaded
|
||||||
|
useEffect(() => {
|
||||||
|
if (info) {
|
||||||
|
setAuthState({ enabled: info.auth.enabled, username: info.auth.username ?? null });
|
||||||
|
setRepositories(info.repositories);
|
||||||
|
}
|
||||||
|
}, [info, setAuthState, setRepositories]);
|
||||||
|
|
||||||
|
return <Container maxWidth="xl">
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", py: 1, gap: 1 }}>
|
||||||
|
<a href="https://github.com/arcan1s/ahriman" title="logo">
|
||||||
|
<img src="/static/logo.svg" width={30} height={30} alt="" />
|
||||||
|
</a>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Navbar />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<PackageTable
|
||||||
|
autoRefreshIntervals={info?.autorefresh_intervals ?? []}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Footer
|
||||||
|
version={info?.version ?? ""}
|
||||||
|
docsEnabled={info?.docs_enabled ?? false}
|
||||||
|
indexUrl={info?.index_url}
|
||||||
|
onLoginClick={() => info?.auth.external ? window.location.assign("/api/v1/login") : setLoginOpen(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LoginDialog open={loginOpen} onClose={() => setLoginOpen(false)} />
|
||||||
|
</Container>;
|
||||||
|
}
|
||||||
92
frontend/src/components/layout/Footer.tsx
Normal file
92
frontend/src/components/layout/Footer.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* 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 GitHubIcon from "@mui/icons-material/GitHub";
|
||||||
|
import HomeIcon from "@mui/icons-material/Home";
|
||||||
|
import LoginIcon from "@mui/icons-material/Login";
|
||||||
|
import LogoutIcon from "@mui/icons-material/Logout";
|
||||||
|
import { Box, Button, Link, Typography } from "@mui/material";
|
||||||
|
import { useAuth } from "hooks/useAuth";
|
||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
interface FooterProps {
|
||||||
|
version: string;
|
||||||
|
docsEnabled: boolean;
|
||||||
|
indexUrl?: string;
|
||||||
|
onLoginClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Footer({ version, docsEnabled, indexUrl, onLoginClick }: FooterProps): React.JSX.Element {
|
||||||
|
const { enabled: authEnabled, username, logout } = useAuth();
|
||||||
|
|
||||||
|
return <Box
|
||||||
|
component="footer"
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
borderTop: 1,
|
||||||
|
borderColor: "divider",
|
||||||
|
mt: 2,
|
||||||
|
py: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: "flex", gap: 2, alignItems: "center" }}>
|
||||||
|
<Link href="https://github.com/arcan1s/ahriman" underline="hover" color="inherit" sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
||||||
|
<GitHubIcon fontSize="small" />
|
||||||
|
<Typography variant="body2">ahriman {version}</Typography>
|
||||||
|
</Link>
|
||||||
|
<Link href="https://github.com/arcan1s/ahriman/releases" underline="hover" color="text.secondary" variant="body2">
|
||||||
|
releases
|
||||||
|
</Link>
|
||||||
|
<Link href="https://github.com/arcan1s/ahriman/issues" underline="hover" color="text.secondary" variant="body2">
|
||||||
|
report a bug
|
||||||
|
</Link>
|
||||||
|
{docsEnabled &&
|
||||||
|
<Link href="/api-docs" underline="hover" color="text.secondary" variant="body2">
|
||||||
|
api
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{indexUrl &&
|
||||||
|
<Box>
|
||||||
|
<Link href={indexUrl} underline="hover" color="inherit" sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
||||||
|
<HomeIcon fontSize="small" />
|
||||||
|
<Typography variant="body2">repo index</Typography>
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
|
||||||
|
{authEnabled &&
|
||||||
|
<Box>
|
||||||
|
{username ?
|
||||||
|
<Button size="small" startIcon={<LogoutIcon />} onClick={() => void logout()} sx={{ textTransform: "none" }}>
|
||||||
|
logout ({username})
|
||||||
|
</Button>
|
||||||
|
:
|
||||||
|
<Button size="small" startIcon={<LoginIcon />} onClick={onLoginClick} sx={{ textTransform: "none" }}>
|
||||||
|
login
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
</Box>;
|
||||||
|
}
|
||||||
55
frontend/src/components/layout/Navbar.tsx
Normal file
55
frontend/src/components/layout/Navbar.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Box, Tab, Tabs } from "@mui/material";
|
||||||
|
import { useRepository } from "hooks/useRepository";
|
||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
export default function Navbar(): React.JSX.Element | null {
|
||||||
|
const { repositories, current, setCurrent } = useRepository();
|
||||||
|
|
||||||
|
if (repositories.length === 0 || !current) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIndex = repositories.findIndex(repository =>
|
||||||
|
repository.architecture === current.architecture && repository.repository === current.repository,
|
||||||
|
);
|
||||||
|
|
||||||
|
return <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||||
|
<Tabs
|
||||||
|
value={currentIndex >= 0 ? currentIndex : 0}
|
||||||
|
onChange={(_, newValue: number) => {
|
||||||
|
const repository = repositories[newValue];
|
||||||
|
if (repository) {
|
||||||
|
setCurrent(repository);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
variant="scrollable"
|
||||||
|
scrollButtons="auto"
|
||||||
|
>
|
||||||
|
{repositories.map(repository =>
|
||||||
|
<Tab
|
||||||
|
key={repository.key}
|
||||||
|
label={repository.label}
|
||||||
|
/>,
|
||||||
|
)}
|
||||||
|
</Tabs>
|
||||||
|
</Box>;
|
||||||
|
}
|
||||||
185
frontend/src/components/package/BuildLogsTab.tsx
Normal file
185
frontend/src/components/package/BuildLogsTab.tsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
/*
|
||||||
|
* 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 ListIcon from "@mui/icons-material/List";
|
||||||
|
import { Box, Button, Menu, MenuItem, Typography } from "@mui/material";
|
||||||
|
import { keepPreviousData, skipToken, useQuery } from "@tanstack/react-query";
|
||||||
|
import CodeBlock from "components/common/CodeBlock";
|
||||||
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
|
import { useAutoScroll } from "hooks/useAutoScroll";
|
||||||
|
import { useClient } from "hooks/useClient";
|
||||||
|
import type { LogRecord } from "models/LogRecord";
|
||||||
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
interface Logs {
|
||||||
|
version: string;
|
||||||
|
processId: string;
|
||||||
|
created: number;
|
||||||
|
logs: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BuildLogsTabProps {
|
||||||
|
packageBase: string;
|
||||||
|
repository: RepositoryId;
|
||||||
|
refreshInterval: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertLogs(records: LogRecord[], filter?: (record: LogRecord) => boolean): string {
|
||||||
|
const filtered = filter ? records.filter(filter) : records;
|
||||||
|
return filtered
|
||||||
|
.map(record => `[${new Date(record.created * 1000).toISOString()}] ${record.message}`)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BuildLogsTab({
|
||||||
|
packageBase,
|
||||||
|
repository,
|
||||||
|
refreshInterval,
|
||||||
|
}: BuildLogsTabProps): React.JSX.Element {
|
||||||
|
const client = useClient();
|
||||||
|
const [selectedVersionKey, setSelectedVersionKey] = useState<string | null>(null);
|
||||||
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const { data: allLogs } = useQuery<LogRecord[]>({
|
||||||
|
queryKey: QueryKeys.logs(packageBase, repository),
|
||||||
|
queryFn: () => client.fetch.fetchPackageLogs(packageBase, repository),
|
||||||
|
enabled: !!packageBase,
|
||||||
|
refetchInterval: refreshInterval > 0 ? refreshInterval : false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build version selectors from all logs
|
||||||
|
const versions = useMemo<Logs[]>(() => {
|
||||||
|
if (!allLogs || allLogs.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const grouped: Record<string, LogRecord & { minCreated: number }> = {};
|
||||||
|
for (const record of allLogs) {
|
||||||
|
const key = `${record.version}-${record.process_id}`;
|
||||||
|
const existing = grouped[key];
|
||||||
|
if (!existing) {
|
||||||
|
grouped[key] = { ...record, minCreated: record.created };
|
||||||
|
} else {
|
||||||
|
existing.minCreated = Math.min(existing.minCreated, record.created);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(grouped)
|
||||||
|
.sort((left, right) => right.minCreated - left.minCreated)
|
||||||
|
.map(record => ({
|
||||||
|
version: record.version,
|
||||||
|
processId: record.process_id,
|
||||||
|
created: record.minCreated,
|
||||||
|
logs: convertLogs(
|
||||||
|
allLogs,
|
||||||
|
right => record.version === right.version && record.process_id === right.process_id,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}, [allLogs]);
|
||||||
|
|
||||||
|
// Compute active index from selected version key, defaulting to newest (index 0)
|
||||||
|
const activeIndex = useMemo(() => {
|
||||||
|
if (selectedVersionKey) {
|
||||||
|
const index = versions.findIndex(record => `${record.version}-${record.processId}` === selectedVersionKey);
|
||||||
|
if (index >= 0) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}, [versions, selectedVersionKey]);
|
||||||
|
|
||||||
|
const activeVersion = versions[activeIndex];
|
||||||
|
const activeVersionKey = activeVersion ? `${activeVersion.version}-${activeVersion.processId}` : null;
|
||||||
|
|
||||||
|
// Refresh active version logs
|
||||||
|
const { data: versionLogs } = useQuery<LogRecord[]>({
|
||||||
|
queryKey: QueryKeys.logsVersion(packageBase, repository, activeVersion?.version ?? "", activeVersion?.processId ?? ""),
|
||||||
|
queryFn: activeVersion
|
||||||
|
? () => client.fetch.fetchPackageLogs(
|
||||||
|
packageBase, repository, activeVersion.version, activeVersion.processId,
|
||||||
|
)
|
||||||
|
: skipToken,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
refetchInterval: refreshInterval > 0 ? refreshInterval : false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Derive displayed logs: prefer fresh polled data when available
|
||||||
|
const displayedLogs = useMemo(() => {
|
||||||
|
if (versionLogs && versionLogs.length > 0) {
|
||||||
|
return convertLogs(versionLogs);
|
||||||
|
}
|
||||||
|
return activeVersion?.logs ?? "";
|
||||||
|
}, [versionLogs, activeVersion]);
|
||||||
|
|
||||||
|
const { preRef, handleScroll, scrollToBottom, resetScroll } = useAutoScroll();
|
||||||
|
|
||||||
|
// Reset scroll tracking when active version changes
|
||||||
|
useEffect(() => {
|
||||||
|
resetScroll();
|
||||||
|
}, [activeVersionKey, resetScroll]);
|
||||||
|
|
||||||
|
// Scroll to bottom on new logs
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [displayedLogs, scrollToBottom]);
|
||||||
|
|
||||||
|
return <Box sx={{ display: "flex", gap: 1, mt: 1 }}>
|
||||||
|
<Box>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
aria-label="Select version"
|
||||||
|
startIcon={<ListIcon />}
|
||||||
|
onClick={event => setAnchorEl(event.currentTarget)}
|
||||||
|
/>
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
onClose={() => setAnchorEl(null)}
|
||||||
|
>
|
||||||
|
{versions.map((logs, index) =>
|
||||||
|
<MenuItem
|
||||||
|
key={`${logs.version}-${logs.processId}`}
|
||||||
|
selected={index === activeIndex}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedVersionKey(`${logs.version}-${logs.processId}`);
|
||||||
|
setAnchorEl(null);
|
||||||
|
resetScroll();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2">{new Date(logs.created * 1000).toISOStringShort()}</Typography>
|
||||||
|
</MenuItem>,
|
||||||
|
)}
|
||||||
|
{versions.length === 0 &&
|
||||||
|
<MenuItem disabled>No logs available</MenuItem>
|
||||||
|
}
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<CodeBlock
|
||||||
|
preRef={preRef}
|
||||||
|
getText={() => displayedLogs}
|
||||||
|
height={400}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
wordBreak
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>;
|
||||||
|
}
|
||||||
70
frontend/src/components/package/ChangesTab.tsx
Normal file
70
frontend/src/components/package/ChangesTab.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Box } from "@mui/material";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import CopyButton from "components/common/CopyButton";
|
||||||
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
|
import { useClient } from "hooks/useClient";
|
||||||
|
import type { Changes } from "models/Changes";
|
||||||
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
import React 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 {
|
||||||
|
packageBase: string;
|
||||||
|
repository: RepositoryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChangesTab({ packageBase, repository }: ChangesTabProps): React.JSX.Element {
|
||||||
|
const client = useClient();
|
||||||
|
|
||||||
|
const { data } = useQuery<Changes>({
|
||||||
|
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>;
|
||||||
|
}
|
||||||
78
frontend/src/components/package/EventsTab.tsx
Normal file
78
frontend/src/components/package/EventsTab.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Box } from "@mui/material";
|
||||||
|
import { DataGrid, type GridColDef } from "@mui/x-data-grid";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import EventDurationLineChart from "components/charts/EventDurationLineChart";
|
||||||
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
|
import { useClient } from "hooks/useClient";
|
||||||
|
import type { Event } from "models/Event";
|
||||||
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
interface EventsTabProps {
|
||||||
|
packageBase: string;
|
||||||
|
repository: RepositoryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventRow {
|
||||||
|
id: number;
|
||||||
|
timestamp: string;
|
||||||
|
event: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: GridColDef<EventRow>[] = [
|
||||||
|
{ field: "timestamp", headerName: "date", width: 180, align: "right", headerAlign: "right" },
|
||||||
|
{ field: "event", headerName: "event", flex: 1 },
|
||||||
|
{ field: "message", headerName: "description", flex: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function EventsTab({ packageBase, repository }: EventsTabProps): React.JSX.Element {
|
||||||
|
const client = useClient();
|
||||||
|
|
||||||
|
const { data: events = [] } = useQuery<Event[]>({
|
||||||
|
queryKey: QueryKeys.events(repository, packageBase),
|
||||||
|
queryFn: () => client.fetch.fetchPackageEvents(repository, packageBase, 30),
|
||||||
|
enabled: !!packageBase,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows: EventRow[] = events.map((event, index) => ({
|
||||||
|
id: index,
|
||||||
|
timestamp: new Date(event.created * 1000).toISOStringShort(),
|
||||||
|
event: event.event,
|
||||||
|
message: event.message ?? "",
|
||||||
|
}));
|
||||||
|
|
||||||
|
return <Box sx={{ mt: 1 }}>
|
||||||
|
<EventDurationLineChart events={events} />
|
||||||
|
<DataGrid
|
||||||
|
rows={rows}
|
||||||
|
columns={columns}
|
||||||
|
density="compact"
|
||||||
|
initialState={{
|
||||||
|
sorting: { sortModel: [{ field: "timestamp", sort: "desc" }] },
|
||||||
|
}}
|
||||||
|
pageSizeOptions={[10, 25]}
|
||||||
|
sx={{ height: 400, mt: 1 }}
|
||||||
|
disableRowSelectionOnClick
|
||||||
|
/>
|
||||||
|
</Box>;
|
||||||
|
}
|
||||||
115
frontend/src/components/package/PackageDetailsGrid.tsx
Normal file
115
frontend/src/components/package/PackageDetailsGrid.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Grid, Link, Typography } from "@mui/material";
|
||||||
|
import StringList from "components/common/StringList";
|
||||||
|
import type { Dependencies } from "models/Dependencies";
|
||||||
|
import type { Package } from "models/Package";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface PackageDetailsGridProps {
|
||||||
|
pkg: Package;
|
||||||
|
dependencies?: Dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PackageDetailsGrid({ pkg, dependencies }: PackageDetailsGridProps): React.JSX.Element {
|
||||||
|
const packagesList = Object.entries(pkg.packages)
|
||||||
|
.map(([name, properties]) => `${name}${properties.description ? ` (${properties.description})` : ""}`);
|
||||||
|
|
||||||
|
const groups = Object.values(pkg.packages)
|
||||||
|
.flatMap(properties => properties.groups ?? []);
|
||||||
|
|
||||||
|
const licenses = Object.values(pkg.packages)
|
||||||
|
.flatMap(properties => properties.licenses ?? []);
|
||||||
|
|
||||||
|
const upstreamUrls = Object.values(pkg.packages)
|
||||||
|
.map(properties => properties.url)
|
||||||
|
.filter((url): url is string => !!url)
|
||||||
|
.unique();
|
||||||
|
|
||||||
|
const aurUrl = pkg.remote.web_url;
|
||||||
|
|
||||||
|
const pkgNames = Object.keys(pkg.packages);
|
||||||
|
const pkgValues = Object.values(pkg.packages);
|
||||||
|
const deps = pkgValues
|
||||||
|
.flatMap(properties => (properties.depends ?? []).filter(dep => !pkgNames.includes(dep)))
|
||||||
|
.unique();
|
||||||
|
const makeDeps = pkgValues
|
||||||
|
.flatMap(properties => (properties.make_depends ?? []).filter(dep => !pkgNames.includes(dep)))
|
||||||
|
.map(dep => `${dep} (make)`)
|
||||||
|
.unique();
|
||||||
|
const optDeps = pkgValues
|
||||||
|
.flatMap(properties => (properties.opt_depends ?? []).filter(dep => !pkgNames.includes(dep)))
|
||||||
|
.map(dep => `${dep} (optional)`)
|
||||||
|
.unique();
|
||||||
|
const allDepends = [...deps, ...makeDeps, ...optDeps];
|
||||||
|
|
||||||
|
const implicitDepends = dependencies
|
||||||
|
? Object.values(dependencies.paths).flat()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Grid container spacing={1} sx={{ mt: 1 }}>
|
||||||
|
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">packages</Typography></Grid>
|
||||||
|
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}><StringList items={packagesList.unique()} /></Typography></Grid>
|
||||||
|
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">version</Typography></Grid>
|
||||||
|
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2">{pkg.version}</Typography></Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<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={{ xs: 8, md: 5 }}><Typography variant="body2">{pkg.packager ?? ""}</Typography></Grid>
|
||||||
|
<Grid size={{ xs: 4, md: 1 }} />
|
||||||
|
<Grid size={{ xs: 8, md: 5 }} />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid container spacing={1} sx={{ mt: 0.5 }}>
|
||||||
|
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">groups</Typography></Grid>
|
||||||
|
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}><StringList items={groups.unique()} /></Typography></Grid>
|
||||||
|
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">licenses</Typography></Grid>
|
||||||
|
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}><StringList items={licenses.unique()} /></Typography></Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid container spacing={1} sx={{ mt: 0.5 }}>
|
||||||
|
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">upstream</Typography></Grid>
|
||||||
|
<Grid size={{ xs: 8, md: 5 }}>
|
||||||
|
{upstreamUrls.map(url =>
|
||||||
|
<Link key={url} href={url} target="_blank" rel="noopener" underline="hover" display="block" variant="body2">
|
||||||
|
{url}
|
||||||
|
</Link>,
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">AUR</Typography></Grid>
|
||||||
|
<Grid size={{ xs: 8, md: 5 }}>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{aurUrl &&
|
||||||
|
<Link href={aurUrl} target="_blank" rel="noopener" underline="hover">AUR link</Link>
|
||||||
|
}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid container spacing={1} sx={{ mt: 0.5 }}>
|
||||||
|
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">depends</Typography></Grid>
|
||||||
|
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}><StringList items={allDepends} /></Typography></Grid>
|
||||||
|
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">implicitly depends</Typography></Grid>
|
||||||
|
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}><StringList items={implicitDepends.unique()} /></Typography></Grid>
|
||||||
|
</Grid>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
69
frontend/src/components/package/PackageInfoActions.tsx
Normal file
69
frontend/src/components/package/PackageInfoActions.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* 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 DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||||
|
import { Button, Checkbox, DialogActions, FormControlLabel } from "@mui/material";
|
||||||
|
import AutoRefreshControl from "components/common/AutoRefreshControl";
|
||||||
|
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
interface PackageInfoActionsProps {
|
||||||
|
isAuthorized: boolean;
|
||||||
|
refreshDatabase: boolean;
|
||||||
|
onRefreshDatabaseChange: (checked: boolean) => void;
|
||||||
|
onUpdate: () => void;
|
||||||
|
onRemove: () => void;
|
||||||
|
autoRefreshIntervals: AutoRefreshInterval[];
|
||||||
|
autoRefreshInterval: number;
|
||||||
|
onAutoRefreshIntervalChange: (interval: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PackageInfoActions({
|
||||||
|
isAuthorized,
|
||||||
|
refreshDatabase,
|
||||||
|
onRefreshDatabaseChange,
|
||||||
|
onUpdate,
|
||||||
|
onRemove,
|
||||||
|
autoRefreshIntervals,
|
||||||
|
autoRefreshInterval,
|
||||||
|
onAutoRefreshIntervalChange,
|
||||||
|
}: PackageInfoActionsProps): React.JSX.Element {
|
||||||
|
return <DialogActions sx={{ flexWrap: "wrap", gap: 1 }}>
|
||||||
|
{isAuthorized &&
|
||||||
|
<>
|
||||||
|
<FormControlLabel
|
||||||
|
control={<Checkbox checked={refreshDatabase} onChange={(_, checked) => onRefreshDatabaseChange(checked)} size="small" />}
|
||||||
|
label="update pacman databases"
|
||||||
|
/>
|
||||||
|
<Button onClick={onUpdate} variant="contained" color="success" startIcon={<PlayArrowIcon />} size="small">
|
||||||
|
update
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onRemove} variant="contained" color="error" startIcon={<DeleteIcon />} size="small">
|
||||||
|
remove
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
<AutoRefreshControl
|
||||||
|
intervals={autoRefreshIntervals}
|
||||||
|
currentInterval={autoRefreshInterval}
|
||||||
|
onIntervalChange={onAutoRefreshIntervalChange}
|
||||||
|
/>
|
||||||
|
</DialogActions>;
|
||||||
|
}
|
||||||
65
frontend/src/components/package/PackagePatchesList.tsx
Normal file
65
frontend/src/components/package/PackagePatchesList.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* 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 DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import { Box, IconButton, TextField, Typography } from "@mui/material";
|
||||||
|
import type { Patch } from "models/Patch";
|
||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
interface PackagePatchesListProps {
|
||||||
|
patches: Patch[];
|
||||||
|
editable: boolean;
|
||||||
|
onDelete: (key: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PackagePatchesList({
|
||||||
|
patches,
|
||||||
|
editable,
|
||||||
|
onDelete,
|
||||||
|
}: PackagePatchesListProps): React.JSX.Element | null {
|
||||||
|
if (patches.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Box sx={{ mt: 2 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>Environment variables</Typography>
|
||||||
|
{patches.map(patch =>
|
||||||
|
<Box key={patch.key} sx={{ display: "flex", alignItems: "center", gap: 1, mb: 0.5 }}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
value={patch.key}
|
||||||
|
disabled
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<Box>=</Box>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
value={JSON.stringify(patch.value)}
|
||||||
|
disabled
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
{editable &&
|
||||||
|
<IconButton size="small" color="error" onClick={() => onDelete(patch.key)}>
|
||||||
|
<DeleteIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
</Box>,
|
||||||
|
)}
|
||||||
|
</Box>;
|
||||||
|
}
|
||||||
197
frontend/src/components/table/PackageTable.tsx
Normal file
197
frontend/src/components/table/PackageTable.tsx
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Box, Link } from "@mui/material";
|
||||||
|
import {
|
||||||
|
DataGrid,
|
||||||
|
GRID_CHECKBOX_SELECTION_COL_DEF,
|
||||||
|
type GridColDef,
|
||||||
|
type GridFilterModel,
|
||||||
|
type GridRenderCellParams,
|
||||||
|
type GridRowId,
|
||||||
|
useGridApiRef,
|
||||||
|
} from "@mui/x-data-grid";
|
||||||
|
import StringList from "components/common/StringList";
|
||||||
|
import DashboardDialog from "components/dialogs/DashboardDialog";
|
||||||
|
import KeyImportDialog from "components/dialogs/KeyImportDialog";
|
||||||
|
import PackageAddDialog from "components/dialogs/PackageAddDialog";
|
||||||
|
import PackageInfoDialog from "components/dialogs/PackageInfoDialog";
|
||||||
|
import PackageRebuildDialog from "components/dialogs/PackageRebuildDialog";
|
||||||
|
import PackageTableToolbar from "components/table/PackageTableToolbar";
|
||||||
|
import StatusCell from "components/table/StatusCell";
|
||||||
|
import { useDebounce } from "hooks/useDebounce";
|
||||||
|
import { usePackageTable } from "hooks/usePackageTable";
|
||||||
|
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
||||||
|
import type { PackageRow } from "models/PackageRow";
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
|
||||||
|
interface PackageTableProps {
|
||||||
|
autoRefreshIntervals: AutoRefreshInterval[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
|
||||||
|
|
||||||
|
function createListColumn(
|
||||||
|
field: keyof PackageRow,
|
||||||
|
headerName: string,
|
||||||
|
options: { flex?: number; minWidth?: number; width?: number },
|
||||||
|
): GridColDef<PackageRow> {
|
||||||
|
return {
|
||||||
|
field,
|
||||||
|
headerName,
|
||||||
|
...options,
|
||||||
|
valueGetter: (value: string[]) => (value ?? []).join(" "),
|
||||||
|
renderCell: (params: GridRenderCellParams<PackageRow>) =>
|
||||||
|
<Box sx={{ whiteSpace: "pre-line" }}><StringList items={(params.row[field] as string[]) ?? []} /></Box>,
|
||||||
|
sortComparator: (left: string, right: string) => left.localeCompare(right),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PackageTable({ autoRefreshIntervals }: PackageTableProps): React.JSX.Element {
|
||||||
|
const table = usePackageTable(autoRefreshIntervals);
|
||||||
|
const apiRef = useGridApiRef();
|
||||||
|
const debouncedSearch = useDebounce(table.searchText, 300);
|
||||||
|
|
||||||
|
const effectiveFilterModel: GridFilterModel = useMemo(
|
||||||
|
() => ({
|
||||||
|
...table.filterModel,
|
||||||
|
quickFilterValues: debouncedSearch ? debouncedSearch.split(/\s+/) : undefined,
|
||||||
|
}),
|
||||||
|
[table.filterModel, debouncedSearch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns: GridColDef<PackageRow>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
field: "base",
|
||||||
|
headerName: "package base",
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 150,
|
||||||
|
renderCell: (params: GridRenderCellParams<PackageRow>) =>
|
||||||
|
params.row.webUrl ?
|
||||||
|
<Link href={params.row.webUrl} target="_blank" rel="noopener" underline="hover">
|
||||||
|
{params.value as string}
|
||||||
|
</Link>
|
||||||
|
: params.value as string,
|
||||||
|
},
|
||||||
|
{ field: "version", headerName: "version", width: 180, align: "right", headerAlign: "right" },
|
||||||
|
createListColumn("packages", "packages", { flex: 1, minWidth: 120 }),
|
||||||
|
createListColumn("groups", "groups", { width: 150 }),
|
||||||
|
createListColumn("licenses", "licenses", { width: 150 }),
|
||||||
|
{ field: "packager", headerName: "packager", width: 150 },
|
||||||
|
{
|
||||||
|
field: "timestamp",
|
||||||
|
headerName: "last update",
|
||||||
|
width: 180,
|
||||||
|
align: "right",
|
||||||
|
headerAlign: "right",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "status",
|
||||||
|
headerName: "status",
|
||||||
|
width: 120,
|
||||||
|
align: "center",
|
||||||
|
headerAlign: "center",
|
||||||
|
renderCell: (params: GridRenderCellParams<PackageRow>) => <StatusCell status={params.row.status} />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return <Box>
|
||||||
|
<PackageTableToolbar
|
||||||
|
hasSelection={table.selectionModel.length > 0}
|
||||||
|
isAuthorized={table.isAuthorized}
|
||||||
|
status={table.status}
|
||||||
|
searchText={table.searchText}
|
||||||
|
onSearchChange={table.setSearchText}
|
||||||
|
autoRefresh={{
|
||||||
|
autoRefreshIntervals,
|
||||||
|
currentInterval: table.autoRefreshInterval,
|
||||||
|
onIntervalChange: table.onAutoRefreshIntervalChange,
|
||||||
|
}}
|
||||||
|
actions={{
|
||||||
|
onDashboardClick: () => table.setDialogOpen("dashboard"),
|
||||||
|
onAddClick: () => table.setDialogOpen("add"),
|
||||||
|
onUpdateClick: () => void table.handleUpdate(),
|
||||||
|
onRefreshDatabaseClick: () => void table.handleRefreshDatabase(),
|
||||||
|
onRebuildClick: () => table.setDialogOpen("rebuild"),
|
||||||
|
onRemoveClick: () => void table.handleRemove(),
|
||||||
|
onKeyImportClick: () => table.setDialogOpen("keyImport"),
|
||||||
|
onReloadClick: table.handleReload,
|
||||||
|
onExportClick: () => apiRef.current?.exportDataAsCsv(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DataGrid
|
||||||
|
apiRef={apiRef}
|
||||||
|
rows={table.rows}
|
||||||
|
columns={columns}
|
||||||
|
loading={table.isLoading}
|
||||||
|
getRowHeight={() => "auto"}
|
||||||
|
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}
|
||||||
|
onColumnVisibilityModelChange={table.setColumnVisibility}
|
||||||
|
filterModel={effectiveFilterModel}
|
||||||
|
onFilterModelChange={table.setFilterModel}
|
||||||
|
initialState={{
|
||||||
|
sorting: { sortModel: [{ field: "base", sort: "asc" }] },
|
||||||
|
}}
|
||||||
|
onCellClick={(params, event) => {
|
||||||
|
// Don't open info dialog when clicking checkbox or link
|
||||||
|
if (params.field === GRID_CHECKBOX_SELECTION_COL_DEF.field) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((event.target as HTMLElement).closest("a")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
table.setSelectedPackage(String(params.id));
|
||||||
|
}}
|
||||||
|
autoHeight
|
||||||
|
sx={{
|
||||||
|
"& .MuiDataGrid-row": { cursor: "pointer" },
|
||||||
|
}}
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DashboardDialog open={table.dialogOpen === "dashboard"} onClose={() => table.setDialogOpen(null)} />
|
||||||
|
<PackageAddDialog open={table.dialogOpen === "add"} onClose={() => table.setDialogOpen(null)} />
|
||||||
|
<PackageRebuildDialog open={table.dialogOpen === "rebuild"} onClose={() => table.setDialogOpen(null)} />
|
||||||
|
<KeyImportDialog open={table.dialogOpen === "keyImport"} onClose={() => table.setDialogOpen(null)} />
|
||||||
|
<PackageInfoDialog
|
||||||
|
packageBase={table.selectedPackage}
|
||||||
|
open={table.selectedPackage !== null}
|
||||||
|
onClose={() => table.setSelectedPackage(null)}
|
||||||
|
autoRefreshIntervals={autoRefreshIntervals}
|
||||||
|
/>
|
||||||
|
</Box>;
|
||||||
|
}
|
||||||
185
frontend/src/components/table/PackageTableToolbar.tsx
Normal file
185
frontend/src/components/table/PackageTableToolbar.tsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
/*
|
||||||
|
* 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 AddIcon from "@mui/icons-material/Add";
|
||||||
|
import ClearIcon from "@mui/icons-material/Clear";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
|
import FileDownloadIcon from "@mui/icons-material/FileDownload";
|
||||||
|
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||||
|
import InventoryIcon from "@mui/icons-material/Inventory";
|
||||||
|
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||||
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
|
import ReplayIcon from "@mui/icons-material/Replay";
|
||||||
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
|
import VpnKeyIcon from "@mui/icons-material/VpnKey";
|
||||||
|
import { Box, Button, Divider, IconButton, InputAdornment, Menu, MenuItem, TextField, Tooltip } from "@mui/material";
|
||||||
|
import AutoRefreshControl from "components/common/AutoRefreshControl";
|
||||||
|
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
||||||
|
import type { BuildStatus } from "models/BuildStatus";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { StatusColors } from "theme/StatusColors";
|
||||||
|
|
||||||
|
export interface AutoRefreshProps {
|
||||||
|
autoRefreshIntervals: AutoRefreshInterval[];
|
||||||
|
currentInterval: number;
|
||||||
|
onIntervalChange: (interval: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolbarActions {
|
||||||
|
onDashboardClick: () => void;
|
||||||
|
onAddClick: () => void;
|
||||||
|
onUpdateClick: () => void;
|
||||||
|
onRefreshDatabaseClick: () => void;
|
||||||
|
onRebuildClick: () => void;
|
||||||
|
onRemoveClick: () => void;
|
||||||
|
onKeyImportClick: () => void;
|
||||||
|
onReloadClick: () => void;
|
||||||
|
onExportClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PackageTableToolbarProps {
|
||||||
|
hasSelection: boolean;
|
||||||
|
isAuthorized: boolean;
|
||||||
|
status?: BuildStatus;
|
||||||
|
searchText: string;
|
||||||
|
onSearchChange: (text: string) => void;
|
||||||
|
autoRefresh: AutoRefreshProps;
|
||||||
|
actions: ToolbarActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PackageTableToolbar({
|
||||||
|
hasSelection,
|
||||||
|
isAuthorized,
|
||||||
|
status,
|
||||||
|
searchText,
|
||||||
|
onSearchChange,
|
||||||
|
autoRefresh,
|
||||||
|
actions,
|
||||||
|
}: PackageTableToolbarProps): React.JSX.Element {
|
||||||
|
const [packagesAnchorEl, setPackagesAnchorEl] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
return <Box sx={{ display: "flex", gap: 1, mb: 1, flexWrap: "wrap", alignItems: "center" }}>
|
||||||
|
<Tooltip title="System health">
|
||||||
|
<IconButton
|
||||||
|
aria-label="System health"
|
||||||
|
onClick={actions.onDashboardClick}
|
||||||
|
sx={{
|
||||||
|
borderColor: status ? StatusColors[status] : undefined,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: "solid",
|
||||||
|
color: status ? StatusColors[status] : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InfoOutlinedIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{isAuthorized &&
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<InventoryIcon />}
|
||||||
|
onClick={event => setPackagesAnchorEl(event.currentTarget)}
|
||||||
|
>
|
||||||
|
packages
|
||||||
|
</Button>
|
||||||
|
<Menu
|
||||||
|
anchorEl={packagesAnchorEl}
|
||||||
|
open={Boolean(packagesAnchorEl)}
|
||||||
|
onClose={() => setPackagesAnchorEl(null)}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
setPackagesAnchorEl(null); actions.onAddClick();
|
||||||
|
}}>
|
||||||
|
<AddIcon fontSize="small" sx={{ mr: 1 }} /> add
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
setPackagesAnchorEl(null); actions.onUpdateClick();
|
||||||
|
}}>
|
||||||
|
<PlayArrowIcon fontSize="small" sx={{ mr: 1 }} /> update
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
setPackagesAnchorEl(null); actions.onRefreshDatabaseClick();
|
||||||
|
}}>
|
||||||
|
<DownloadIcon fontSize="small" sx={{ mr: 1 }} /> update pacman databases
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
setPackagesAnchorEl(null); actions.onRebuildClick();
|
||||||
|
}}>
|
||||||
|
<ReplayIcon fontSize="small" sx={{ mr: 1 }} /> rebuild
|
||||||
|
</MenuItem>
|
||||||
|
<Divider />
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
setPackagesAnchorEl(null); actions.onRemoveClick();
|
||||||
|
}} disabled={!hasSelection}>
|
||||||
|
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> remove
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<Button variant="contained" color="info" startIcon={<VpnKeyIcon />} onClick={actions.onKeyImportClick}>
|
||||||
|
import key
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Button variant="outlined" color="secondary" startIcon={<RefreshIcon />} onClick={actions.onReloadClick}>
|
||||||
|
reload
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<AutoRefreshControl
|
||||||
|
intervals={autoRefresh.autoRefreshIntervals}
|
||||||
|
currentInterval={autoRefresh.currentInterval}
|
||||||
|
onIntervalChange={autoRefresh.onIntervalChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
aria-label="Search packages"
|
||||||
|
placeholder="search packages..."
|
||||||
|
value={searchText}
|
||||||
|
onChange={event => onSearchChange(event.target.value)}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
startAdornment:
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<SearchIcon fontSize="small" />
|
||||||
|
</InputAdornment>
|
||||||
|
,
|
||||||
|
endAdornment: searchText ?
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton size="small" aria-label="Clear search" onClick={() => onSearchChange("")}>
|
||||||
|
<ClearIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{ minWidth: 200 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tooltip title="Export CSV">
|
||||||
|
<IconButton size="small" aria-label="Export CSV" onClick={actions.onExportClick}>
|
||||||
|
<FileDownloadIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>;
|
||||||
|
}
|
||||||
39
frontend/src/components/table/StatusCell.tsx
Normal file
39
frontend/src/components/table/StatusCell.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Chip } from "@mui/material";
|
||||||
|
import type { BuildStatus } from "models/BuildStatus";
|
||||||
|
import type React from "react";
|
||||||
|
import { StatusColors } from "theme/StatusColors";
|
||||||
|
|
||||||
|
interface StatusCellProps {
|
||||||
|
status: BuildStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StatusCell({ status }: StatusCellProps): React.JSX.Element {
|
||||||
|
return <Chip
|
||||||
|
label={status}
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: StatusColors[status],
|
||||||
|
color: "common.white",
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
34
frontend/src/contexts/AuthContext.ts
Normal file
34
frontend/src/contexts/AuthContext.ts
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 { createContext } from "react";
|
||||||
|
|
||||||
|
interface AuthState {
|
||||||
|
enabled: boolean;
|
||||||
|
username: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthContextValue extends AuthState {
|
||||||
|
isAuthorized: boolean;
|
||||||
|
setAuthState: (state: AuthState) => void;
|
||||||
|
login: (username: string, password: string) => Promise<void>;
|
||||||
|
logout: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthContext = createContext<AuthContextValue | null>(null);
|
||||||
49
frontend/src/contexts/AuthProvider.tsx
Normal file
49
frontend/src/contexts/AuthProvider.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* 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 { AuthContext } from "contexts/AuthContext";
|
||||||
|
import { useClient } from "hooks/useClient";
|
||||||
|
import React, { type ReactNode, useCallback, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: { children: ReactNode }): React.JSX.Element {
|
||||||
|
const client = useClient();
|
||||||
|
const [state, setState] = useState({ enabled: true, username: null as string | null });
|
||||||
|
|
||||||
|
const login = useCallback(async (username: string, password: string) => {
|
||||||
|
await client.login({ username, password });
|
||||||
|
setState(prev => ({ ...prev, username }));
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
|
const doLogout = useCallback(async () => {
|
||||||
|
await client.logout();
|
||||||
|
setState(prev => ({ ...prev, username: null }));
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
|
const isAuthorized = useMemo(() => !state.enabled || state.username !== null, [state.enabled, state.username]);
|
||||||
|
|
||||||
|
const value = useMemo(() => ({
|
||||||
|
...state, isAuthorized, setAuthState: setState, login, logout: doLogout,
|
||||||
|
}), [state, isAuthorized, login, doLogout]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
frontend/src/contexts/ClientContext.ts
Normal file
23
frontend/src/contexts/ClientContext.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/>.
|
||||||
|
*/
|
||||||
|
import type { AhrimanClient } from "api/client/AhrimanClient";
|
||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
export const ClientContext = createContext<AhrimanClient | null>(null);
|
||||||
32
frontend/src/contexts/ClientProvider.tsx
Normal file
32
frontend/src/contexts/ClientProvider.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* 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 { AhrimanClient } from "api/client/AhrimanClient";
|
||||||
|
import { ClientContext } from "contexts/ClientContext";
|
||||||
|
import React, { type ReactNode, useMemo } from "react";
|
||||||
|
|
||||||
|
export function ClientProvider({ children }: { children: ReactNode }): React.JSX.Element {
|
||||||
|
const client = useMemo(() => new AhrimanClient(), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClientContext.Provider value={client}>
|
||||||
|
{children}
|
||||||
|
</ClientContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
frontend/src/contexts/Notification.ts
Normal file
27
frontend/src/contexts/Notification.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 type { AlertColor } from "@mui/material";
|
||||||
|
|
||||||
|
export interface Notification {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
severity: AlertColor;
|
||||||
|
}
|
||||||
27
frontend/src/contexts/NotificationContext.ts
Normal file
27
frontend/src/contexts/NotificationContext.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 NotificationContextValue {
|
||||||
|
showSuccess: (title: string, message: string) => void;
|
||||||
|
showError: (title: string, message: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotificationContext = createContext<NotificationContextValue | null>(null);
|
||||||
50
frontend/src/contexts/NotificationItem.tsx
Normal file
50
frontend/src/contexts/NotificationItem.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Alert, Slide } from "@mui/material";
|
||||||
|
import type { Notification } from "contexts/Notification";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface NotificationItemProps {
|
||||||
|
notification: Notification;
|
||||||
|
onClose: (id: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NotificationItem({ notification, onClose }: NotificationItemProps): React.JSX.Element {
|
||||||
|
const [show, setShow] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => setShow(false), 5000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slide direction="down" in={show} mountOnEnter unmountOnExit onExited={() => onClose(notification.id)}>
|
||||||
|
<Alert
|
||||||
|
onClose={() => setShow(false)}
|
||||||
|
severity={notification.severity}
|
||||||
|
variant="filled"
|
||||||
|
sx={{ width: "100%", pointerEvents: "auto" }}
|
||||||
|
>
|
||||||
|
<strong>{notification.title}</strong>
|
||||||
|
{notification.message && ` - ${notification.message}`}
|
||||||
|
</Alert>
|
||||||
|
</Slide>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
frontend/src/contexts/NotificationProvider.tsx
Normal file
74
frontend/src/contexts/NotificationProvider.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* 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 { type AlertColor, Box } from "@mui/material";
|
||||||
|
import type { Notification } from "contexts/Notification";
|
||||||
|
import { NotificationContext } from "contexts/NotificationContext";
|
||||||
|
import NotificationItem from "contexts/NotificationItem";
|
||||||
|
import React, { type ReactNode, useCallback, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
|
export function NotificationProvider({ children }: { children: ReactNode }): React.JSX.Element {
|
||||||
|
const nextId = useRef(0);
|
||||||
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||||
|
|
||||||
|
const addNotification = useCallback((title: string, message: string, severity: AlertColor) => {
|
||||||
|
const id = nextId.current++;
|
||||||
|
setNotifications(prev => [...prev, { id, title, message, severity }]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeNotification = useCallback((id: number) => {
|
||||||
|
setNotifications(prev => prev.filter(notification => notification.id !== id));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showSuccess = useCallback(
|
||||||
|
(title: string, message: string) => addNotification(title, message, "success"),
|
||||||
|
[addNotification],
|
||||||
|
);
|
||||||
|
const showError = useCallback(
|
||||||
|
(title: string, message: string) => addNotification(title, message, "error"),
|
||||||
|
[addNotification],
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = useMemo(() => ({ showSuccess, showError }), [showSuccess, showError]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 16,
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
zIndex: theme => theme.zIndex.snackbar,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 1,
|
||||||
|
maxWidth: 500,
|
||||||
|
width: "100%",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{notifications.map(notification =>
|
||||||
|
<NotificationItem key={notification.id} notification={notification} onClose={removeNotification} />,
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</NotificationContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
frontend/src/contexts/RepositoryContext.ts
Normal file
30
frontend/src/contexts/RepositoryContext.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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 type { RepositoryId } from "models/RepositoryId";
|
||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
export interface RepositoryContextValue {
|
||||||
|
repositories: RepositoryId[];
|
||||||
|
current: RepositoryId | null;
|
||||||
|
setRepositories: (repositories: RepositoryId[]) => void;
|
||||||
|
setCurrent: (repository: RepositoryId) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RepositoryContext = createContext<RepositoryContextValue | null>(null);
|
||||||
53
frontend/src/contexts/RepositoryProvider.tsx
Normal file
53
frontend/src/contexts/RepositoryProvider.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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 { RepositoryContext } from "contexts/RepositoryContext";
|
||||||
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
import React, { type ReactNode, useCallback, useMemo, useState, useSyncExternalStore } from "react";
|
||||||
|
|
||||||
|
function subscribeToHash(callback: () => void): () => void {
|
||||||
|
window.addEventListener("hashchange", callback);
|
||||||
|
return () => window.removeEventListener("hashchange", callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHashSnapshot(): string {
|
||||||
|
return window.location.hash.replace("#", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RepositoryProvider({ children }: { children: ReactNode }): React.JSX.Element {
|
||||||
|
const [repositories, setRepositories] = useState<RepositoryId[]>([]);
|
||||||
|
const hash = useSyncExternalStore(subscribeToHash, getHashSnapshot);
|
||||||
|
|
||||||
|
const current = useMemo(() => {
|
||||||
|
if (repositories.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return repositories.find(repository => repository.key === hash) ?? repositories[0] ?? null;
|
||||||
|
}, [repositories, hash]);
|
||||||
|
|
||||||
|
const setCurrent = useCallback((repository: RepositoryId) => {
|
||||||
|
window.location.hash = repository.key;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value = useMemo(() => ({
|
||||||
|
repositories, current, setRepositories, setCurrent,
|
||||||
|
}), [repositories, current, setCurrent]);
|
||||||
|
|
||||||
|
return <RepositoryContext.Provider value={value}>{children}</RepositoryContext.Provider>;
|
||||||
|
}
|
||||||
46
frontend/src/hooks/QueryKeys.ts
Normal file
46
frontend/src/hooks/QueryKeys.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* 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 type { RepositoryId } from "models/RepositoryId";
|
||||||
|
|
||||||
|
export const QueryKeys = {
|
||||||
|
|
||||||
|
changes: (packageBase: string, repository: RepositoryId) => ["changes", repository.key, packageBase] as const,
|
||||||
|
|
||||||
|
dependencies: (packageBase: string, repository: RepositoryId) => ["dependencies", repository.key, packageBase] as const,
|
||||||
|
|
||||||
|
events: (repository: RepositoryId, objectId?: string) => ["events", repository.key, objectId] as const,
|
||||||
|
|
||||||
|
info: ["info"] as const,
|
||||||
|
|
||||||
|
logs: (packageBase: string, repository: RepositoryId) => ["logs", repository.key, packageBase] as const,
|
||||||
|
|
||||||
|
logsVersion: (packageBase: string, repository: RepositoryId, version: string, processId: string) =>
|
||||||
|
["logs", repository.key, packageBase, version, processId] as const,
|
||||||
|
|
||||||
|
package: (packageBase: string, repository: RepositoryId) => ["packages", repository.key, packageBase] as const,
|
||||||
|
|
||||||
|
packages: (repository: RepositoryId) => ["packages", repository.key] as const,
|
||||||
|
|
||||||
|
patches: (packageBase: string) => ["patches", packageBase] as const,
|
||||||
|
|
||||||
|
search: (query: string) => ["search", query] as const,
|
||||||
|
|
||||||
|
status: (repository: RepositoryId) => ["status", repository.key] as const,
|
||||||
|
};
|
||||||
25
frontend/src/hooks/useAuth.ts
Normal file
25
frontend/src/hooks/useAuth.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 { AuthContext, type AuthContextValue } from "contexts/AuthContext";
|
||||||
|
import { useContextNotNull } from "hooks/useContextNotNull";
|
||||||
|
|
||||||
|
export function useAuth(): AuthContextValue {
|
||||||
|
return useContextNotNull(AuthContext);
|
||||||
|
}
|
||||||
49
frontend/src/hooks/useAutoRefresh.ts
Normal file
49
frontend/src/hooks/useAutoRefresh.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useLocalStorage } from "hooks/useLocalStorage";
|
||||||
|
import { type Dispatch, type SetStateAction, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface AutoRefreshResult {
|
||||||
|
interval: number;
|
||||||
|
setInterval: Dispatch<SetStateAction<number>>;
|
||||||
|
setPaused: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAutoRefresh(key: string, defaultInterval: number): AutoRefreshResult {
|
||||||
|
const storageKey = `ahriman-${key}`;
|
||||||
|
const [interval, setInterval] = useLocalStorage<number>(storageKey, defaultInterval);
|
||||||
|
const [paused, setPaused] = useState(false);
|
||||||
|
|
||||||
|
// Apply defaultInterval when it becomes available (e.g. after info endpoint loads)
|
||||||
|
// but only if the user hasn't explicitly set a preference
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultInterval > 0 && window.localStorage.getItem(storageKey) === null) {
|
||||||
|
setInterval(defaultInterval);
|
||||||
|
}
|
||||||
|
}, [storageKey, defaultInterval, setInterval]);
|
||||||
|
|
||||||
|
const effectiveInterval = paused ? 0 : interval;
|
||||||
|
|
||||||
|
return {
|
||||||
|
interval: effectiveInterval,
|
||||||
|
setInterval,
|
||||||
|
setPaused,
|
||||||
|
};
|
||||||
|
}
|
||||||
63
frontend/src/hooks/useAutoScroll.ts
Normal file
63
frontend/src/hooks/useAutoScroll.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* 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 { type RefObject, useRef } from "react";
|
||||||
|
|
||||||
|
interface UseAutoScrollResult {
|
||||||
|
preRef: RefObject<HTMLElement | null>;
|
||||||
|
handleScroll: () => void;
|
||||||
|
scrollToBottom: () => void;
|
||||||
|
resetScroll: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAutoScroll(): UseAutoScrollResult {
|
||||||
|
const preRef = useRef<HTMLElement>(null);
|
||||||
|
const initialScrollDone = useRef(false);
|
||||||
|
const wasAtBottom = useRef(true);
|
||||||
|
|
||||||
|
const handleScroll: () => void = () => {
|
||||||
|
if (preRef.current) {
|
||||||
|
const element = preRef.current;
|
||||||
|
wasAtBottom.current = element.scrollTop + element.clientHeight >= element.scrollHeight - 50;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetScroll: () => void = () => {
|
||||||
|
initialScrollDone.current = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// scroll to bottom on initial load, then only if already near bottom and no active text selection
|
||||||
|
const scrollToBottom: () => void = () => {
|
||||||
|
if (!preRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const element = preRef.current;
|
||||||
|
if (!initialScrollDone.current) {
|
||||||
|
element.scrollTop = element.scrollHeight;
|
||||||
|
initialScrollDone.current = true;
|
||||||
|
} else {
|
||||||
|
const hasSelection = !document.getSelection()?.isCollapsed;
|
||||||
|
if (wasAtBottom.current && !hasSelection) {
|
||||||
|
element.scrollTop = element.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { preRef, handleScroll, scrollToBottom, resetScroll };
|
||||||
|
}
|
||||||
26
frontend/src/hooks/useClient.ts
Normal file
26
frontend/src/hooks/useClient.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* 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 type { AhrimanClient } from "api/client/AhrimanClient";
|
||||||
|
import { ClientContext } from "contexts/ClientContext";
|
||||||
|
import { useContextNotNull } from "hooks/useContextNotNull";
|
||||||
|
|
||||||
|
export function useClient(): AhrimanClient {
|
||||||
|
return useContextNotNull(ClientContext);
|
||||||
|
}
|
||||||
28
frontend/src/hooks/useContextNotNull.ts
Normal file
28
frontend/src/hooks/useContextNotNull.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/>.
|
||||||
|
*/
|
||||||
|
import { type Context, useContext } from "react";
|
||||||
|
|
||||||
|
export function useContextNotNull<T>(context: Context<T | null>): T {
|
||||||
|
const ctx = useContext(context);
|
||||||
|
if (ctx === null) {
|
||||||
|
throw new Error("must be used within a Provider");
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
31
frontend/src/hooks/useDebounce.ts
Normal file
31
frontend/src/hooks/useDebounce.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function useDebounce<T>(value: T, delay: number): T {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => setDebouncedValue(value), delay);
|
||||||
|
return () => clearTimeout(handler);
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
||||||
44
frontend/src/hooks/useLocalStorage.ts
Normal file
44
frontend/src/hooks/useLocalStorage.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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 { type Dispatch, type SetStateAction, useCallback, useState } from "react";
|
||||||
|
|
||||||
|
export function useLocalStorage<T>(key: string, initialValue: T): [T, Dispatch<SetStateAction<T>>] {
|
||||||
|
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||||
|
try {
|
||||||
|
const item = window.localStorage.getItem(key);
|
||||||
|
return item !== null ? (JSON.parse(item) as T) : initialValue;
|
||||||
|
} catch {
|
||||||
|
return initialValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const setValue: Dispatch<SetStateAction<T>> = useCallback(
|
||||||
|
value => {
|
||||||
|
setStoredValue(prev => {
|
||||||
|
const nextValue = value instanceof Function ? value(prev) : value;
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(nextValue));
|
||||||
|
return nextValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[key],
|
||||||
|
);
|
||||||
|
|
||||||
|
return [storedValue, setValue];
|
||||||
|
}
|
||||||
25
frontend/src/hooks/useNotification.ts
Normal file
25
frontend/src/hooks/useNotification.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 { NotificationContext, type NotificationContextValue } from "contexts/NotificationContext";
|
||||||
|
import { useContextNotNull } from "hooks/useContextNotNull";
|
||||||
|
|
||||||
|
export function useNotification(): NotificationContextValue {
|
||||||
|
return useContextNotNull(NotificationContext);
|
||||||
|
}
|
||||||
108
frontend/src/hooks/usePackageActions.ts
Normal file
108
frontend/src/hooks/usePackageActions.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { ApiError } from "api/client/ApiError";
|
||||||
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
|
import { useClient } from "hooks/useClient";
|
||||||
|
import { useNotification } from "hooks/useNotification";
|
||||||
|
import { useRepository } from "hooks/useRepository";
|
||||||
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
|
||||||
|
export interface UsePackageActionsResult {
|
||||||
|
handleReload: () => void;
|
||||||
|
handleUpdate: () => Promise<void>;
|
||||||
|
handleRefreshDatabase: () => Promise<void>;
|
||||||
|
handleRemove: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePackageActions(
|
||||||
|
selectionModel: string[],
|
||||||
|
setSelectionModel: (model: string[]) => void,
|
||||||
|
): UsePackageActionsResult {
|
||||||
|
const client = useClient();
|
||||||
|
const { current } = useRepository();
|
||||||
|
const { showSuccess, showError } = useNotification();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const invalidate = (repository: RepositoryId): void => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: QueryKeys.packages(repository) });
|
||||||
|
void queryClient.invalidateQueries({ queryKey: QueryKeys.status(repository) });
|
||||||
|
};
|
||||||
|
|
||||||
|
const performAction = async (
|
||||||
|
action: (repository: RepositoryId) => Promise<string>,
|
||||||
|
errorMessage: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
if (!current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const successMessage = await action(current);
|
||||||
|
showSuccess("Success", successMessage);
|
||||||
|
invalidate(current);
|
||||||
|
setSelectionModel([]);
|
||||||
|
} catch (exception) {
|
||||||
|
showError("Action failed", `${errorMessage}: ${ApiError.errorDetail(exception)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReload: () => void = () => {
|
||||||
|
if (current !== null) {
|
||||||
|
invalidate(current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = (): Promise<void> => performAction(async (repository): Promise<string> => {
|
||||||
|
if (selectionModel.length === 0) {
|
||||||
|
await client.service.servicePackageUpdate(repository, { packages: [] });
|
||||||
|
return "Repository update has been run";
|
||||||
|
}
|
||||||
|
await client.service.servicePackageAdd(repository, { packages: selectionModel });
|
||||||
|
return `Run update for packages ${selectionModel.join(", ")}`;
|
||||||
|
}, "Packages update failed");
|
||||||
|
|
||||||
|
const handleRefreshDatabase = (): Promise<void> => performAction(async (repository): Promise<string> => {
|
||||||
|
await client.service.servicePackageUpdate(repository, {
|
||||||
|
packages: [],
|
||||||
|
refresh: true,
|
||||||
|
aur: false,
|
||||||
|
local: false,
|
||||||
|
manual: false,
|
||||||
|
});
|
||||||
|
return "Pacman database update has been requested";
|
||||||
|
}, "Could not update pacman databases");
|
||||||
|
|
||||||
|
const handleRemove = (): Promise<void> => {
|
||||||
|
if (selectionModel.length === 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return performAction(async (repository): Promise<string> => {
|
||||||
|
await client.service.servicePackageRemove(repository, selectionModel);
|
||||||
|
return `Packages ${selectionModel.join(", ")} have been removed`;
|
||||||
|
}, "Could not remove packages");
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleReload,
|
||||||
|
handleUpdate,
|
||||||
|
handleRefreshDatabase,
|
||||||
|
handleRemove,
|
||||||
|
};
|
||||||
|
}
|
||||||
68
frontend/src/hooks/usePackageData.ts
Normal file
68
frontend/src/hooks/usePackageData.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* 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 { skipToken, useQuery } from "@tanstack/react-query";
|
||||||
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
|
import { useAuth } from "hooks/useAuth";
|
||||||
|
import { useAutoRefresh } from "hooks/useAutoRefresh";
|
||||||
|
import { useClient } from "hooks/useClient";
|
||||||
|
import { useRepository } from "hooks/useRepository";
|
||||||
|
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
||||||
|
import type { BuildStatus } from "models/BuildStatus";
|
||||||
|
import { PackageRow } from "models/PackageRow";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { defaultInterval } from "utils";
|
||||||
|
|
||||||
|
export interface UsePackageDataResult {
|
||||||
|
rows: PackageRow[];
|
||||||
|
isLoading: boolean;
|
||||||
|
isAuthorized: boolean;
|
||||||
|
status: BuildStatus | undefined;
|
||||||
|
autoRefresh: ReturnType<typeof useAutoRefresh>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePackageData(autoRefreshIntervals: AutoRefreshInterval[]): UsePackageDataResult {
|
||||||
|
const client = useClient();
|
||||||
|
const { current } = useRepository();
|
||||||
|
const { isAuthorized } = useAuth();
|
||||||
|
|
||||||
|
const autoRefresh = useAutoRefresh("table-autoreload-button", defaultInterval(autoRefreshIntervals));
|
||||||
|
|
||||||
|
const { data: packages = [], isLoading } = useQuery({
|
||||||
|
queryKey: current ? QueryKeys.packages(current) : ["packages"],
|
||||||
|
queryFn: current ? () => client.fetch.fetchPackages(current) : skipToken,
|
||||||
|
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: status } = useQuery({
|
||||||
|
queryKey: current ? QueryKeys.status(current) : ["status"],
|
||||||
|
queryFn: current ? () => client.fetch.fetchServerStatus(current) : skipToken,
|
||||||
|
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = useMemo(() => packages.map(descriptor => new PackageRow(descriptor)), [packages]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows,
|
||||||
|
isLoading,
|
||||||
|
isAuthorized,
|
||||||
|
status: status?.status.status,
|
||||||
|
autoRefresh,
|
||||||
|
};
|
||||||
|
}
|
||||||
86
frontend/src/hooks/usePackageTable.ts
Normal file
86
frontend/src/hooks/usePackageTable.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* 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 type { GridFilterModel } from "@mui/x-data-grid";
|
||||||
|
import { usePackageActions } from "hooks/usePackageActions";
|
||||||
|
import { usePackageData } from "hooks/usePackageData";
|
||||||
|
import { useTableState } from "hooks/useTableState";
|
||||||
|
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
||||||
|
import type { BuildStatus } from "models/BuildStatus";
|
||||||
|
import type { PackageRow } from "models/PackageRow";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
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;
|
||||||
|
onAutoRefreshIntervalChange: (interval: number) => void;
|
||||||
|
|
||||||
|
handleReload: () => void;
|
||||||
|
handleUpdate: () => Promise<void>;
|
||||||
|
handleRefreshDatabase: () => Promise<void>;
|
||||||
|
handleRemove: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePackageTable(autoRefreshIntervals: AutoRefreshInterval[]): UsePackageTableResult {
|
||||||
|
const { rows, isLoading, isAuthorized, status, autoRefresh } = usePackageData(autoRefreshIntervals);
|
||||||
|
const tableState = useTableState();
|
||||||
|
const actions = usePackageActions(tableState.selectionModel, tableState.setSelectionModel);
|
||||||
|
|
||||||
|
// Pause auto-refresh when dialog is open
|
||||||
|
const isDialogOpen = tableState.dialogOpen !== null || tableState.selectedPackage !== null;
|
||||||
|
const setPaused = autoRefresh.setPaused;
|
||||||
|
useEffect(() => {
|
||||||
|
setPaused(isDialogOpen);
|
||||||
|
}, [isDialogOpen, setPaused]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows,
|
||||||
|
isLoading,
|
||||||
|
isAuthorized,
|
||||||
|
status,
|
||||||
|
|
||||||
|
...tableState,
|
||||||
|
|
||||||
|
autoRefreshInterval: autoRefresh.interval,
|
||||||
|
onAutoRefreshIntervalChange: autoRefresh.setInterval,
|
||||||
|
|
||||||
|
...actions,
|
||||||
|
};
|
||||||
|
}
|
||||||
25
frontend/src/hooks/useRepository.ts
Normal file
25
frontend/src/hooks/useRepository.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 { RepositoryContext, type RepositoryContextValue } from "contexts/RepositoryContext";
|
||||||
|
import { useContextNotNull } from "hooks/useContextNotNull";
|
||||||
|
|
||||||
|
export function useRepository(): RepositoryContextValue {
|
||||||
|
return useContextNotNull(RepositoryContext);
|
||||||
|
}
|
||||||
48
frontend/src/hooks/useSelectedRepository.ts
Normal file
48
frontend/src/hooks/useSelectedRepository.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useRepository } from "hooks/useRepository";
|
||||||
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export interface SelectedRepositoryResult {
|
||||||
|
selectedKey: string;
|
||||||
|
setSelectedKey: (key: string) => void;
|
||||||
|
selectedRepository: RepositoryId | null;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSelectedRepository(): SelectedRepositoryResult {
|
||||||
|
const { repositories, current } = useRepository();
|
||||||
|
const [selectedKey, setSelectedKey] = useState("");
|
||||||
|
|
||||||
|
let selectedRepository: RepositoryId | null = current;
|
||||||
|
if (selectedKey) {
|
||||||
|
const repository = repositories.find(repository => repository.key === selectedKey);
|
||||||
|
if (repository) {
|
||||||
|
selectedRepository = repository;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reset: () => void = () => {
|
||||||
|
setSelectedKey("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return { selectedKey, setSelectedKey, selectedRepository, reset };
|
||||||
|
}
|
||||||
82
frontend/src/hooks/useTableState.ts
Normal file
82
frontend/src/hooks/useTableState.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* 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 type { GridFilterModel } from "@mui/x-data-grid";
|
||||||
|
import { useLocalStorage } from "hooks/useLocalStorage";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export type DialogType = "dashboard" | "add" | "rebuild" | "keyImport";
|
||||||
|
|
||||||
|
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>;
|
||||||
|
setColumnVisibility: (model: Record<string, boolean>) => void;
|
||||||
|
filterModel: GridFilterModel;
|
||||||
|
setFilterModel: (model: GridFilterModel) => void;
|
||||||
|
searchText: string;
|
||||||
|
setSearchText: (text: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTableState(): UseTableStateResult {
|
||||||
|
const [selectionModel, setSelectionModel] = useState<string[]>([]);
|
||||||
|
const [dialogOpen, setDialogOpen] = useState<DialogType | null>(null);
|
||||||
|
const [selectedPackage, setSelectedPackage] = useState<string | null>(null);
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
|
||||||
|
const [paginationModel, setPaginationModel] = useLocalStorage("ahriman-packages-pagination", {
|
||||||
|
pageSize: 10,
|
||||||
|
page: 0,
|
||||||
|
});
|
||||||
|
const [columnVisibility, setColumnVisibility] = useLocalStorage<Record<string, boolean>>(
|
||||||
|
"ahriman-packages-columns",
|
||||||
|
{ groups: false, licenses: false, packager: false },
|
||||||
|
);
|
||||||
|
const [filterModel, setFilterModel] = useLocalStorage<GridFilterModel>(
|
||||||
|
"ahriman-packages-filters",
|
||||||
|
{ items: [] },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectionModel,
|
||||||
|
setSelectionModel,
|
||||||
|
|
||||||
|
dialogOpen,
|
||||||
|
setDialogOpen,
|
||||||
|
selectedPackage,
|
||||||
|
setSelectedPackage,
|
||||||
|
|
||||||
|
paginationModel,
|
||||||
|
setPaginationModel,
|
||||||
|
columnVisibility,
|
||||||
|
setColumnVisibility,
|
||||||
|
filterModel,
|
||||||
|
setFilterModel,
|
||||||
|
searchText,
|
||||||
|
setSearchText,
|
||||||
|
};
|
||||||
|
}
|
||||||
31
frontend/src/main.tsx
Normal file
31
frontend/src/main.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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 "chartSetup";
|
||||||
|
import "utils";
|
||||||
|
|
||||||
|
import App from "App";
|
||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
23
frontend/src/models/AURPackage.ts
Normal file
23
frontend/src/models/AURPackage.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 AURPackage {
|
||||||
|
package: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
25
frontend/src/models/AuthInfo.ts
Normal file
25
frontend/src/models/AuthInfo.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/>.
|
||||||
|
*/
|
||||||
|
export interface AuthInfo {
|
||||||
|
control: string;
|
||||||
|
enabled: boolean;
|
||||||
|
external: boolean;
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
24
frontend/src/models/AutoRefreshInterval.ts
Normal file
24
frontend/src/models/AutoRefreshInterval.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* 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 AutoRefreshInterval {
|
||||||
|
interval: number;
|
||||||
|
is_active: boolean;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
20
frontend/src/models/BuildStatus.ts
Normal file
20
frontend/src/models/BuildStatus.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* 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 BuildStatus = "unknown" | "pending" | "building" | "failed" | "success";
|
||||||
23
frontend/src/models/Changes.ts
Normal file
23
frontend/src/models/Changes.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 Changes {
|
||||||
|
changes?: string;
|
||||||
|
last_commit_sha?: string;
|
||||||
|
}
|
||||||
27
frontend/src/models/Counters.ts
Normal file
27
frontend/src/models/Counters.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/>.
|
||||||
|
*/
|
||||||
|
export interface Counters {
|
||||||
|
building: number;
|
||||||
|
failed: number;
|
||||||
|
pending: number;
|
||||||
|
success: number;
|
||||||
|
total: number;
|
||||||
|
unknown: number;
|
||||||
|
}
|
||||||
22
frontend/src/models/Dependencies.ts
Normal file
22
frontend/src/models/Dependencies.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* 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 Dependencies {
|
||||||
|
paths: Record<string, string[]>;
|
||||||
|
}
|
||||||
26
frontend/src/models/Event.ts
Normal file
26
frontend/src/models/Event.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* 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 Event {
|
||||||
|
created: number;
|
||||||
|
data?: Record<string, unknown>;
|
||||||
|
event: string;
|
||||||
|
message?: string;
|
||||||
|
object_id: string;
|
||||||
|
}
|
||||||
31
frontend/src/models/InfoResponse.ts
Normal file
31
frontend/src/models/InfoResponse.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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 type { AuthInfo } from "models/AuthInfo";
|
||||||
|
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
||||||
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
|
||||||
|
export interface InfoResponse {
|
||||||
|
auth: AuthInfo;
|
||||||
|
repositories: RepositoryId[];
|
||||||
|
version: string;
|
||||||
|
autorefresh_intervals: AutoRefreshInterval[];
|
||||||
|
docs_enabled: boolean;
|
||||||
|
index_url?: string;
|
||||||
|
}
|
||||||
31
frontend/src/models/InternalStatus.ts
Normal file
31
frontend/src/models/InternalStatus.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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 type { Counters } from "models/Counters";
|
||||||
|
import type { RepositoryStats } from "models/RepositoryStats";
|
||||||
|
import type { Status } from "models/Status";
|
||||||
|
|
||||||
|
export interface InternalStatus {
|
||||||
|
architecture: string;
|
||||||
|
repository: string;
|
||||||
|
packages: Counters;
|
||||||
|
stats: RepositoryStats;
|
||||||
|
status: Status;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
25
frontend/src/models/LogRecord.ts
Normal file
25
frontend/src/models/LogRecord.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/>.
|
||||||
|
*/
|
||||||
|
export interface LogRecord {
|
||||||
|
created: number;
|
||||||
|
message: string;
|
||||||
|
process_id: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
23
frontend/src/models/LoginRequest.ts
Normal file
23
frontend/src/models/LoginRequest.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 LoginRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
22
frontend/src/models/PGPKey.ts
Normal file
22
frontend/src/models/PGPKey.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* 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 PGPKey {
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
23
frontend/src/models/PGPKeyRequest.ts
Normal file
23
frontend/src/models/PGPKeyRequest.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 PGPKeyRequest {
|
||||||
|
key: string;
|
||||||
|
server: string;
|
||||||
|
}
|
||||||
29
frontend/src/models/Package.ts
Normal file
29
frontend/src/models/Package.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* 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 type { PackageProperties } from "models/PackageProperties";
|
||||||
|
import type { Remote } from "models/Remote";
|
||||||
|
|
||||||
|
export interface Package {
|
||||||
|
base: string;
|
||||||
|
packager?: string;
|
||||||
|
packages: Record<string, PackageProperties>;
|
||||||
|
remote: Remote;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
29
frontend/src/models/PackageActionRequest.ts
Normal file
29
frontend/src/models/PackageActionRequest.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* 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 type { Patch } from "models/Patch";
|
||||||
|
|
||||||
|
export interface PackageActionRequest {
|
||||||
|
packages: string[];
|
||||||
|
patches?: Patch[];
|
||||||
|
refresh?: boolean;
|
||||||
|
aur?: boolean;
|
||||||
|
local?: boolean;
|
||||||
|
manual?: boolean;
|
||||||
|
}
|
||||||
35
frontend/src/models/PackageProperties.ts
Normal file
35
frontend/src/models/PackageProperties.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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 PackageProperties {
|
||||||
|
architecture?: string;
|
||||||
|
archive_size?: number;
|
||||||
|
build_date?: number;
|
||||||
|
check_depends?: string[];
|
||||||
|
depends?: string[];
|
||||||
|
description?: string;
|
||||||
|
filename?: string;
|
||||||
|
groups?: string[];
|
||||||
|
installed_size?: number;
|
||||||
|
licenses?: string[];
|
||||||
|
make_depends?: string[];
|
||||||
|
opt_depends?: string[];
|
||||||
|
provides?: string[];
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
58
frontend/src/models/PackageRow.ts
Normal file
58
frontend/src/models/PackageRow.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* 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 type { BuildStatus } from "models/BuildStatus";
|
||||||
|
import type { PackageStatus } from "models/PackageStatus";
|
||||||
|
|
||||||
|
export class PackageRow {
|
||||||
|
id: string;
|
||||||
|
base: string;
|
||||||
|
webUrl?: string;
|
||||||
|
version: string;
|
||||||
|
packages: string[];
|
||||||
|
groups: string[];
|
||||||
|
licenses: string[];
|
||||||
|
packager: string;
|
||||||
|
timestamp: string;
|
||||||
|
timestampValue: number;
|
||||||
|
status: BuildStatus;
|
||||||
|
|
||||||
|
constructor(descriptor: PackageStatus) {
|
||||||
|
this.id = 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.licenses = PackageRow.extractListProperties(descriptor.package, "licenses");
|
||||||
|
this.packager = descriptor.package.packager ?? "";
|
||||||
|
this.timestamp = new Date(descriptor.status.timestamp * 1000).toISOStringShort();
|
||||||
|
this.timestampValue = descriptor.status.timestamp;
|
||||||
|
this.status = descriptor.status.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static extractListProperties(pkg: PackageStatus["package"], property: "groups" | "licenses"): string[] {
|
||||||
|
return [
|
||||||
|
...new Set(
|
||||||
|
Object.values(pkg.packages)
|
||||||
|
.flatMap(properties => properties[property] ?? []),
|
||||||
|
),
|
||||||
|
].sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
20
frontend/src/models/PackageSource.ts
Normal file
20
frontend/src/models/PackageSource.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* 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 PackageSource = "auto" | "archive" | "aur" | "directory" | "local" | "remote" | "repository";
|
||||||
26
frontend/src/models/PackageStatus.ts
Normal file
26
frontend/src/models/PackageStatus.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* 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 type { Package } from "models/Package";
|
||||||
|
import type { Status } from "models/Status";
|
||||||
|
|
||||||
|
export interface PackageStatus {
|
||||||
|
package: Package;
|
||||||
|
status: Status;
|
||||||
|
}
|
||||||
23
frontend/src/models/Patch.ts
Normal file
23
frontend/src/models/Patch.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 Patch {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
28
frontend/src/models/Remote.ts
Normal file
28
frontend/src/models/Remote.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/>.
|
||||||
|
*/
|
||||||
|
import type { PackageSource } from "models/PackageSource";
|
||||||
|
|
||||||
|
export interface Remote {
|
||||||
|
branch?: string;
|
||||||
|
git_url?: string;
|
||||||
|
path?: string;
|
||||||
|
source: PackageSource;
|
||||||
|
web_url?: string;
|
||||||
|
}
|
||||||
40
frontend/src/models/RepositoryId.ts
Normal file
40
frontend/src/models/RepositoryId.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* 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 class RepositoryId {
|
||||||
|
readonly architecture: string;
|
||||||
|
readonly repository: string;
|
||||||
|
|
||||||
|
constructor(architecture: string, repository: string) {
|
||||||
|
this.architecture = architecture;
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
get key(): string {
|
||||||
|
return `${this.architecture}-${this.repository}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get label(): string {
|
||||||
|
return `${this.repository} (${this.architecture})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
toQuery(): Record<string, string> {
|
||||||
|
return { architecture: this.architecture, repository: this.repository };
|
||||||
|
}
|
||||||
|
}
|
||||||
25
frontend/src/models/RepositoryStats.ts
Normal file
25
frontend/src/models/RepositoryStats.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/>.
|
||||||
|
*/
|
||||||
|
export interface RepositoryStats {
|
||||||
|
archive_size?: number;
|
||||||
|
bases?: number;
|
||||||
|
installed_size?: number;
|
||||||
|
packages?: number;
|
||||||
|
}
|
||||||
25
frontend/src/models/Status.ts
Normal file
25
frontend/src/models/Status.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 type { BuildStatus } from "models/BuildStatus";
|
||||||
|
|
||||||
|
export interface Status {
|
||||||
|
status: BuildStatus;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user