chore(release): automatic release v1.23.0

This commit is contained in:
homarr-releases[bot]
2025-06-06 19:15:09 +00:00
committed by GitHub
90 changed files with 1707 additions and 1439 deletions

View File

@@ -31,6 +31,7 @@ body:
label: Version label: Version
description: What version of Homarr are you running? description: What version of Homarr are you running?
options: options:
- 1.22.0
- 1.21.0 - 1.21.0
- 1.20.0 - 1.20.0
- 1.19.1 - 1.19.1

View File

@@ -6,8 +6,8 @@
**Thank you for your contribution. Please ensure that your pull request meets the following pull request:** **Thank you for your contribution. Please ensure that your pull request meets the following pull request:**
- [ ] Builds without warnings or errors (``pnpm build``, autofix with ``pnpm format:fix``) - [ ] Builds without warnings or errors (`pnpm build`, autofix with `pnpm format:fix`)
- [ ] Pull request targets ``dev`` branch - [ ] Pull request targets `dev` branch
- [ ] Commits follow the [conventional commits guideline](https://www.conventionalcommits.org/en/v1.0.0/) - [ ] Commits follow the [conventional commits guideline](https://www.conventionalcommits.org/en/v1.0.0/)
- [ ] No shorthand variable names are used (eg. ``x``, ``y``, ``i`` or any abbrevation) - [ ] No shorthand variable names are used (eg. `x`, `y`, `i` or any abbrevation)
- [ ] Documentation is up to date. Create a pull request [here](https://github.com/homarr-labs/documentation/).

View File

@@ -62,7 +62,7 @@ const nextConfig: NextConfig = {
script-src * 'unsafe-inline' 'unsafe-eval'; script-src * 'unsafe-inline' 'unsafe-eval';
base-uri 'self'; base-uri 'self';
connect-src *; connect-src *;
style-src 'self' 'unsafe-inline'; style-src * 'unsafe-inline';
frame-ancestors *; frame-ancestors *;
frame-src *; frame-src *;
form-action 'self'; form-action 'self';

View File

@@ -55,14 +55,14 @@
"@mantine/modals": "^8.0.2", "@mantine/modals": "^8.0.2",
"@mantine/tiptap": "^8.0.2", "@mantine/tiptap": "^8.0.2",
"@million/lint": "1.0.14", "@million/lint": "1.0.14",
"@tabler/icons-react": "^3.33.0", "@tabler/icons-react": "^3.34.0",
"@tanstack/react-query": "^5.79.0", "@tanstack/react-query": "^5.80.6",
"@tanstack/react-query-devtools": "^5.79.0", "@tanstack/react-query-devtools": "^5.80.6",
"@tanstack/react-query-next-experimental": "^5.79.0", "@tanstack/react-query-next-experimental": "^5.80.6",
"@trpc/client": "^11.1.4", "@trpc/client": "^11.3.1",
"@trpc/next": "^11.1.4", "@trpc/next": "^11.3.1",
"@trpc/react-query": "^11.1.4", "@trpc/react-query": "^11.3.1",
"@trpc/server": "^11.1.4", "@trpc/server": "^11.3.1",
"@xterm/addon-canvas": "^0.7.0", "@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "0.10.0", "@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
@@ -81,24 +81,24 @@
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-simple-code-editor": "^0.14.1", "react-simple-code-editor": "^0.14.1",
"sass": "^1.89.0", "sass": "^1.89.1",
"superjson": "2.2.2", "superjson": "2.2.2",
"swagger-ui-react": "^5.22.0", "swagger-ui-react": "^5.24.0",
"use-deep-compare-effect": "^1.8.1", "use-deep-compare-effect": "^1.8.1",
"zod": "^3.25.42" "zod": "^3.25.55"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.1", "@types/chroma-js": "3.1.1",
"@types/node": "^22.15.28", "@types/node": "^22.15.30",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"@types/react": "19.1.6", "@types/react": "19.1.6",
"@types/react-dom": "19.1.5", "@types/react-dom": "19.1.6",
"@types/swagger-ui-react": "^5.18.0", "@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"node-loader": "^2.1.0", "node-loader": "^2.1.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"

View File

@@ -44,10 +44,10 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.15.28", "@types/node": "^22.15.30",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"esbuild": "^0.25.5", "esbuild": "^0.25.5",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"tsx": "4.19.4", "tsx": "4.19.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"

View File

@@ -35,7 +35,7 @@
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"esbuild": "^0.25.5", "esbuild": "^0.25.5",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }

View File

@@ -39,21 +39,21 @@
"@semantic-release/npm": "^12.0.1", "@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.3", "@semantic-release/release-notes-generator": "^14.0.3",
"@turbo/gen": "^2.5.4", "@turbo/gen": "^2.5.4",
"@vitejs/plugin-react": "^4.5.0", "@vitejs/plugin-react": "^4.5.1",
"@vitest/coverage-v8": "^3.1.4", "@vitest/coverage-v8": "^3.2.2",
"@vitest/ui": "^3.1.4", "@vitest/ui": "^3.2.2",
"conventional-changelog-conventionalcommits": "^9.0.0", "conventional-changelog-conventionalcommits": "^9.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"jsdom": "^26.1.0", "jsdom": "^26.1.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"semantic-release": "^24.2.5", "semantic-release": "^24.2.5",
"testcontainers": "^10.28.0", "testcontainers": "^11.0.0",
"turbo": "^2.5.4", "turbo": "^2.5.4",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.4" "vitest": "^3.2.2"
}, },
"packageManager": "pnpm@10.11.0", "packageManager": "pnpm@10.11.1",
"engines": { "engines": {
"node": ">=22.16.0" "node": ">=22.16.0"
}, },

View File

@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -41,24 +41,24 @@
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.3.0", "@kubernetes/client-node": "^1.3.0",
"@tanstack/react-query": "^5.79.0", "@tanstack/react-query": "^5.80.6",
"@trpc/client": "^11.1.4", "@trpc/client": "^11.3.1",
"@trpc/react-query": "^11.1.4", "@trpc/react-query": "^11.3.1",
"@trpc/server": "^11.1.4", "@trpc/server": "^11.3.1",
"@trpc/tanstack-react-query": "^11.1.4", "@trpc/tanstack-react-query": "^11.3.1",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"next": "15.3.3", "next": "15.3.3",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"superjson": "2.2.2", "superjson": "2.2.2",
"trpc-to-openapi": "^2.3.1", "trpc-to-openapi": "^2.3.1",
"zod": "^3.25.42" "zod": "^3.25.55"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }

