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