View File

@@ -39,7 +39,7 @@
"next-auth": "5.0.0-beta.28", "next-auth": "5.0.0-beta.28",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"zod": "^3.25.42" "zod": "^3.25.55"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
@@ -47,7 +47,7 @@
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "5.0.2", "@types/bcrypt": "5.0.2",
"@types/cookies": "0.9.0", "@types/cookies": "0.9.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }

View File

@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -30,7 +30,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -35,7 +35,7 @@
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"esbuild": "^0.25.5", "esbuild": "^0.25.5",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -34,14 +34,14 @@
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"undici": "7.10.0", "undici": "7.10.0",
"zod": "^3.25.42", "zod": "^3.25.55",
"zod-validation-error": "^3.4.1" "zod-validation-error": "^3.4.1"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -25,14 +25,14 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"node-cron": "^4.0.7" "node-cron": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -44,7 +44,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -46,11 +46,11 @@
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^8.0.2", "@mantine/core": "^8.0.2",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@testcontainers/mysql": "^10.28.0", "@testcontainers/mysql": "^11.0.0",
"better-sqlite3": "^11.10.0", "better-sqlite3": "^11.10.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"drizzle-kit": "^0.31.1", "drizzle-kit": "^0.31.1",
"drizzle-orm": "^0.44.0", "drizzle-orm": "^0.44.2",
"drizzle-zod": "^0.7.1", "drizzle-zod": "^0.7.1",
"mysql2": "3.14.1" "mysql2": "3.14.1"
}, },
@@ -61,7 +61,7 @@
"@types/better-sqlite3": "7.6.13", "@types/better-sqlite3": "7.6.13",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"esbuild": "^0.25.5", "esbuild": "^0.25.5",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"tsx": "4.19.4", "tsx": "4.19.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"

View File

@@ -9,7 +9,7 @@ import * as mysqlSchema from "../schema/mysql";
describe("Mysql Migration", () => { describe("Mysql Migration", () => {
test("should add all tables and keys specified in migration files", async () => { test("should add all tables and keys specified in migration files", async () => {
const mysqlContainer = await new MySqlContainer().start(); const mysqlContainer = await new MySqlContainer("mysql:latest").start();
const connection = mysql.createConnection({ const connection = mysql.createConnection({
host: mysqlContainer.getHost(), host: mysqlContainer.getHost(),

View File

@@ -24,14 +24,14 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"fast-xml-parser": "^5.2.3", "fast-xml-parser": "^5.2.4",
"zod": "^3.25.42" "zod": "^3.25.55"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"tsx": "4.19.4", "tsx": "4.19.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }

View File

@@ -26,14 +26,14 @@
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0", "@homarr/env": "workspace:^0.1.0",
"dockerode": "^4.0.6" "dockerode": "^4.0.7"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.39", "@types/dockerode": "^3.3.39",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -24,13 +24,13 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@t3-oss/env-nextjs": "^0.13.6", "@t3-oss/env-nextjs": "^0.13.6",
"zod": "^3.25.42" "zod": "^3.25.55"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -27,13 +27,13 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/form": "^8.0.2", "@mantine/form": "^8.0.2",
"zod": "^3.25.42" "zod": "^3.25.55"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -31,13 +31,13 @@
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.0.2", "@mantine/core": "^8.0.2",
"react": "19.1.0", "react": "19.1.0",
"zod": "^3.25.42" "zod": "^3.25.55"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -31,7 +31,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -44,7 +44,7 @@
"tsdav": "^2.1.4", "tsdav": "^2.1.4",
"undici": "7.10.0", "undici": "7.10.0",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
"zod": "^3.25.42" "zod": "^3.25.55"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
@@ -52,7 +52,7 @@
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-unifi": "^2.5.1", "@types/node-unifi": "^2.5.1",
"@types/xml2js": "^0.4.14", "@types/xml2js": "^0.4.14",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -1,6 +1,3 @@
import dayjs from "dayjs";
import objectSupport from "dayjs/plugin/objectSupport";
import utc from "dayjs/plugin/utc";
import * as ical from "node-ical"; import * as ical from "node-ical";
import { DAVClient } from "tsdav"; import { DAVClient } from "tsdav";
import type { Dispatcher, RequestInit as UndiciFetchRequestInit } from "undici"; import type { Dispatcher, RequestInit as UndiciFetchRequestInit } from "undici";
@@ -15,9 +12,6 @@ import { Integration } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service"; import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { CalendarEvent } from "../calendar-types"; import type { CalendarEvent } from "../calendar-types";
dayjs.extend(utc);
dayjs.extend(objectSupport);
@HandleIntegrationErrors([integrationTsdavHttpErrorHandler]) @HandleIntegrationErrors([integrationTsdavHttpErrorHandler])
export class NextcloudIntegration extends Integration { export class NextcloudIntegration extends Integration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> { protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
@@ -27,7 +21,7 @@ export class NextcloudIntegration extends Integration {
return { success: true }; return { success: true };
} }
public async getCalendarEventsAsync(start: Date, end: Date): Promise<CalendarEvent[]> { public async getCalendarEventsAsync(start: Date, end: Date, _showUnmonitored?: boolean): Promise<CalendarEvent[]> {
const client = await this.createCalendarClientAsync(); const client = await this.createCalendarClientAsync();
await client.login(); await client.login();
@@ -57,14 +51,7 @@ export class NextcloudIntegration extends Integration {
logger.debug(`Converting VEVENT event to ${event.etag} from Nextcloud: ${JSON.stringify(veventObject)}`); logger.debug(`Converting VEVENT event to ${event.etag} from Nextcloud: ${JSON.stringify(veventObject)}`);
const date = dayjs.utc({ const date = veventObject.start;
days: veventObject.start.getDay(),
month: veventObject.start.getMonth(),
year: veventObject.start.getFullYear(),
hours: veventObject.start.getHours(),
minutes: veventObject.start.getMinutes(),
seconds: veventObject.start.getSeconds(),
});
const eventUrlWithoutHost = new URL(event.url).pathname; const eventUrlWithoutHost = new URL(event.url).pathname;
const dateInMillis = veventObject.start.valueOf(); const dateInMillis = veventObject.start.valueOf();
@@ -75,7 +62,7 @@ export class NextcloudIntegration extends Integration {
return { return {
name: veventObject.summary, name: veventObject.summary,
date: date.toDate(), date,
subName: "", subName: "",
description: veventObject.description, description: veventObject.description,
links: [ links: [

View File

@@ -66,7 +66,7 @@ export class OpenMediaVaultIntegration extends Integration {
return { return {
version: systemResult.data.response.version, version: systemResult.data.response.version,
cpuModelName: systemResult.data.response.cpuModelName, cpuModelName: systemResult.data.response.cpuModelName ?? "Unknown CPU",
cpuUtilization: systemResult.data.response.cpuUtilization, cpuUtilization: systemResult.data.response.cpuUtilization,
memUsed: systemResult.data.response.memUsed, memUsed: systemResult.data.response.memUsed,
memAvailable: systemResult.data.response.memAvailable, memAvailable: systemResult.data.response.memAvailable,

View File

@@ -4,7 +4,7 @@ import { z } from "zod";
export const systemInformationSchema = z.object({ export const systemInformationSchema = z.object({
response: z.object({ response: z.object({
version: z.string(), version: z.string(),
cpuModelName: z.string(), cpuModelName: z.string().nullable(),
cpuUtilization: z.number(), cpuUtilization: z.number(),
memUsed: z.string(), memUsed: z.string(),
memAvailable: z.string(), memAvailable: z.string(),

View File

@@ -64,14 +64,14 @@ describe("Home Assistant integration", () => {
const prepareHomeAssistantContainerAsync = async () => { const prepareHomeAssistantContainerAsync = async () => {
const homeAssistantContainer = createHomeAssistantContainer(); const homeAssistantContainer = createHomeAssistantContainer();
const startedContainer = await homeAssistantContainer.start(); const startedContainer = await homeAssistantContainer.start();
await startedContainer.exec(["unzip", "-o", "/tmp/config.zip", "-d", "/config"]); await startedContainer.exec(["unzip", "-o", "/tmp/config.zip", "-d", "/config"]);
await startedContainer.restart(); await startedContainer.restart();
return startedContainer; return startedContainer;
}; };
const createHomeAssistantContainer = () => { const createHomeAssistantContainer = () => {
return new GenericContainer(IMAGE_NAME) return (
new GenericContainer(IMAGE_NAME)
.withCopyFilesToContainer([ .withCopyFilesToContainer([
{ {
source: join(__dirname, "/volumes/home-assistant-config.zip"), source: join(__dirname, "/volumes/home-assistant-config.zip"),
@@ -80,10 +80,13 @@ const createHomeAssistantContainer = () => {
]) ])
.withPrivilegedMode() .withPrivilegedMode()
.withExposedPorts(8123) .withExposedPorts(8123)
.withWaitStrategy(Wait.forHttp("/", 8123)); // This has to be a page that is not redirected (or a status code has to be defined withStatusCode(statusCode))
.withWaitStrategy(Wait.forHttp("/onboarding.html", 8123))
);
}; };
const createHomeAssistantIntegration = (container: StartedTestContainer, apiKeyOverride?: string) => { const createHomeAssistantIntegration = (container: StartedTestContainer, apiKeyOverride?: string) => {
console.log("Creating Home Assistant integration...");
return new HomeAssistantIntegration({ return new HomeAssistantIntegration({
id: "1", id: "1",
decryptedSecrets: [ decryptedSecrets: [

View File

@@ -145,8 +145,8 @@ describe("Nzbget integration", () => {
// Assert // Assert
await expect(actAsync()).resolves.not.toThrow(); await expect(actAsync()).resolves.not.toThrow();
// NzbGet is slow and we wait for a second before querying the items. Test was flaky without this. // NzbGet is slow and we wait for a few seconds before querying the items. Test was flaky without this.
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 5000));
const result = await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 }); const result = await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
expect(result.items).toHaveLength(0); expect(result.items).toHaveLength(0);

View File

@@ -211,12 +211,15 @@ const createPiHoleIntegrationV5 = (container: StartedTestContainer, apiKey: stri
}; };
const createPiHoleV6Container = (password: string) => { const createPiHoleV6Container = (password: string) => {
return new GenericContainer("pihole/pihole:latest") return (
new GenericContainer("pihole/pihole:latest")
.withEnvironment({ .withEnvironment({
FTLCONF_webserver_api_password: password, FTLCONF_webserver_api_password: password,
}) })
.withExposedPorts(80) .withExposedPorts(80)
.withWaitStrategy(Wait.forHttp("/admin", 80)); // This has to be a page that is not redirected (or a status code has to be defined withStatusCode(statusCode))
.withWaitStrategy(Wait.forHttp("/admin/login", 80))
);
}; };
const createPiHoleIntegrationV6 = (container: StartedTestContainer, apiKey: string) => { const createPiHoleIntegrationV6 = (container: StartedTestContainer, apiKey: string) => {

View File

@@ -192,7 +192,8 @@ describe("Sabnzbd integration", () => {
}); });
const createSabnzbdContainer = () => { const createSabnzbdContainer = () => {
return new GenericContainer(IMAGE_NAME) return (
new GenericContainer(IMAGE_NAME)
.withCopyFilesToContainer([ .withCopyFilesToContainer([
{ {
source: join(__dirname, "/volumes/usenet/sabnzbd.ini"), source: join(__dirname, "/volumes/usenet/sabnzbd.ini"),
@@ -201,7 +202,9 @@ const createSabnzbdContainer = () => {
]) ])
.withExposedPorts(1212) .withExposedPorts(1212)
.withEnvironment({ PUID: "0", PGID: "0" }) .withEnvironment({ PUID: "0", PGID: "0" })
.withWaitStrategy(Wait.forHttp("/", 1212)); // This has to be a page that is not redirected (or a status code has to be defined withStatusCode(statusCode))
.withWaitStrategy(Wait.forHttp("/sabnzbd/wizard/", 1212))
);
}; };
const createSabnzbdIntegration = (container: StartedTestContainer, apiKey: string) => { const createSabnzbdIntegration = (container: StartedTestContainer, apiKey: string) => {

View File

@@ -27,13 +27,13 @@
"ioredis": "5.6.1", "ioredis": "5.6.1",
"superjson": "2.2.2", "superjson": "2.2.2",
"winston": "3.17.0", "winston": "3.17.0",
"zod": "^3.25.42" "zod": "^3.25.55"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -34,18 +34,18 @@
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.0.2", "@mantine/core": "^8.0.2",
"@tabler/icons-react": "^3.33.0", "@tabler/icons-react": "^3.34.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"next": "15.3.3", "next": "15.3.3",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"zod": "^3.25.42" "zod": "^3.25.55"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -25,13 +25,13 @@
"dependencies": { "dependencies": {
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@mantine/notifications": "^8.0.2", "@mantine/notifications": "^8.0.2",
"@tabler/icons-react": "^3.33.0" "@tabler/icons-react": "^3.34.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -44,7 +44,7 @@
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"superjson": "2.2.2", "superjson": "2.2.2",
"zod": "^3.25.42", "zod": "^3.25.55",
"zod-form-data": "^2.0.7" "zod-form-data": "^2.0.7"
}, },
"devDependencies": { "devDependencies": {
@@ -52,7 +52,7 @@
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/adm-zip": "0.5.7", "@types/adm-zip": "0.5.7",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -23,13 +23,13 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"zod": "^3.25.42" "zod": "^3.25.55"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -30,7 +30,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -37,7 +37,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -35,7 +35,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -36,7 +36,7 @@
"@mantine/core": "^8.0.2", "@mantine/core": "^8.0.2",
"@mantine/hooks": "^8.0.2", "@mantine/hooks": "^8.0.2",
"@mantine/spotlight": "^8.0.2", "@mantine/spotlight": "^8.0.2",
"@tabler/icons-react": "^3.33.0", "@tabler/icons-react": "^3.34.0",
"jotai": "^2.12.5", "jotai": "^2.12.5",
"next": "15.3.3", "next": "15.3.3",
"react": "19.1.0", "react": "19.1.0",
@@ -47,7 +47,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -41,7 +41,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -612,17 +612,17 @@
"select": { "select": {
"label": "选择应用", "label": "选择应用",
"notFound": "未找到应用", "notFound": "未找到应用",
"search": "", "search": "搜索应用",
"noResults": "", "noResults": "没有结果",
"action": "", "action": "选择 {app}",
"title": "" "title": "选择一个应用程序添加到此面板"
}, },
"create": { "create": {
"title": "", "title": "创建应用",
"description": "", "description": "创建应用 ",
"action": "" "action": "打开应用创建"
}, },
"add": "" "add": "添加应用"
} }
}, },
"integration": { "integration": {
@@ -705,59 +705,59 @@
"error": { "error": {
"common": { "common": {
"cause": { "cause": {
"title": "" "title": "更多详情"
} }
}, },
"unknown": { "unknown": {
"title": "", "title": "未知错误",
"description": "" "description": "发生未知错误,打开下面的原因以查看更多详情"
}, },
"parse": { "parse": {
"title": "", "title": "解析错误",
"description": "" "description": "无法解析该响应。请确认URL指向服务的基本URL。"
}, },
"authorization": { "authorization": {
"title": "", "title": "授权错误",
"description": "" "description": "请求未被授权。请验证凭据是否正确,并且您拥有足够的权限。"
}, },
"statusCode": { "statusCode": {
"title": "", "title": "响应错误",
"description": "", "description": "",
"otherDescription": "", "otherDescription": "",
"reason": { "reason": {
"badRequest": "", "badRequest": "错误请求",
"notFound": "", "notFound": "未找到",
"tooManyRequests": "", "tooManyRequests": "请求过于频繁",
"internalServerError": "", "internalServerError": "服务器内部错误",
"serviceUnavailable": "", "serviceUnavailable": "服务暂时不可用",
"gatewayTimeout": "" "gatewayTimeout": "网关超时"
} }
}, },
"certificate": { "certificate": {
"title": "", "title": "证书错误",
"description": { "description": {
"expired": "", "expired": "此证书已经过期。",
"notYetValid": "", "notYetValid": "此证书尚未生效。",
"untrusted": "", "untrusted": "此证书不受信任。",
"hostnameMismatch": "" "hostnameMismatch": "证书主机名与 URL 不匹配。"
}, },
"alert": { "alert": {
"permission": { "permission": {
"title": "", "title": "权限不足",
"message": "" "message": "您无权信任或上传证书。请与您的管理员联系上传必要的根证书。"
}, },
"hostnameMismatch": { "hostnameMismatch": {
"title": "", "title": "主机名不匹配",
"message": "" "message": "证书中的主机名与您连接的主机名不匹配。 这可能会显示安全风险,但您仍然可以选择信任此证书。"
}, },
"extract": { "extract": {
"title": "", "title": "CA证书提取失败",
"message": "" "message": ""
} }
}, },
"action": { "action": {
"retry": { "retry": {
"label": "" "label": "重试创建"
}, },
"trust": { "trust": {
"label": "" "label": ""
@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "Repositories", "label": "Repositories",
"addRRepository": { "addRepository": {
"label": "Tilføj repository" "label": "Tilføj repository"
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "Repositories", "label": "Repositories",
"addRRepository": { "addRepository": {
"label": "Repository hinzufügen" "label": "Repository hinzufügen"
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,9 +2223,18 @@
}, },
"repositories": { "repositories": {
"label": "Repositories", "label": "Repositories",
"addRRepository": { "addRepository": {
"label": "Add repository" "label": "Add repository"
}, },
"importRepositories": {
"label": "Import from docker",
"loading": "Loading docker images",
"noImagesFound": "No docker images found",
"listFoundImages": "List of found images",
"listAlreadyImportedImages": "List of already imported images",
"allImagesAlreadyImported": "All images already imported",
"onlyAdminCanImport": "Only administrators can import from docker"
},
"provider": { "provider": {
"label": "Provider" "label": "Provider"
}, },
@@ -2266,6 +2275,9 @@
"label": "Confirm" "label": "Confirm"
} }
}, },
"importForm": {
"title": "Import from Docker"
},
"example": { "example": {
"label": "Example" "label": "Example"
}, },

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -747,7 +747,7 @@
"message": "" "message": ""
}, },
"hostnameMismatch": { "hostnameMismatch": {
"title": "", "title": "Nom d'hôte incohérent",
"message": "" "message": ""
}, },
"extract": { "extract": {
@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -1758,7 +1758,7 @@
"label": "הצג מידע זיכרון" "label": "הצג מידע זיכרון"
}, },
"showUptime": { "showUptime": {
"label": "" "label": "הצג זמן פעולה"
}, },
"fileSystem": { "fileSystem": {
"label": "הצג מידע על מערכת הקבצים" "label": "הצג מידע על מערכת הקבצים"
@@ -1767,7 +1767,7 @@
"label": "כרטיסיית ברירת מחדל" "label": "כרטיסיית ברירת מחדל"
}, },
"visibleClusterSections": { "visibleClusterSections": {
"label": "" "label": "מקטעי אשכול גלויים"
}, },
"sectionIndicatorRequirement": { "sectionIndicatorRequirement": {
"label": "דרישת מציין מקטע" "label": "דרישת מציין מקטע"
@@ -1961,8 +1961,8 @@
"label": "השתמש במסנן כדי לחשב יחס" "label": "השתמש במסנן כדי לחשב יחס"
}, },
"limitPerIntegration": { "limitPerIntegration": {
"label": "", "label": "הגבלת פריטים לכל אינטגרציה",
"description": "" "description": "פעולה זו תגביל את מספר הפריטים המוצגים בכל אינטגרציה, לא באופן גלובלי"
} }
}, },
"errors": { "errors": {
@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "מאגרים", "label": "מאגרים",
"addRRepository": { "addRepository": {
"label": "הוסף מאגר" "label": "הוסף מאגר"
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "Depolar", "label": "Depolar",
"addRRepository": { "addRepository": {
"label": "Depo Ekle" "label": "Depo Ekle"
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRRepository": { "addRepository": {
"label": "" "label": ""
}, },
"provider": { "provider": {

View File

@@ -2223,7 +2223,7 @@
}, },
"repositories": { "repositories": {
"label": "儲存庫", "label": "儲存庫",
"addRRepository": { "addRepository": {
"label": "新增儲存庫" "label": "新增儲存庫"
}, },
"provider": { "provider": {

View File

@@ -32,7 +32,7 @@
"@mantine/core": "^8.0.2", "@mantine/core": "^8.0.2",
"@mantine/dates": "^8.0.2", "@mantine/dates": "^8.0.2",
"@mantine/hooks": "^8.0.2", "@mantine/hooks": "^8.0.2",
"@tabler/icons-react": "^3.33.0", "@tabler/icons-react": "^3.34.0",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "15.3.3", "next": "15.3.3",
"react": "19.1.0", "react": "19.1.0",
@@ -43,7 +43,7 @@
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/css-modules": "^1.0.5", "@types/css-modules": "^1.0.5",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -24,14 +24,14 @@
"dependencies": { "dependencies": {
"@homarr/definitions": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"zod": "^3.25.42", "zod": "^3.25.55",
"zod-form-data": "^2.0.7" "zod-form-data": "^2.0.7"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -51,22 +51,22 @@
"@mantine/charts": "^8.0.2", "@mantine/charts": "^8.0.2",
"@mantine/core": "^8.0.2", "@mantine/core": "^8.0.2",
"@mantine/hooks": "^8.0.2", "@mantine/hooks": "^8.0.2",
"@tabler/icons-react": "^3.33.0", "@tabler/icons-react": "^3.34.0",
"@tiptap/extension-color": "2.12.0", "@tiptap/extension-color": "2.14.0",
"@tiptap/extension-highlight": "2.12.0", "@tiptap/extension-highlight": "2.14.0",
"@tiptap/extension-image": "2.12.0", "@tiptap/extension-image": "2.14.0",
"@tiptap/extension-link": "^2.12.0", "@tiptap/extension-link": "^2.14.0",
"@tiptap/extension-table": "2.12.0", "@tiptap/extension-table": "2.14.0",
"@tiptap/extension-table-cell": "2.12.0", "@tiptap/extension-table-cell": "2.14.0",
"@tiptap/extension-table-header": "2.12.0", "@tiptap/extension-table-header": "2.14.0",
"@tiptap/extension-table-row": "2.12.0", "@tiptap/extension-table-row": "2.14.0",
"@tiptap/extension-task-item": "2.12.0", "@tiptap/extension-task-item": "2.14.0",
"@tiptap/extension-task-list": "2.12.0", "@tiptap/extension-task-list": "2.14.0",
"@tiptap/extension-text-align": "2.12.0", "@tiptap/extension-text-align": "2.14.0",
"@tiptap/extension-text-style": "2.12.0", "@tiptap/extension-text-style": "2.14.0",
"@tiptap/extension-underline": "2.12.0", "@tiptap/extension-underline": "2.14.0",
"@tiptap/react": "^2.12.0", "@tiptap/react": "^2.14.0",
"@tiptap/starter-kit": "^2.12.0", "@tiptap/starter-kit": "^2.14.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
@@ -75,15 +75,15 @@
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"recharts": "^2.15.3", "recharts": "^2.15.3",
"video.js": "^8.22.0", "video.js": "^8.23.3",
"zod": "^3.25.42" "zod": "^3.25.55"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/video.js": "^7.3.58", "@types/video.js": "^7.3.58",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -1,19 +1,46 @@
"use client"; "use client";
import React, { useCallback, useEffect, useMemo, useState } from "react"; import React, { useCallback, useEffect, useMemo, useState } from "react";
import { ActionIcon, Button, Divider, Fieldset, Group, Select, Stack, Text, TextInput } from "@mantine/core"; import {
Accordion,
ActionIcon,
Button,
Checkbox,
Code,
Divider,
Fieldset,
Group,
Image,
Loader,
Select,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from "@mantine/core";
import type { CheckboxProps } from "@mantine/core";
import type { FormErrors } from "@mantine/form"; import type { FormErrors } from "@mantine/form";
import { useDebouncedValue } from "@mantine/hooks"; import { useDebouncedValue } from "@mantine/hooks";
import { IconEdit, IconTrash, IconTriangleFilled } from "@tabler/icons-react"; import {
IconBrandDocker,
IconEdit,
IconPlus,
IconSquare,
IconSquareCheck,
IconTrash,
IconTriangleFilled,
} from "@tabler/icons-react";
import { escapeForRegEx } from "@tiptap/react"; import { escapeForRegEx } from "@tiptap/react";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { findBestIconMatch, IconPicker } from "@homarr/forms-collection"; import { findBestIconMatch, IconPicker } from "@homarr/forms-collection";
import { createModal, useModalAction } from "@homarr/modals"; import { createModal, useModalAction } from "@homarr/modals";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { MaskedOrNormalImage } from "@homarr/ui"; import { MaskedImage } from "@homarr/ui";
import { Providers } from "../releases/releases-providers"; import { isProviderKey, Providers } from "../releases/releases-providers";
import type { ReleasesRepository, ReleasesVersionFilter } from "../releases/releases-repository"; import type { ReleasesRepository, ReleasesVersionFilter } from "../releases/releases-repository";
import type { CommonWidgetInputProps } from "./common"; import type { CommonWidgetInputProps } from "./common";
import { useWidgetInputTranslation } from "./common"; import { useWidgetInputTranslation } from "./common";
@@ -32,11 +59,14 @@ export const WidgetMultiReleasesRepositoriesInput = ({
const tRepository = useScopedI18n("widget.releases.option.repositories"); const tRepository = useScopedI18n("widget.releases.option.repositories");
const form = useFormContext(); const form = useFormContext();
const repositories = form.values.options[property] as ReleasesRepository[]; const repositories = form.values.options[property] as ReleasesRepository[];
const { openModal } = useModalAction(ReleaseEditModal); const { openModal: openEditModal } = useModalAction(RepositoryEditModal);
const { openModal: openImportModal } = useModalAction(RepositoryImportModal);
const versionFilterPrecisionOptions = useMemo( const versionFilterPrecisionOptions = useMemo(
() => [tRepository("versionFilter.precision.options.none"), "#", "#.#", "#.#.#", "#.#.#.#", "#.#.#.#.#"], () => [tRepository("versionFilter.precision.options.none"), "#", "#.#", "#.#.#", "#.#.#.#", "#.#.#.#.#"],
[tRepository], [tRepository],
); );
const { data: session } = useSession();
const isAdmin = session?.user.permissions.includes("admin") ?? false;
const onRepositorySave = useCallback( const onRepositorySave = useCallback(
(repository: ReleasesRepository, index: number): FormValidation => { (repository: ReleasesRepository, index: number): FormValidation => {
@@ -62,8 +92,8 @@ export const WidgetMultiReleasesRepositoriesInput = ({
[form, property], [form, property],
); );
const addNewItem = () => { const addNewRepository = () => {
const item = { const repository: ReleasesRepository = {
providerKey: "DockerHub", providerKey: "DockerHub",
identifier: "", identifier: "",
}; };
@@ -74,16 +104,16 @@ export const WidgetMultiReleasesRepositoriesInput = ({
...previous, ...previous,
options: { options: {
...previous.options, ...previous.options,
[property]: [...previousValues, item], [property]: [...previousValues, repository],
}, },
}; };
}); });
const index = repositories.length; const index = repositories.length;
openModal({ openEditModal({
fieldPath: `options.${property}.${index}`, fieldPath: `options.${property}.${index}`,
repository: item, repository,
onRepositorySave: (saved) => onRepositorySave(saved, index), onRepositorySave: (saved) => onRepositorySave(saved, index),
onRepositoryCancel: () => onRepositoryRemove(index), onRepositoryCancel: () => onRepositoryRemove(index),
versionFilterPrecisionOptions, versionFilterPrecisionOptions,
@@ -106,24 +136,56 @@ export const WidgetMultiReleasesRepositoriesInput = ({
return ( return (
<Fieldset legend={t("label")}> <Fieldset legend={t("label")}>
<Stack gap="5"> <Stack gap="5">
<Button onClick={addNewItem}>{tRepository("addRRepository.label")}</Button> <Group grow>
<Button leftSection={<IconPlus />} onClick={addNewRepository}>
{tRepository("addRepository.label")}
</Button>
<Tooltip label={tRepository("importRepositories.onlyAdminCanImport")} disabled={isAdmin} withArrow>
<Button
disabled={!isAdmin}
leftSection={<IconBrandDocker stroke={1.25} />}
onClick={() =>
openImportModal({
repositories,
versionFilterPrecisionOptions,
onConfirm: (selectedRepositories) => {
if (!selectedRepositories.length) return;
form.setValues((previous) => {
const previousValues = previous.options?.[property] as ReleasesRepository[];
return {
...previous,
options: {
...previous.options,
[property]: [...previousValues, ...selectedRepositories],
},
};
});
},
isAdmin,
})
}
>
{tRepository("importRepositories.label")}
</Button>
</Tooltip>
</Group>
<Divider my="sm" /> <Divider my="sm" />
{repositories.map((repository, index) => { {repositories.map((repository, index) => {
return ( return (
<Stack key={`${repository.providerKey}.${repository.identifier}`} gap={5}> <Stack key={`${repository.providerKey}.${repository.identifier}`} gap={5}>
<Group align="center" gap="xs"> <Group align="center" gap="xs">
<MaskedOrNormalImage <Image
hasColor={false} src={repository.iconUrl ?? Providers[repository.providerKey].iconUrl}
imageUrl={repository.iconUrl ?? Providers[repository.providerKey]?.iconUrl}
style={{ style={{
height: "1em", height: "1.2em",
width: "1em", width: "1.2em",
}} }}
/> />
<Text c="dimmed" fw={100} size="xs"> <Text c="dimmed" fw={100} size="xs">
{Providers[repository.providerKey]?.name} {Providers[repository.providerKey].name}
</Text> </Text>
<Group justify="space-between" align="center" style={{ flex: 1 }} gap={5}> <Group justify="space-between" align="center" style={{ flex: 1 }} gap={5}>
@@ -135,7 +197,7 @@ export const WidgetMultiReleasesRepositoriesInput = ({
<Button <Button
onClick={() => onClick={() =>
openModal({ openEditModal({
fieldPath: `options.${property}.${index}`, fieldPath: `options.${property}.${index}`,
repository, repository,
onRepositorySave: (saved) => onRepositorySave(saved, index), onRepositorySave: (saved) => onRepositorySave(saved, index),
@@ -185,7 +247,7 @@ const formatIdentifierName = (identifier: string) => {
return unformattedName?.replace(/[-_]/g, " ").replace(/(?:^\w|[A-Z]|\b\w)/g, (char) => char.toUpperCase()) ?? ""; return unformattedName?.replace(/[-_]/g, " ").replace(/(?:^\w|[A-Z]|\b\w)/g, (char) => char.toUpperCase()) ?? "";
}; };
interface ReleaseEditProps { interface RepositoryEditProps {
fieldPath: string; fieldPath: string;
repository: ReleasesRepository; repository: ReleasesRepository;
onRepositorySave: (repository: ReleasesRepository) => FormValidation; onRepositorySave: (repository: ReleasesRepository) => FormValidation;
@@ -193,7 +255,7 @@ interface ReleaseEditProps {
versionFilterPrecisionOptions: string[]; versionFilterPrecisionOptions: string[];
} }
const ReleaseEditModal = createModal<ReleaseEditProps>(({ innerProps, actions }) => { const RepositoryEditModal = createModal<RepositoryEditProps>(({ innerProps, actions }) => {
const tRepository = useScopedI18n("widget.releases.option.repositories"); const tRepository = useScopedI18n("widget.releases.option.repositories");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [tempRepository, setTempRepository] = useState(() => ({ ...innerProps.repository })); const [tempRepository, setTempRepository] = useState(() => ({ ...innerProps.repository }));
@@ -262,7 +324,7 @@ const ReleaseEditModal = createModal<ReleaseEditProps>(({ innerProps, actions })
value={tempRepository.providerKey} value={tempRepository.providerKey}
error={formErrors[`${innerProps.fieldPath}.providerKey`]} error={formErrors[`${innerProps.fieldPath}.providerKey`]}
onChange={(value) => { onChange={(value) => {
if (value && Providers[value]) { if (value && isProviderKey(value)) {
handleChange({ providerKey: value }); handleChange({ providerKey: value });
} }
}} }}
@@ -386,7 +448,7 @@ const ReleaseEditModal = createModal<ReleaseEditProps>(({ innerProps, actions })
{tRepository("editForm.cancel.label")} {tRepository("editForm.cancel.label")}
</Button> </Button>
<Button data-autofocus onClick={handleConfirm} color="red.9" loading={loading}> <Button data-autofocus onClick={handleConfirm} loading={loading}>
{tRepository("editForm.confirm.label")} {tRepository("editForm.confirm.label")}
</Button> </Button>
</Group> </Group>
@@ -398,3 +460,247 @@ const ReleaseEditModal = createModal<ReleaseEditProps>(({ innerProps, actions })
}, },
size: "xl", size: "xl",
}); });
interface ReleasesRepositoryImport extends ReleasesRepository {
alreadyImported: boolean;
}
interface ContainerImageSelectorProps {
containerImage: ReleasesRepositoryImport;
versionFilterPrecisionOptions: string[];
onImageSelectionChanged?: (isSelected: boolean) => void;
}
const ContainerImageSelector = ({
containerImage,
versionFilterPrecisionOptions,
onImageSelectionChanged,
}: ContainerImageSelectorProps) => {
const tRepository = useScopedI18n("widget.releases.option.repositories");
const checkBoxProps: CheckboxProps = !onImageSelectionChanged
? {
disabled: true,
checked: true,
}
: {
onChange: (event) => onImageSelectionChanged(event.currentTarget.checked),
};
return (
<Group
key={`${Providers[containerImage.providerKey].name}/${containerImage.identifier}`}
gap="xl"
justify="space-between"
>
<Group gap="md">
<Checkbox
label={
<Group>
<Image
src={containerImage.iconUrl}
style={{
height: "1.2em",
width: "1.2em",
}}
/>
<Text>{containerImage.identifier}</Text>
</Group>
}
{...checkBoxProps}
/>
{containerImage.versionFilter && (
<Group gap={5}>
<Text c="dimmed" size="xs">
{tRepository("versionFilter.label")}:
</Text>
<Code>{containerImage.versionFilter.prefix && containerImage.versionFilter.prefix}</Code>
<Code color="var(--mantine-primary-color-light)" fw={700}>
{versionFilterPrecisionOptions[containerImage.versionFilter.precision]}
</Code>
<Code>{containerImage.versionFilter.suffix && containerImage.versionFilter.suffix}</Code>
</Group>
)}
</Group>
<Group>
<MaskedImage
color="dimmed"
imageUrl={Providers[containerImage.providerKey].iconUrl}
style={{
height: "1em",
width: "1em",
}}
/>
<Text ff="monospace" c="dimmed" size="sm">
{Providers[containerImage.providerKey].name}
</Text>
</Group>
</Group>
);
};
interface RepositoryImportProps {
repositories: ReleasesRepository[];
versionFilterPrecisionOptions: string[];
onConfirm: (selectedRepositories: ReleasesRepositoryImport[]) => void;
isAdmin: boolean;
}
const RepositoryImportModal = createModal<RepositoryImportProps>(({ innerProps, actions }) => {
const tRepository = useScopedI18n("widget.releases.option.repositories");
const [loading, setLoading] = useState(false);
const [selectedImages, setSelectedImages] = useState([] as ReleasesRepositoryImport[]);
const docker = clientApi.docker.getContainers.useQuery(undefined, {
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
enabled: innerProps.isAdmin,
});
const containersImages: ReleasesRepositoryImport[] = useMemo(
() =>
docker.data?.containers.reduce<ReleasesRepositoryImport[]>((acc, containerImage) => {
const providerKey = containerImage.image.startsWith("ghcr.io/") ? "Github" : "DockerHub";
const [identifier, version] = containerImage.image.replace(/^(ghcr\.io\/|docker\.io\/)/, "").split(":");
if (!identifier) return acc;
if (acc.some((item) => item.providerKey === providerKey && item.identifier === identifier)) return acc;
acc.push({
providerKey,
identifier,
iconUrl: containerImage.iconUrl ?? undefined,
name: formatIdentifierName(identifier),
versionFilter: version ? parseImageVersionToVersionFilter(version) : undefined,
alreadyImported: innerProps.repositories.some(
(item) => item.providerKey === providerKey && item.identifier === identifier,
),
});
return acc;
}, []) ?? [],
[docker.data, innerProps.repositories],
);
const handleConfirm = useCallback(() => {
setLoading(true);
innerProps.onConfirm(selectedImages);
setLoading(false);
actions.closeModal();
}, [innerProps, selectedImages, actions]);
const allImagesImported = useMemo(
() => containersImages.every((containerImage) => containerImage.alreadyImported),
[containersImages],
);
const anyImagesImported = useMemo(
() => containersImages.some((containerImage) => containerImage.alreadyImported),
[containersImages],
);
return (
<Stack>
{docker.isPending ? (
<Stack justify="center" align="center">
<Loader size="xl" />
<Title order={3}>{tRepository("importRepositories.loading")}</Title>
</Stack>
) : containersImages.length === 0 ? (
<Stack justify="center" align="center">
<IconBrandDocker stroke={1} size={128} />
<Title order={3}>{tRepository("importRepositories.noImagesFound")}</Title>
</Stack>
) : (
<Stack>
<Accordion defaultValue={!allImagesImported ? "foundImages" : anyImagesImported ? "alreadyImported" : ""}>
<Accordion.Item value="foundImages">
<Accordion.Control disabled={allImagesImported} icon={<IconSquare stroke={1.25} />}>
<Group>
{tRepository("importRepositories.listFoundImages")}
{allImagesImported && (
<Text c="dimmed" size="sm">
{tRepository("importRepositories.allImagesAlreadyImported")}
</Text>
)}
</Group>
</Accordion.Control>
<Accordion.Panel>
{!allImagesImported &&
containersImages
.filter((containerImage) => !containerImage.alreadyImported)
.map((containerImage) => {
return (
<ContainerImageSelector
key={`${containerImage.providerKey}/${containerImage.identifier}`}
containerImage={containerImage}
versionFilterPrecisionOptions={innerProps.versionFilterPrecisionOptions}
onImageSelectionChanged={(isSelected) =>
isSelected
? setSelectedImages([...selectedImages, containerImage])
: setSelectedImages(selectedImages.filter((img) => img !== containerImage))
}
/>
);
})}
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value="alreadyImported">
<Accordion.Control disabled={!anyImagesImported} icon={<IconSquareCheck stroke={1.25} />}>
{tRepository("importRepositories.listAlreadyImportedImages")}
</Accordion.Control>
<Accordion.Panel>
{anyImagesImported &&
containersImages
.filter((containerImage) => containerImage.alreadyImported)
.map((containerImage) => {
return (
<ContainerImageSelector
key={`${containerImage.providerKey}/${containerImage.identifier}`}
containerImage={containerImage}
versionFilterPrecisionOptions={innerProps.versionFilterPrecisionOptions}
/>
);
})}
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</Stack>
)}
<Group justify="flex-end">
<Button variant="default" onClick={actions.closeModal} color="gray.5">
{tRepository("editForm.cancel.label")}
</Button>
<Button onClick={handleConfirm} loading={loading} disabled={selectedImages.length === 0}>
{tRepository("editForm.confirm.label")}
</Button>
</Group>
</Stack>
);
}).withOptions({
defaultTitle(t) {
return t("widget.releases.option.repositories.importForm.title");
},
size: "xl",
});
const parseImageVersionToVersionFilter = (imageVersion: string): ReleasesVersionFilter | undefined => {
const version = /(?<=\D|^)\d+(?:\.\d+)*(?![\d.])/.exec(imageVersion)?.[0];
if (!version) return undefined;
const [prefix, suffix] = imageVersion.split(version);
return {
prefix,
precision: version.split(".").length,
suffix,
};
};

View File

@@ -156,7 +156,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
> >
<MaskedOrNormalImage <MaskedOrNormalImage
className="releases-repository-header-icon" className="releases-repository-header-icon"
imageUrl={repository.iconUrl ?? Providers[repository.providerKey]?.iconUrl} imageUrl={repository.iconUrl ?? Providers[repository.providerKey].iconUrl}
hasColor={hasIconColor} hasColor={hasIconColor}
style={{ style={{
width: "1em", width: "1em",
@@ -474,7 +474,7 @@ const ExpandedDisplay = ({ repository, hasIconColor }: ExtendedDisplayProps) =>
<Group className="releases-repository-expanded-header-provider-wrapper" gap={5} align="center"> <Group className="releases-repository-expanded-header-provider-wrapper" gap={5} align="center">
<MaskedOrNormalImage <MaskedOrNormalImage
className="releases-repository-expanded-header-provider-icon" className="releases-repository-expanded-header-provider-icon"
imageUrl={Providers[repository.providerKey]?.iconUrl} imageUrl={Providers[repository.providerKey].iconUrl}
hasColor={hasIconColor} hasColor={hasIconColor}
style={{ style={{
width: "1em", width: "1em",
@@ -482,7 +482,7 @@ const ExpandedDisplay = ({ repository, hasIconColor }: ExtendedDisplayProps) =>
}} }}
/> />
<Text className="releases-repository-expanded-header-provider-name" size="xs" c="iconColor" ff="monospace"> <Text className="releases-repository-expanded-header-provider-name" size="xs" c="iconColor" ff="monospace">
{Providers[repository.providerKey]?.name} {Providers[repository.providerKey].name}
</Text> </Text>
</Group> </Group>
</Group> </Group>

View File

@@ -3,16 +3,7 @@ export interface ReleasesProvider {
iconUrl: string; iconUrl: string;
} }
interface ProvidersProps { export const Providers = {
[key: string]: ReleasesProvider;
DockerHub: ReleasesProvider;
Github: ReleasesProvider;
Gitlab: ReleasesProvider;
Npm: ReleasesProvider;
Codeberg: ReleasesProvider;
}
export const Providers: ProvidersProps = {
DockerHub: { DockerHub: {
name: "Docker Hub", name: "Docker Hub",
iconUrl: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/docker.svg", iconUrl: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/docker.svg",
@@ -33,4 +24,10 @@ export const Providers: ProvidersProps = {
name: "Codeberg", name: "Codeberg",
iconUrl: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/codeberg.svg", iconUrl: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/codeberg.svg",
}, },
} as const satisfies Record<string, ReleasesProvider>;
export type ProviderKey = keyof typeof Providers;
export const isProviderKey = (key: string): key is ProviderKey => {
return key in Providers;
}; };

View File

@@ -1,3 +1,5 @@
import type { ProviderKey } from "./releases-providers";
export interface ReleasesVersionFilter { export interface ReleasesVersionFilter {
prefix?: string; prefix?: string;
precision: number; precision: number;
@@ -5,7 +7,7 @@ export interface ReleasesVersionFilter {
} }
export interface ReleasesRepository { export interface ReleasesRepository {
providerKey: string; providerKey: ProviderKey;
identifier: string; identifier: string;
name?: string; name?: string;
versionFilter?: ReleasesVersionFilter; versionFilter?: ReleasesVersionFilter;

2300
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,12 +24,12 @@
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"typescript-eslint": "^8.33.0" "typescript-eslint": "^8.33.1"
}, },
"devDependencies": { "devDependencies": {
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.27.0", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -15,7 +15,7 @@
}, },
"devDependencies": { "devDependencies": {
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"prettier-plugin-packagejson": "^2.5.14", "prettier-plugin-packagejson": "^2.5.15",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }