Compare commits
77
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6143e25686 | ||
|
|
384d83ef76 | ||
|
|
998161b1d9 | ||
|
|
a6e060265a | ||
|
|
d8b278d716 | ||
|
|
813a91cb01 | ||
|
|
990452a877 | ||
|
|
0165e2e415 | ||
|
|
ed31e32150 | ||
|
|
d07730b5f8 | ||
|
|
681236ec3b | ||
|
|
524e1a485b | ||
|
|
2bc6964f7e | ||
|
|
21de817dba | ||
|
|
b15d96e8dd | ||
|
|
514c6edc8f | ||
|
|
99949fe095 | ||
|
|
fdcfb775fa | ||
|
|
0843ccda99 | ||
|
|
2b6eb88a53 | ||
|
|
e65c3cd801 | ||
|
|
85d4931538 | ||
|
|
77a86ead41 | ||
|
|
27ca124d2a | ||
|
|
3ca6cb18eb | ||
|
|
245bab7cdd | ||
|
|
18a6bfd69d | ||
|
|
c4e6528e6c | ||
|
|
24c985424e | ||
|
|
292e36fec5 | ||
|
|
7eee48ae88 | ||
|
|
1b09b588f3 | ||
|
|
22fb696f25 | ||
|
|
60ab98ae36 | ||
|
|
9a3feb404c | ||
|
|
9c5c4f33ef | ||
|
|
3ec0c4097a | ||
|
|
e87170ae6e | ||
|
|
30fedcb6f1 | ||
|
|
6cc1209fc0 | ||
|
|
2b8fdc0281 | ||
|
|
d9cc35b985 | ||
|
|
5a418d73bd | ||
|
|
ded4e72e54 | ||
|
|
de3602da84 | ||
|
|
c80075f1c3 | ||
|
|
be52cb82fd | ||
|
|
9304d73054 | ||
|
|
912f9ff884 | ||
|
|
5e57a4906b | ||
|
|
b3db5d5f7b | ||
|
|
ebc5bd170d | ||
|
|
b4f9fb8b46 | ||
|
|
7d0c1a14fd | ||
|
|
c89e35e13c | ||
|
|
8ea6cffb53 | ||
|
|
1bb96ce5cf | ||
|
|
6da511bc2e | ||
|
|
064277373c | ||
|
|
88fd0a63b6 | ||
|
|
7fd5bf0699 | ||
|
|
1bfa9d7158 | ||
|
|
57140f2c71 | ||
|
|
a5b6203be6 | ||
|
|
aae81d3120 | ||
|
|
0fb01c90e9 | ||
|
|
06942ecbcd | ||
|
|
006dee9e9b | ||
|
|
f2df9839f1 | ||
|
|
dcc23e721a | ||
|
|
64657d831c | ||
|
|
cd124d6e83 | ||
|
|
19ecedea43 | ||
|
|
5b55a4751b | ||
|
|
266b9ffd1b | ||
|
|
7a3a05b172 | ||
|
|
a62c1385e3 |
No files matched your search
@@ -33,6 +33,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
# The below comment is used to insert a new version with on-release.yml
|
# The below comment is used to insert a new version with on-release.yml
|
||||||
#NEXT_VERSION#
|
#NEXT_VERSION#
|
||||||
|
- 1.44.0
|
||||||
- 1.43.1
|
- 1.43.1
|
||||||
- 1.43.0
|
- 1.43.0
|
||||||
- 1.42.1
|
- 1.42.1
|
||||||
|
|||||||
@@ -15,6 +15,21 @@
|
|||||||
matchUpdateTypes: ["minor", "patch", "pin", "digest"],
|
matchUpdateTypes: ["minor", "patch", "pin", "digest"],
|
||||||
automerge: true,
|
automerge: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// disallow automerge for onlyBuiltDependencies
|
||||||
|
matchPackageNames: [
|
||||||
|
"@tree-sitter-grammars/tree-sitter-yaml",
|
||||||
|
"bcrypt",
|
||||||
|
"better-sqlite3",
|
||||||
|
"cpu-features",
|
||||||
|
"esbuild",
|
||||||
|
"sharp",
|
||||||
|
"ssh2",
|
||||||
|
"tree-sitter",
|
||||||
|
"tree-sitter-json",
|
||||||
|
],
|
||||||
|
automerge: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
updateInternalDeps: true,
|
updateInternalDeps: true,
|
||||||
rangeStrategy: "bump",
|
rangeStrategy: "bump",
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
name: "[Quality] Code Analysis"
|
name: "[Quality] Code Analysis"
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
name: "[Crowdin] Upload translations"
|
name: "[Crowdin] Upload translations"
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v6
|
||||||
if: env.SKIP_RELEASE == 'false'
|
if: env.SKIP_RELEASE == 'false'
|
||||||
with:
|
with:
|
||||||
node-version: 24.11.0
|
node-version: 24.11.1
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: npm i -g pnpm
|
- run: npm i -g pnpm
|
||||||
if: env.SKIP_RELEASE == 'false'
|
if: env.SKIP_RELEASE == 'false'
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ env:
|
|||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
CROWDIN_TOKEN: "${{ secrets.CROWDIN_UPDATE_CONTRIBUTORS_TOKEN }}"
|
CROWDIN_TOKEN: "${{ secrets.CROWDIN_UPDATE_CONTRIBUTORS_TOKEN }}"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
jobs:
|
jobs:
|
||||||
update-contributors:
|
update-contributors:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -3,3 +3,4 @@
|
|||||||
# @link https://docs.expo.dev/guides/monorepos/#common-issues
|
# @link https://docs.expo.dev/guides/monorepos/#common-issues
|
||||||
node-linker=hoisted
|
node-linker=hoisted
|
||||||
strict-peer-dependencies=false
|
strict-peer-dependencies=false
|
||||||
|
engine-strict=true
|
||||||
@@ -1,3 +1,44 @@
|
|||||||
|
## [1.45.0](https://github.com/homarr-labs/homarr/compare/v1.44.0...v1.45.0) (2025-11-28)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **docker:** hide containers with label homarr.hide ([#4506](https://github.com/homarr-labs/homarr/issues/4506)) ([60ab98a](https://github.com/homarr-labs/homarr/commit/60ab98ae36abe712c825f7828394d3e3e98c888a))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Add fallback when no icon in Minecraft Server widget ([#4544](https://github.com/homarr-labs/homarr/issues/4544)) ([#4545](https://github.com/homarr-labs/homarr/issues/4545)) ([99949fe](https://github.com/homarr-labs/homarr/commit/99949fe095ac5a3616c8e9c1964d7eb253c9f826))
|
||||||
|
* **deps:** update dependency @tanstack/react-query to ^5.90.10 ([#4523](https://github.com/homarr-labs/homarr/issues/4523)) ([e87170a](https://github.com/homarr-labs/homarr/commit/e87170ae6e56cdbeef52e943353060329c39bdc9))
|
||||||
|
* **deps:** update dependency glob to ^11.1.0 ([#4529](https://github.com/homarr-labs/homarr/issues/4529)) ([24c9854](https://github.com/homarr-labs/homarr/commit/24c985424ecc795eb96b56881a9b00c0c37386c1))
|
||||||
|
* **deps:** update dependency glob to v12 ([#4530](https://github.com/homarr-labs/homarr/issues/4530)) ([18a6bfd](https://github.com/homarr-labs/homarr/commit/18a6bfd69d86de8c58dbfe6dc8b8fab78b2b5ee6))
|
||||||
|
* **deps:** update dependency next-intl to v4.5.4 ([#4540](https://github.com/homarr-labs/homarr/issues/4540)) ([77a86ea](https://github.com/homarr-labs/homarr/commit/77a86ead41c0d098987561249203667ac4d88177))
|
||||||
|
* **deps:** update dependency next-intl to v4.5.5 ([#4542](https://github.com/homarr-labs/homarr/issues/4542)) ([2bc6964](https://github.com/homarr-labs/homarr/commit/2bc6964f7efb5203f626d2f07b847dffd53191f0))
|
||||||
|
* **deps:** update dependency sass to ^1.94.1 ([#4531](https://github.com/homarr-labs/homarr/issues/4531)) ([292e36f](https://github.com/homarr-labs/homarr/commit/292e36fec5acff9b1d143f19e18ea170c87fa0d0))
|
||||||
|
* **deps:** update dependency zod-validation-error to v5 ([#4437](https://github.com/homarr-labs/homarr/issues/4437)) ([9a3feb4](https://github.com/homarr-labs/homarr/commit/9a3feb404c0c284b4f33fce12d87a469741a6ebd))
|
||||||
|
* **security:** restrict modification of entrypoint script ([#4555](https://github.com/homarr-labs/homarr/issues/4555)) ([524e1a4](https://github.com/homarr-labs/homarr/commit/524e1a485be56dbebf8f71d4546bc65672737f30))
|
||||||
|
|
||||||
|
## [1.44.0](https://github.com/homarr-labs/homarr/compare/v1.43.3...v1.44.0) (2025-11-21)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **pi-hole:** support no-auth ([#4480](https://github.com/homarr-labs/homarr/issues/4480)) ([c89e35e](https://github.com/homarr-labs/homarr/commit/c89e35e13c794bbdf98748c00644b86f7f5dbe68))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deps:** update dependency @tanstack/react-query to ^5.90.8 ([#4494](https://github.com/homarr-labs/homarr/issues/4494)) ([a5b6203](https://github.com/homarr-labs/homarr/commit/a5b6203be6ac3c2308614f10ef6a17590917e2c9))
|
||||||
|
* **deps:** update dependency @tanstack/react-query to ^5.90.9 ([#4508](https://github.com/homarr-labs/homarr/issues/4508)) ([b3db5d5](https://github.com/homarr-labs/homarr/commit/b3db5d5f7bc2d146f0b2c230a52e7bd4efad3bcb))
|
||||||
|
* **deps:** update dependency @tanstack/react-query-next-experimental to ^5.91.0 ([#4505](https://github.com/homarr-labs/homarr/issues/4505)) ([8ea6cff](https://github.com/homarr-labs/homarr/commit/8ea6cffb537ca54a413c583ab8b8fd24a52185be))
|
||||||
|
* **deps:** update dependency drizzle-kit to ^0.31.7 ([#4507](https://github.com/homarr-labs/homarr/issues/4507)) ([b4f9fb8](https://github.com/homarr-labs/homarr/commit/b4f9fb8b46a8f74e2d7896dfe98e80618cfbcde4))
|
||||||
|
* **deps:** update dependency fast-xml-parser to ^5.3.2 ([#4510](https://github.com/homarr-labs/homarr/issues/4510)) ([5e57a49](https://github.com/homarr-labs/homarr/commit/5e57a4906bcedc53d220e983827293f4a699c425))
|
||||||
|
* **deps:** update dependency next-intl to v4.5.1 ([#4484](https://github.com/homarr-labs/homarr/issues/4484)) ([06942ec](https://github.com/homarr-labs/homarr/commit/06942ecbcd245f5540f2960f437ca2955a5ed0c3))
|
||||||
|
* **deps:** update dependency next-intl to v4.5.3 ([#4503](https://github.com/homarr-labs/homarr/issues/4503)) ([0642773](https://github.com/homarr-labs/homarr/commit/064277373cacd3309d8da3779d5451d195da7f81))
|
||||||
|
* **deps:** update dependency sass to ^1.94.0 ([#4482](https://github.com/homarr-labs/homarr/issues/4482)) ([19ecede](https://github.com/homarr-labs/homarr/commit/19ecedea4370924d47545ca8d9d46a0b7c016cc9))
|
||||||
|
* **deps:** update mantine monorepo to ^8.3.8 ([#4516](https://github.com/homarr-labs/homarr/issues/4516)) ([ded4e72](https://github.com/homarr-labs/homarr/commit/ded4e72e5407b0076495a06111f3b04445f27276))
|
||||||
|
* **deps:** update nextjs monorepo to v16.0.3 ([#4504](https://github.com/homarr-labs/homarr/issues/4504)) ([7d0c1a1](https://github.com/homarr-labs/homarr/commit/7d0c1a14fdf1731ff44e3d00793cf4cd1c3e9b1f))
|
||||||
|
* **deps:** update testcontainers-node monorepo to ^11.8.1 ([#4486](https://github.com/homarr-labs/homarr/issues/4486)) ([64657d8](https://github.com/homarr-labs/homarr/commit/64657d831cbe28a59708b280b07889b9f0232743))
|
||||||
|
* **deps:** update turbo monorepo to ^2.6.1 ([#4483](https://github.com/homarr-labs/homarr/issues/4483)) ([cd124d6](https://github.com/homarr-labs/homarr/commit/cd124d6e8392eef5df0092481ec3cc26599f2902))
|
||||||
|
* **iframe:** allow clicking links within ([#4521](https://github.com/homarr-labs/homarr/issues/4521)) ([5a418d7](https://github.com/homarr-labs/homarr/commit/5a418d73bdb07cf3d3270983b6881f2de8d58ad7))
|
||||||
|
* **media-requests:** incorrect availability mapping ([#4520](https://github.com/homarr-labs/homarr/issues/4520)) ([d9cc35b](https://github.com/homarr-labs/homarr/commit/d9cc35b9859a14f44c4e4b13c6518287f8d18e54))
|
||||||
|
|
||||||
## [1.43.3](https://github.com/homarr-labs/homarr/compare/v1.43.2...v1.43.3) (2025-11-14)
|
## [1.43.3](https://github.com/homarr-labs/homarr/compare/v1.43.2...v1.43.3) (2025-11-14)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
FROM node:24.11.0-alpine AS base
|
FROM node:24.11.1-alpine AS base
|
||||||
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
@@ -55,7 +55,7 @@ COPY --from=builder /app/apps/nextjs/.next/standalone ./
|
|||||||
COPY --from=builder /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
|
COPY --from=builder /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
|
||||||
COPY --from=builder /app/apps/nextjs/public ./apps/nextjs/public
|
COPY --from=builder /app/apps/nextjs/public ./apps/nextjs/public
|
||||||
COPY scripts/run.sh ./run.sh
|
COPY scripts/run.sh ./run.sh
|
||||||
COPY --chmod=777 scripts/entrypoint.sh ./entrypoint.sh
|
COPY --chmod=755 scripts/entrypoint.sh ./entrypoint.sh
|
||||||
COPY packages/redis/redis.conf /app/redis.conf
|
COPY packages/redis/redis.conf /app/redis.conf
|
||||||
COPY nginx.conf /etc/nginx/templates/nginx.conf
|
COPY nginx.conf /etc/nginx/templates/nginx.conf
|
||||||
|
|
||||||
|
|||||||
+14
-14
@@ -50,17 +50,17 @@
|
|||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@homarr/widgets": "workspace:^0.1.0",
|
"@homarr/widgets": "workspace:^0.1.0",
|
||||||
"@mantine/colors-generator": "^8.3.7",
|
"@mantine/colors-generator": "^8.3.8",
|
||||||
"@mantine/core": "^8.3.7",
|
"@mantine/core": "^8.3.8",
|
||||||
"@mantine/dropzone": "^8.3.7",
|
"@mantine/dropzone": "^8.3.8",
|
||||||
"@mantine/hooks": "^8.3.7",
|
"@mantine/hooks": "^8.3.8",
|
||||||
"@mantine/modals": "^8.3.7",
|
"@mantine/modals": "^8.3.8",
|
||||||
"@mantine/tiptap": "^8.3.7",
|
"@mantine/tiptap": "^8.3.8",
|
||||||
"@million/lint": "1.0.14",
|
"@million/lint": "1.0.14",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"@tanstack/react-query": "^5.90.7",
|
"@tanstack/react-query": "^5.90.10",
|
||||||
"@tanstack/react-query-devtools": "^5.90.2",
|
"@tanstack/react-query-devtools": "^5.90.2",
|
||||||
"@tanstack/react-query-next-experimental": "^5.90.2",
|
"@tanstack/react-query-next-experimental": "^5.91.0",
|
||||||
"@trpc/client": "^11.7.1",
|
"@trpc/client": "^11.7.1",
|
||||||
"@trpc/next": "^11.7.1",
|
"@trpc/next": "^11.7.1",
|
||||||
"@trpc/react-query": "^11.7.1",
|
"@trpc/react-query": "^11.7.1",
|
||||||
@@ -74,18 +74,18 @@
|
|||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"flag-icons": "^7.5.0",
|
"flag-icons": "^7.5.0",
|
||||||
"glob": "^11.0.3",
|
"glob": "^12.0.0",
|
||||||
"isomorphic-dompurify": "^2.32.0",
|
"isomorphic-dompurify": "^2.32.0",
|
||||||
"jotai": "^2.15.1",
|
"jotai": "^2.15.1",
|
||||||
"mantine-react-table": "2.0.0-beta.9",
|
"mantine-react-table": "2.0.0-beta.9",
|
||||||
"next": "16.0.1",
|
"next": "16.0.3",
|
||||||
"postcss-preset-mantine": "^1.18.0",
|
"postcss-preset-mantine": "^1.18.0",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.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.93.3",
|
"sass": "^1.94.1",
|
||||||
"superjson": "2.2.5",
|
"superjson": "2.2.5",
|
||||||
"swagger-ui-react": "^5.30.2",
|
"swagger-ui-react": "^5.30.2",
|
||||||
"use-deep-compare-effect": "^1.8.1",
|
"use-deep-compare-effect": "^1.8.1",
|
||||||
@@ -96,10 +96,10 @@
|
|||||||
"@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.2",
|
"@types/chroma-js": "3.1.2",
|
||||||
"@types/node": "^24.10.0",
|
"@types/node": "^24.10.1",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
"@types/react": "19.2.2",
|
"@types/react": "19.2.6",
|
||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/swagger-ui-react": "^5.18.0",
|
"@types/swagger-ui-react": "^5.18.0",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
IconSettings,
|
IconSettings,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { applyAutoLayoutToBoard, clientApi } from "@homarr/api/client";
|
||||||
import { useSession } from "@homarr/auth/client";
|
import { useSession } from "@homarr/auth/client";
|
||||||
import { useRequiredBoard } from "@homarr/boards/context";
|
import { useRequiredBoard } from "@homarr/boards/context";
|
||||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||||
@@ -165,7 +165,7 @@ const EditModeMenu = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const toggle = useCallback(() => {
|
const toggle = useCallback(() => {
|
||||||
if (isEditMode) return saveBoard(board);
|
if (isEditMode) return saveBoard(applyAutoLayoutToBoard(board));
|
||||||
open();
|
open();
|
||||||
}, [board, isEditMode, saveBoard, open]);
|
}, [board, isEditMode, saveBoard, open]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,31 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, Fieldset, Grid, Group, Input, NumberInput, Slider, Stack, Text, TextInput } from "@mantine/core";
|
import {
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
ChipGroup,
|
||||||
|
Fieldset,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
Input,
|
||||||
|
InputWrapper,
|
||||||
|
NumberInput,
|
||||||
|
RadioGroup,
|
||||||
|
SimpleGrid,
|
||||||
|
Slider,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
} from "@mantine/core";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
|
import { boardLayoutModes } from "@homarr/definitions";
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
import { CustomRadioCard } from "@homarr/ui";
|
||||||
import { boardSaveLayoutsSchema } from "@homarr/validation/board";
|
import { boardSaveLayoutsSchema } from "@homarr/validation/board";
|
||||||
|
|
||||||
import type { Board } from "../../_types";
|
import type { Board } from "../../_types";
|
||||||
@@ -25,6 +45,8 @@ export const LayoutSettingsContent = ({ board }: Props) => {
|
|||||||
const form = useZodForm(boardSaveLayoutsSchema.omit({ id: true }).required(), {
|
const form = useZodForm(boardSaveLayoutsSchema.omit({ id: true }).required(), {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
layouts: board.layouts,
|
layouts: board.layouts,
|
||||||
|
layoutMode: board.layoutMode,
|
||||||
|
baseLayoutId: board.baseLayoutId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,6 +60,53 @@ export const LayoutSettingsContent = ({ board }: Props) => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
|
<RadioGroup {...form.getInputProps("layoutMode")} label={t("board.field.layoutMode.label")}>
|
||||||
|
<SimpleGrid cols={{ sm: 2, xs: 1 }} spacing="md">
|
||||||
|
{boardLayoutModes.values.map((layoutMode) => (
|
||||||
|
<Box w="100%" key={layoutMode} pos="relative">
|
||||||
|
<CustomRadioCard
|
||||||
|
value={layoutMode}
|
||||||
|
label={t(`board.field.layoutMode.option.${layoutMode}.label`)}
|
||||||
|
description={t(`board.field.layoutMode.option.${layoutMode}.description`)}
|
||||||
|
/>
|
||||||
|
{layoutMode === boardLayoutModes.defaultValue && (
|
||||||
|
<Badge pos="absolute" top="8px" right="8px" variant="light" size="sm">
|
||||||
|
{t("common.select.badge.recommended")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
{form.values.layoutMode === "auto" && (
|
||||||
|
<InputWrapper label={t("board.field.baseLayout.label")} description={t("board.field.baseLayout.description")}>
|
||||||
|
<ChipGroup {...form.getInputProps("baseLayoutId")} multiple={false}>
|
||||||
|
<SimpleGrid
|
||||||
|
cols={{
|
||||||
|
lg: Math.min(6, form.values.layouts.length),
|
||||||
|
sm: Math.min(4, form.values.layouts.length),
|
||||||
|
xs: Math.min(2, form.values.layouts.length),
|
||||||
|
}}
|
||||||
|
spacing="md"
|
||||||
|
mt="sm"
|
||||||
|
>
|
||||||
|
{form.values.layouts.map((layout) => (
|
||||||
|
<Chip
|
||||||
|
key={layout.id}
|
||||||
|
value={layout.id}
|
||||||
|
w="100%"
|
||||||
|
size="sm"
|
||||||
|
styles={{ label: { width: "100%", justifyContent: "center" } }}
|
||||||
|
>
|
||||||
|
{layout.name}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</ChipGroup>
|
||||||
|
</InputWrapper>
|
||||||
|
)}
|
||||||
|
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
<Text fw={500}>{t("board.setting.section.layout.responsive.title")}</Text>
|
<Text fw={500}>{t("board.setting.section.layout.responsive.title")}</Text>
|
||||||
@@ -82,18 +151,21 @@ export const LayoutSettingsContent = ({ board }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
{form.values.layouts.length >= 2 && (
|
{form.values.layouts.length >= 2 && form.values.baseLayoutId !== layout.id && (
|
||||||
<Group justify="end">
|
<Group justify="end">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
form.setValues((previous) =>
|
form.setValues((previous) => {
|
||||||
previous.layouts !== undefined && previous.layouts.length >= 2
|
if (!previous.layouts) return previous;
|
||||||
? {
|
if (previous.layouts.length < 2) return previous;
|
||||||
layouts: form.values.layouts.filter((filteredLayout) => filteredLayout.id !== layout.id),
|
if (previous.baseLayoutId === layout.id) return previous;
|
||||||
}
|
|
||||||
: previous,
|
return {
|
||||||
);
|
...previous,
|
||||||
|
layouts: previous.layouts.filter((filteredLayout) => filteredLayout.id !== layout.id),
|
||||||
|
};
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("common.action.remove")}
|
{t("common.action.remove")}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export class BoardMockBuilder {
|
|||||||
private readonly board: Board;
|
private readonly board: Board;
|
||||||
|
|
||||||
constructor(board?: Partial<Omit<Board, "groupPermissions" | "userPermissions" | "sections" | "items" | "layouts">>) {
|
constructor(board?: Partial<Omit<Board, "groupPermissions" | "userPermissions" | "sections" | "items" | "layouts">>) {
|
||||||
|
const baseLayoutId = createId();
|
||||||
this.board = {
|
this.board = {
|
||||||
id: createId(),
|
id: createId(),
|
||||||
backgroundImageRepeat: "no-repeat",
|
backgroundImageRepeat: "no-repeat",
|
||||||
@@ -41,12 +42,14 @@ export class BoardMockBuilder {
|
|||||||
items: [],
|
items: [],
|
||||||
layouts: [
|
layouts: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
id: baseLayoutId,
|
||||||
name: "Base",
|
name: "Base",
|
||||||
columnCount: 12,
|
columnCount: 12,
|
||||||
breakpoint: 0,
|
breakpoint: 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
baseLayoutId,
|
||||||
|
layoutMode: "auto",
|
||||||
...board,
|
...board,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,9 +47,9 @@
|
|||||||
"@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": "^24.10.0",
|
"@types/node": "^24.10.1",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"esbuild": "^0.26.0",
|
"esbuild": "^0.27.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"tsx": "4.20.4",
|
"tsx": "4.20.4",
|
||||||
|
|||||||
@@ -34,7 +34,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/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"esbuild": "^0.26.0",
|
"esbuild": "^0.27.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
|
|||||||
+19
-17
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"version": "1.43.3",
|
"version": "1.45.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env CI=true turbo build",
|
"build": "cross-env CI=true turbo build",
|
||||||
@@ -40,27 +40,29 @@
|
|||||||
"@semantic-release/commit-analyzer": "^13.0.1",
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
||||||
"@semantic-release/git": "^10.0.1",
|
"@semantic-release/git": "^10.0.1",
|
||||||
"@semantic-release/github": "^12.0.2",
|
"@semantic-release/github": "^12.0.2",
|
||||||
"@semantic-release/npm": "^13.1.1",
|
"@semantic-release/npm": "^13.1.2",
|
||||||
"@semantic-release/release-notes-generator": "^14.1.0",
|
"@semantic-release/release-notes-generator": "^14.1.0",
|
||||||
"@testcontainers/redis": "^11.8.0",
|
"@testcontainers/redis": "^11.8.1",
|
||||||
"@turbo/gen": "^2.6.0",
|
"@turbo/gen": "^2.6.1",
|
||||||
"@vitejs/plugin-react": "^5.1.0",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"@vitest/coverage-v8": "^4.0.8",
|
"@vitest/coverage-v8": "^4.0.10",
|
||||||
"@vitest/ui": "^4.0.8",
|
"@vitest/ui": "^4.0.10",
|
||||||
"conventional-changelog-conventionalcommits": "^9.1.0",
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"jsdom": "^27.1.0",
|
"jsdom": "^27.2.0",
|
||||||
|
"json5": "^2.2.3",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"semantic-release": "^25.0.2",
|
"semantic-release": "^25.0.2",
|
||||||
"testcontainers": "^11.8.0",
|
"testcontainers": "^11.8.1",
|
||||||
"turbo": "^2.6.0",
|
"turbo": "^2.6.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^4.0.8"
|
"vitest": "^4.0.10"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.20.0",
|
"packageManager": "pnpm@10.22.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.11.0"
|
"node": ">=24.11.1",
|
||||||
|
"pnpm": ">=10.22.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
@@ -80,15 +82,15 @@
|
|||||||
"axios@>=1.0.0 <1.8.2": ">=1.13.2",
|
"axios@>=1.0.0 <1.8.2": ">=1.13.2",
|
||||||
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
|
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
|
||||||
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
|
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
|
||||||
"esbuild@<=0.24.2": ">=0.26.0",
|
"esbuild@<=0.24.2": ">=0.27.0",
|
||||||
"form-data@>=4.0.0 <4.0.4": ">=4.0.4",
|
"form-data@>=4.0.0 <4.0.4": ">=4.0.5",
|
||||||
"hono@<4.6.5": ">=4.10.4",
|
"hono@<4.6.5": ">=4.10.6",
|
||||||
"linkifyjs@<4.3.2": ">=4.3.2",
|
"linkifyjs@<4.3.2": ">=4.3.2",
|
||||||
"nanoid@>=4.0.0 <5.0.9": ">=5.1.6",
|
"nanoid@>=4.0.0 <5.0.9": ">=5.1.6",
|
||||||
"prismjs@<1.30.0": ">=1.30.0",
|
"prismjs@<1.30.0": ">=1.30.0",
|
||||||
"proxmox-api>undici": "7.16.0",
|
"proxmox-api>undici": "7.16.0",
|
||||||
"react-is": "^19.2.0",
|
"react-is": "^19.2.0",
|
||||||
"rollup@>=4.0.0 <4.22.4": ">=4.53.1",
|
"rollup@>=4.0.0 <4.22.4": ">=4.53.3",
|
||||||
"sha.js@<=2.4.11": ">=2.4.12",
|
"sha.js@<=2.4.11": ">=2.4.12",
|
||||||
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
|
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
|
||||||
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
|
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
|
||||||
|
|||||||
@@ -43,13 +43,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",
|
||||||
"@kubernetes/client-node": "^1.4.0",
|
"@kubernetes/client-node": "^1.4.0",
|
||||||
"@tanstack/react-query": "^5.90.7",
|
"@tanstack/react-query": "^5.90.10",
|
||||||
"@trpc/client": "^11.7.1",
|
"@trpc/client": "^11.7.1",
|
||||||
"@trpc/react-query": "^11.7.1",
|
"@trpc/react-query": "^11.7.1",
|
||||||
"@trpc/server": "^11.7.1",
|
"@trpc/server": "^11.7.1",
|
||||||
"@trpc/tanstack-react-query": "^11.7.1",
|
"@trpc/tanstack-react-query": "^11.7.1",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"next": "16.0.1",
|
"next": "16.0.3",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"superjson": "2.2.5",
|
"superjson": "2.2.5",
|
||||||
|
|||||||
@@ -17,3 +17,5 @@ export const fetchApi = createTRPCClient<AppRouter>({
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export { applyAutoLayoutToBoard } from "./router/board/board-layout";
|
||||||
@@ -5,7 +5,7 @@ import { z } from "zod/v4";
|
|||||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
import type { DeviceType } from "@homarr/common/server";
|
import type { DeviceType } from "@homarr/common/server";
|
||||||
import type { Database, InferInsertModel, InferSelectModel, SQL } from "@homarr/db";
|
import type { Database, InferInsertModel, InferSelectModel } from "@homarr/db";
|
||||||
import { and, asc, eq, handleTransactionsAsync, inArray, isNull, like, not, or, sql } from "@homarr/db";
|
import { and, asc, eq, handleTransactionsAsync, inArray, isNull, like, not, or, sql } from "@homarr/db";
|
||||||
import { createDbInsertCollectionWithoutTransaction } from "@homarr/db/collection";
|
import { createDbInsertCollectionWithoutTransaction } from "@homarr/db/collection";
|
||||||
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||||
@@ -27,13 +27,11 @@ import {
|
|||||||
sections,
|
sections,
|
||||||
users,
|
users,
|
||||||
} from "@homarr/db/schema";
|
} from "@homarr/db/schema";
|
||||||
import type { WidgetKind } from "@homarr/definitions";
|
|
||||||
import {
|
import {
|
||||||
emptySuperJSON,
|
emptySuperJSON,
|
||||||
everyoneGroup,
|
everyoneGroup,
|
||||||
getPermissionsWithChildren,
|
getPermissionsWithChildren,
|
||||||
getPermissionsWithParents,
|
getPermissionsWithParents,
|
||||||
widgetKinds,
|
|
||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
import { importOldmarrAsync } from "@homarr/old-import";
|
import { importOldmarrAsync } from "@homarr/old-import";
|
||||||
import { importJsonFileSchema } from "@homarr/old-import/shared";
|
import { importJsonFileSchema } from "@homarr/old-import/shared";
|
||||||
@@ -50,13 +48,11 @@ import {
|
|||||||
boardSaveSchema,
|
boardSaveSchema,
|
||||||
} from "@homarr/validation/board";
|
} from "@homarr/validation/board";
|
||||||
import { byIdSchema } from "@homarr/validation/common";
|
import { byIdSchema } from "@homarr/validation/common";
|
||||||
import { zodUnionFromArray } from "@homarr/validation/enums";
|
|
||||||
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
|
|
||||||
import { sectionSchema, sharedItemSchema } from "@homarr/validation/shared";
|
|
||||||
|
|
||||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
|
||||||
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
||||||
import { generateResponsiveGridFor } from "./board/grid-algorithm";
|
import { applyAutoLayoutToBoard, getUpdatedBoardLayout } from "./board/board-layout";
|
||||||
|
import { getFullBoardWithWhereAsync } from "./board/full-board-query";
|
||||||
|
|
||||||
export const boardRouter = createTRPCRouter({
|
export const boardRouter = createTRPCRouter({
|
||||||
exists: permissionRequiredProcedure
|
exists: permissionRequiredProcedure
|
||||||
@@ -279,6 +275,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
name: input.name,
|
name: input.name,
|
||||||
isPublic: input.isPublic,
|
isPublic: input.isPublic,
|
||||||
creatorId: ctx.session.user.id,
|
creatorId: ctx.session.user.id,
|
||||||
|
layoutMode: input.layoutMode,
|
||||||
});
|
});
|
||||||
createBoardCollection.sections.push({
|
createBoardCollection.sections.push({
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -287,8 +284,9 @@ export const boardRouter = createTRPCRouter({
|
|||||||
yOffset: 0,
|
yOffset: 0,
|
||||||
boardId,
|
boardId,
|
||||||
});
|
});
|
||||||
|
const baseLayoutId = createId();
|
||||||
createBoardCollection.layouts.push({
|
createBoardCollection.layouts.push({
|
||||||
id: createId(),
|
id: baseLayoutId,
|
||||||
name: "Base",
|
name: "Base",
|
||||||
columnCount: input.columnCount,
|
columnCount: input.columnCount,
|
||||||
breakpoint: 0,
|
breakpoint: 0,
|
||||||
@@ -296,6 +294,10 @@ export const boardRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
await createBoardCollection.insertAllAsync(ctx.db);
|
await createBoardCollection.insertAllAsync(ctx.db);
|
||||||
|
// Define the base layout (only for automatic layout mode)
|
||||||
|
if (input.layoutMode === "auto") {
|
||||||
|
await ctx.db.update(boards).set({ baseLayoutId }).where(eq(boards.id, boardId));
|
||||||
|
}
|
||||||
|
|
||||||
if (!user?.homeBoardId) {
|
if (!user?.homeBoardId) {
|
||||||
await ctx.db.update(users).set({ homeBoardId: boardId }).where(eq(users.id, ctx.session.user.id));
|
await ctx.db.update(users).set({ homeBoardId: boardId }).where(eq(users.id, ctx.session.user.id));
|
||||||
@@ -582,13 +584,15 @@ export const boardRouter = createTRPCRouter({
|
|||||||
|
|
||||||
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
|
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
|
||||||
|
|
||||||
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
const board = await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||||
|
return applyAutoLayoutToBoard(board);
|
||||||
}),
|
}),
|
||||||
getBoardByName: publicProcedure.input(boardByNameSchema).query(async ({ input, ctx }) => {
|
getBoardByName: publicProcedure.input(boardByNameSchema).query(async ({ input, ctx }) => {
|
||||||
const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase());
|
const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase());
|
||||||
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
|
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
|
||||||
|
|
||||||
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
const board = await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||||
|
return applyAutoLayoutToBoard(board);
|
||||||
}),
|
}),
|
||||||
saveLayouts: protectedProcedure.input(boardSaveLayoutsSchema).mutation(async ({ ctx, input }) => {
|
saveLayouts: protectedProcedure.input(boardSaveLayoutsSchema).mutation(async ({ ctx, input }) => {
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
||||||
@@ -596,13 +600,32 @@ export const boardRouter = createTRPCRouter({
|
|||||||
const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
|
const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
|
||||||
|
|
||||||
const addedLayouts = filterAddedItems(input.layouts, board.layouts);
|
const addedLayouts = filterAddedItems(input.layouts, board.layouts);
|
||||||
|
const removedLayouts = filterRemovedItems(input.layouts, board.layouts);
|
||||||
|
|
||||||
|
if (input.layoutMode === "auto" && input.baseLayoutId === null) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Base layout must be set when using automatic layout mode",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedLayouts.some((layout) => layout.id === input.baseLayoutId)) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Cannot remove base layout",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const layoutsToInsert: InferInsertModel<typeof layouts>[] = [];
|
const layoutsToInsert: InferInsertModel<typeof layouts>[] = [];
|
||||||
const itemSectionLayoutsToInsert: InferInsertModel<typeof itemLayouts>[] = [];
|
const itemSectionLayoutsToInsert: InferInsertModel<typeof itemLayouts>[] = [];
|
||||||
const sectionLayoutsToInsert: InferInsertModel<typeof sectionLayouts>[] = [];
|
const sectionLayoutsToInsert: InferInsertModel<typeof sectionLayouts>[] = [];
|
||||||
|
|
||||||
|
let baseLayoutId = input.baseLayoutId;
|
||||||
for (const addedLayout of addedLayouts) {
|
for (const addedLayout of addedLayouts) {
|
||||||
const layoutId = createId();
|
const layoutId = createId();
|
||||||
|
if (addedLayout.id === input.baseLayoutId) {
|
||||||
|
baseLayoutId = layoutId;
|
||||||
|
}
|
||||||
|
|
||||||
layoutsToInsert.push({
|
layoutsToInsert.push({
|
||||||
id: layoutId,
|
id: layoutId,
|
||||||
@@ -709,11 +732,18 @@ export const boardRouter = createTRPCRouter({
|
|||||||
.where(eq(layouts.id, updatedLayout.id));
|
.where(eq(layouts.id, updatedLayout.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
const removedLayouts = filterRemovedItems(input.layouts, board.layouts);
|
|
||||||
const removedLayoutIds = removedLayouts.map((layout) => layout.id);
|
const removedLayoutIds = removedLayouts.map((layout) => layout.id);
|
||||||
if (removedLayoutIds.length > 0) {
|
if (removedLayoutIds.length > 0) {
|
||||||
await ctx.db.delete(layouts).where(inArray(layouts.id, removedLayoutIds));
|
await ctx.db.delete(layouts).where(inArray(layouts.id, removedLayoutIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ctx.db
|
||||||
|
.update(boards)
|
||||||
|
.set({
|
||||||
|
baseLayoutId,
|
||||||
|
layoutMode: input.layoutMode,
|
||||||
|
})
|
||||||
|
.where(eq(boards.id, input.id));
|
||||||
}),
|
}),
|
||||||
savePartialBoardSettings: protectedProcedure
|
savePartialBoardSettings: protectedProcedure
|
||||||
.input(boardSavePartialSettingsSchema.and(z.object({ id: z.string() })))
|
.input(boardSavePartialSettingsSchema.and(z.object({ id: z.string() })))
|
||||||
@@ -1408,244 +1438,6 @@ const noBoardWithSimilarNameAsync = async (db: Database, name: string, ignoredId
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUpdatedBoardLayout = (
|
|
||||||
board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>,
|
|
||||||
options: {
|
|
||||||
previous: {
|
|
||||||
layoutId: string;
|
|
||||||
columnCount: number;
|
|
||||||
};
|
|
||||||
current: {
|
|
||||||
layoutId: string;
|
|
||||||
columnCount: number;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
const itemSectionLayoutsCollection: InferInsertModel<typeof itemLayouts>[] = [];
|
|
||||||
const sectionLayoutsCollection: InferInsertModel<typeof sectionLayouts>[] = [];
|
|
||||||
|
|
||||||
const elements = getElementsForLayout(board, options.previous.layoutId);
|
|
||||||
const rootSections = board.sections.filter((section) => section.kind !== "dynamic");
|
|
||||||
|
|
||||||
for (const rootSection of rootSections) {
|
|
||||||
const result = generateResponsiveGridFor({
|
|
||||||
items: elements,
|
|
||||||
previousWidth: options.previous.columnCount,
|
|
||||||
width: options.current.columnCount,
|
|
||||||
sectionId: rootSection.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
itemSectionLayoutsCollection.push(
|
|
||||||
...board.items
|
|
||||||
.map((item): InferInsertModel<typeof itemLayouts> | null => {
|
|
||||||
const currentElement = result.items.find((element) => element.type === "item" && element.id === item.id);
|
|
||||||
|
|
||||||
if (!currentElement) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
itemId: item.id,
|
|
||||||
layoutId: options.current.layoutId,
|
|
||||||
sectionId: currentElement.sectionId,
|
|
||||||
height: currentElement.height,
|
|
||||||
width: currentElement.width,
|
|
||||||
xOffset: currentElement.xOffset,
|
|
||||||
yOffset: currentElement.yOffset,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((item) => item !== null),
|
|
||||||
);
|
|
||||||
|
|
||||||
sectionLayoutsCollection.push(
|
|
||||||
...board.sections
|
|
||||||
.filter((section) => section.kind === "dynamic")
|
|
||||||
.map((section): InferInsertModel<typeof sectionLayouts> | null => {
|
|
||||||
const currentElement = result.items.find(
|
|
||||||
(element) => element.type === "section" && element.id === section.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!currentElement) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
layoutId: options.current.layoutId,
|
|
||||||
sectionId: section.id,
|
|
||||||
parentSectionId: currentElement.sectionId,
|
|
||||||
height: currentElement.height,
|
|
||||||
width: currentElement.width,
|
|
||||||
xOffset: currentElement.xOffset,
|
|
||||||
yOffset: currentElement.yOffset,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((section) => section !== null),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
itemSectionLayouts: itemSectionLayoutsCollection,
|
|
||||||
sectionLayouts: sectionLayoutsCollection,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getElementsForLayout = (board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>, layoutId: string) => {
|
|
||||||
const sectionElements = board.sections
|
|
||||||
.filter((section) => section.kind === "dynamic")
|
|
||||||
.map((section) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const clonedLayout = section.layouts.find((sectionLayout) => sectionLayout.layoutId === layoutId)!;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: section.id,
|
|
||||||
type: "section" as const,
|
|
||||||
height: clonedLayout.height,
|
|
||||||
width: clonedLayout.width,
|
|
||||||
xOffset: clonedLayout.xOffset,
|
|
||||||
yOffset: clonedLayout.yOffset,
|
|
||||||
sectionId: clonedLayout.parentSectionId,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const itemElements = board.items.map((item) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const clonedLayout = item.layouts.find((itemLayout) => itemLayout.layoutId === layoutId)!;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: item.id,
|
|
||||||
type: "item" as const,
|
|
||||||
height: clonedLayout.height,
|
|
||||||
width: clonedLayout.width,
|
|
||||||
xOffset: clonedLayout.xOffset,
|
|
||||||
yOffset: clonedLayout.yOffset,
|
|
||||||
sectionId: clonedLayout.sectionId,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return [...itemElements, ...sectionElements];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, userId: string | null) => {
|
|
||||||
const groupsOfCurrentUser = await db.query.groupMembers.findMany({
|
|
||||||
where: eq(groupMembers.userId, userId ?? ""),
|
|
||||||
});
|
|
||||||
const board = await db.query.boards.findFirst({
|
|
||||||
where,
|
|
||||||
with: {
|
|
||||||
creator: {
|
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
image: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sections: {
|
|
||||||
with: {
|
|
||||||
collapseStates: {
|
|
||||||
where: eq(sectionCollapseStates.userId, userId ?? ""),
|
|
||||||
},
|
|
||||||
layouts: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
items: {
|
|
||||||
with: {
|
|
||||||
integrations: {
|
|
||||||
with: {
|
|
||||||
integration: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
layouts: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
layouts: true,
|
|
||||||
userPermissions: {
|
|
||||||
where: eq(boardUserPermissions.userId, userId ?? ""),
|
|
||||||
columns: {
|
|
||||||
permission: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
groupPermissions: {
|
|
||||||
where: inArray(boardGroupPermissions.groupId, groupsOfCurrentUser.map((group) => group.groupId).concat("")),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!board) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Board not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { sections, items, layouts, ...otherBoardProperties } = board;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...otherBoardProperties,
|
|
||||||
layouts: layouts
|
|
||||||
.map(({ boardId: _, ...layout }) => layout)
|
|
||||||
.sort((layoutA, layoutB) => layoutA.breakpoint - layoutB.breakpoint),
|
|
||||||
sections: sections.map(({ collapseStates, ...section }) =>
|
|
||||||
parseSection({
|
|
||||||
...section,
|
|
||||||
xOffset: section.xOffset,
|
|
||||||
yOffset: section.yOffset,
|
|
||||||
options: superjson.parse(section.options ?? emptySuperJSON),
|
|
||||||
layouts: section.layouts.map((layout) => ({
|
|
||||||
xOffset: layout.xOffset,
|
|
||||||
yOffset: layout.yOffset,
|
|
||||||
width: layout.width,
|
|
||||||
height: layout.height,
|
|
||||||
parentSectionId: layout.parentSectionId,
|
|
||||||
layoutId: layout.layoutId,
|
|
||||||
})),
|
|
||||||
collapsed: collapseStates.at(0)?.collapsed ?? false,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
items: items.map(({ integrations: itemIntegrations, ...item }) =>
|
|
||||||
parseItem({
|
|
||||||
...item,
|
|
||||||
layouts: item.layouts.map((layout) => ({
|
|
||||||
xOffset: layout.xOffset,
|
|
||||||
yOffset: layout.yOffset,
|
|
||||||
width: layout.width,
|
|
||||||
height: layout.height,
|
|
||||||
layoutId: layout.layoutId,
|
|
||||||
sectionId: layout.sectionId,
|
|
||||||
})),
|
|
||||||
integrationIds: itemIntegrations.map((item) => item.integration.id),
|
|
||||||
advancedOptions: superjson.parse<BoardItemAdvancedOptions>(item.advancedOptions),
|
|
||||||
options: superjson.parse<Record<string, unknown>>(item.options),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const forKind = <T extends WidgetKind>(kind: T) =>
|
|
||||||
z.object({
|
|
||||||
kind: z.literal(kind),
|
|
||||||
options: z.record(z.string(), z.unknown()),
|
|
||||||
});
|
|
||||||
|
|
||||||
const outputItemSchema = zodUnionFromArray(widgetKinds.map((kind) => forKind(kind))).and(sharedItemSchema);
|
|
||||||
|
|
||||||
const parseItem = (item: unknown) => {
|
|
||||||
const result = outputItemSchema.safeParse(item);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error.message);
|
|
||||||
}
|
|
||||||
return result.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseSection = (section: unknown) => {
|
|
||||||
const result = sectionSchema.safeParse(section);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error.message);
|
|
||||||
}
|
|
||||||
return result.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterAddedItems = <TInput extends { id: string }>(inputArray: TInput[], dbArray: TInput[]) =>
|
const filterAddedItems = <TInput extends { id: string }>(inputArray: TInput[], dbArray: TInput[]) =>
|
||||||
inputArray.filter((inputItem) => !dbArray.some((dbItem) => dbItem.id === inputItem.id));
|
inputArray.filter((inputItem) => !dbArray.some((dbItem) => dbItem.id === inputItem.id));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import type { Modify } from "@homarr/common/types";
|
||||||
|
import type { InferInsertModel } from "@homarr/db";
|
||||||
|
import type { itemLayouts, sectionLayouts } from "@homarr/db/schema";
|
||||||
|
|
||||||
|
import type { getFullBoardWithWhereAsync } from "./full-board-query";
|
||||||
|
import { generateResponsiveGridFor } from "./grid-algorithm";
|
||||||
|
|
||||||
|
type DynamicSectionLayout = Modify<InferInsertModel<typeof sectionLayouts>, { parentSectionId: string }>;
|
||||||
|
|
||||||
|
export const getUpdatedBoardLayout = (
|
||||||
|
board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>,
|
||||||
|
options: {
|
||||||
|
previous: {
|
||||||
|
layoutId: string;
|
||||||
|
columnCount: number;
|
||||||
|
};
|
||||||
|
current: {
|
||||||
|
layoutId: string;
|
||||||
|
columnCount: number;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const itemSectionLayoutsCollection: InferInsertModel<typeof itemLayouts>[] = [];
|
||||||
|
const sectionLayoutsCollection: DynamicSectionLayout[] = [];
|
||||||
|
|
||||||
|
const elements = getElementsForLayout(board, options.previous.layoutId);
|
||||||
|
const rootSections = board.sections.filter((section) => section.kind !== "dynamic");
|
||||||
|
|
||||||
|
for (const rootSection of rootSections) {
|
||||||
|
const result = generateResponsiveGridFor({
|
||||||
|
items: elements,
|
||||||
|
previousWidth: options.previous.columnCount,
|
||||||
|
width: options.current.columnCount,
|
||||||
|
sectionId: rootSection.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
itemSectionLayoutsCollection.push(
|
||||||
|
...board.items
|
||||||
|
.map((item): InferInsertModel<typeof itemLayouts> | null => {
|
||||||
|
const currentElement = result.items.find((element) => element.type === "item" && element.id === item.id);
|
||||||
|
|
||||||
|
if (!currentElement) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
itemId: item.id,
|
||||||
|
layoutId: options.current.layoutId,
|
||||||
|
sectionId: currentElement.sectionId,
|
||||||
|
height: currentElement.height,
|
||||||
|
width: currentElement.width,
|
||||||
|
xOffset: currentElement.xOffset,
|
||||||
|
yOffset: currentElement.yOffset,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item) => item !== null),
|
||||||
|
);
|
||||||
|
|
||||||
|
sectionLayoutsCollection.push(
|
||||||
|
...board.sections
|
||||||
|
.filter((section) => section.kind === "dynamic")
|
||||||
|
.map((section): DynamicSectionLayout | null => {
|
||||||
|
const currentElement = result.items.find(
|
||||||
|
(element) => element.type === "section" && element.id === section.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!currentElement) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
layoutId: options.current.layoutId,
|
||||||
|
sectionId: section.id,
|
||||||
|
parentSectionId: currentElement.sectionId,
|
||||||
|
height: currentElement.height,
|
||||||
|
width: currentElement.width,
|
||||||
|
xOffset: currentElement.xOffset,
|
||||||
|
yOffset: currentElement.yOffset,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((section) => section !== null),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
itemSectionLayouts: itemSectionLayoutsCollection,
|
||||||
|
sectionLayouts: sectionLayoutsCollection,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getElementsForLayout = (board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>, layoutId: string) => {
|
||||||
|
const sectionElements = board.sections
|
||||||
|
.filter((section) => section.kind === "dynamic")
|
||||||
|
.map((section) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const clonedLayout = section.layouts.find((sectionLayout) => sectionLayout.layoutId === layoutId)!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: section.id,
|
||||||
|
type: "section" as const,
|
||||||
|
height: clonedLayout.height,
|
||||||
|
width: clonedLayout.width,
|
||||||
|
xOffset: clonedLayout.xOffset,
|
||||||
|
yOffset: clonedLayout.yOffset,
|
||||||
|
sectionId: clonedLayout.parentSectionId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemElements = board.items.map((item) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const clonedLayout = item.layouts.find((itemLayout) => itemLayout.layoutId === layoutId)!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
type: "item" as const,
|
||||||
|
height: clonedLayout.height,
|
||||||
|
width: clonedLayout.width,
|
||||||
|
xOffset: clonedLayout.xOffset,
|
||||||
|
yOffset: clonedLayout.yOffset,
|
||||||
|
sectionId: clonedLayout.sectionId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...itemElements, ...sectionElements];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyAutoLayoutToBoard = (
|
||||||
|
board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>,
|
||||||
|
): Awaited<ReturnType<typeof getFullBoardWithWhereAsync>> => {
|
||||||
|
if (board.layoutMode === "custom") return board;
|
||||||
|
|
||||||
|
const baseLayout = board.layouts.find((layout) => layout.id === board.baseLayoutId) ?? null;
|
||||||
|
if (!baseLayout) return board;
|
||||||
|
|
||||||
|
const updatedLayoutMap = new Map<string, ReturnType<typeof getUpdatedBoardLayout>>();
|
||||||
|
for (const layout of board.layouts) {
|
||||||
|
if (layout.id === baseLayout.id) continue;
|
||||||
|
|
||||||
|
updatedLayoutMap.set(
|
||||||
|
layout.id,
|
||||||
|
getUpdatedBoardLayout(board, {
|
||||||
|
previous: {
|
||||||
|
layoutId: baseLayout.id,
|
||||||
|
columnCount: baseLayout.columnCount,
|
||||||
|
},
|
||||||
|
current: {
|
||||||
|
layoutId: layout.id,
|
||||||
|
columnCount: layout.columnCount,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...board,
|
||||||
|
items: board.items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
layouts: item.layouts.map(
|
||||||
|
(layoutSection) =>
|
||||||
|
updatedLayoutMap
|
||||||
|
.get(layoutSection.layoutId)
|
||||||
|
?.itemSectionLayouts.find(
|
||||||
|
(updatedLayoutSection) =>
|
||||||
|
updatedLayoutSection.itemId === item.id && updatedLayoutSection.layoutId === layoutSection.layoutId,
|
||||||
|
) ?? layoutSection,
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
sections: board.sections.map((section) =>
|
||||||
|
"layouts" in section
|
||||||
|
? {
|
||||||
|
...section,
|
||||||
|
layouts: section.layouts.map(
|
||||||
|
(sectionLayout) =>
|
||||||
|
updatedLayoutMap
|
||||||
|
.get(sectionLayout.layoutId)
|
||||||
|
?.sectionLayouts.find(
|
||||||
|
(updatedSectionLayout) =>
|
||||||
|
updatedSectionLayout.sectionId === section.id &&
|
||||||
|
updatedSectionLayout.layoutId === sectionLayout.layoutId,
|
||||||
|
) ?? sectionLayout,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: section,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import superjson from "superjson";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import type { Database, SQL } from "@homarr/db";
|
||||||
|
import { eq, inArray } from "@homarr/db";
|
||||||
|
import { boardGroupPermissions, boardUserPermissions, groupMembers, sectionCollapseStates } from "@homarr/db/schema";
|
||||||
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
|
import { emptySuperJSON, widgetKinds } from "@homarr/definitions";
|
||||||
|
import { zodUnionFromArray } from "@homarr/validation/enums";
|
||||||
|
import { sectionSchema, sharedItemSchema } from "@homarr/validation/shared";
|
||||||
|
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
|
||||||
|
|
||||||
|
export const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, userId: string | null) => {
|
||||||
|
const groupsOfCurrentUser = await db.query.groupMembers.findMany({
|
||||||
|
where: eq(groupMembers.userId, userId ?? ""),
|
||||||
|
});
|
||||||
|
const board = await db.query.boards.findFirst({
|
||||||
|
where,
|
||||||
|
with: {
|
||||||
|
creator: {
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
image: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sections: {
|
||||||
|
with: {
|
||||||
|
collapseStates: {
|
||||||
|
where: eq(sectionCollapseStates.userId, userId ?? ""),
|
||||||
|
},
|
||||||
|
layouts: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
with: {
|
||||||
|
integrations: {
|
||||||
|
with: {
|
||||||
|
integration: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layouts: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layouts: true,
|
||||||
|
userPermissions: {
|
||||||
|
where: eq(boardUserPermissions.userId, userId ?? ""),
|
||||||
|
columns: {
|
||||||
|
permission: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
groupPermissions: {
|
||||||
|
where: inArray(boardGroupPermissions.groupId, groupsOfCurrentUser.map((group) => group.groupId).concat("")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!board) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Board not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sections, items, layouts, ...otherBoardProperties } = board;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...otherBoardProperties,
|
||||||
|
layouts: layouts
|
||||||
|
.map(({ boardId: _, ...layout }) => layout)
|
||||||
|
.sort((layoutA, layoutB) => layoutA.breakpoint - layoutB.breakpoint),
|
||||||
|
sections: sections.map(({ collapseStates, ...section }) =>
|
||||||
|
parseSection({
|
||||||
|
...section,
|
||||||
|
xOffset: section.xOffset,
|
||||||
|
yOffset: section.yOffset,
|
||||||
|
options: superjson.parse(section.options ?? emptySuperJSON),
|
||||||
|
layouts: section.layouts.map((layout) => ({
|
||||||
|
xOffset: layout.xOffset,
|
||||||
|
yOffset: layout.yOffset,
|
||||||
|
width: layout.width,
|
||||||
|
height: layout.height,
|
||||||
|
parentSectionId: layout.parentSectionId,
|
||||||
|
layoutId: layout.layoutId,
|
||||||
|
})),
|
||||||
|
collapsed: collapseStates.at(0)?.collapsed ?? false,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
items: items.map(({ integrations: itemIntegrations, ...item }) =>
|
||||||
|
parseItem({
|
||||||
|
...item,
|
||||||
|
layouts: item.layouts.map((layout) => ({
|
||||||
|
xOffset: layout.xOffset,
|
||||||
|
yOffset: layout.yOffset,
|
||||||
|
width: layout.width,
|
||||||
|
height: layout.height,
|
||||||
|
layoutId: layout.layoutId,
|
||||||
|
sectionId: layout.sectionId,
|
||||||
|
})),
|
||||||
|
integrationIds: itemIntegrations.map((item) => item.integration.id),
|
||||||
|
advancedOptions: superjson.parse<BoardItemAdvancedOptions>(item.advancedOptions),
|
||||||
|
options: superjson.parse<Record<string, unknown>>(item.options),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const forKind = <T extends WidgetKind>(kind: T) =>
|
||||||
|
z.object({
|
||||||
|
kind: z.literal(kind),
|
||||||
|
options: z.record(z.string(), z.unknown()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const outputItemSchema = zodUnionFromArray(widgetKinds.map((kind) => forKind(kind))).and(sharedItemSchema);
|
||||||
|
|
||||||
|
const parseItem = (item: unknown) => {
|
||||||
|
const result = outputItemSchema.safeParse(item);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error.message);
|
||||||
|
}
|
||||||
|
return result.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseSection = (section: unknown) => {
|
||||||
|
const result = sectionSchema.safeParse(section);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error.message);
|
||||||
|
}
|
||||||
|
return result.data;
|
||||||
|
};
|
||||||
@@ -56,8 +56,7 @@ export const generateResponsiveGridFor = ({
|
|||||||
dynamicSectionHeightMap.set(dynamicSection.id, result.height);
|
dynamicSectionHeightMap.set(dynamicSection.id, result.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return same positions for items in the current section
|
if (width === previousWidth) {
|
||||||
if (width >= previousWidth) {
|
|
||||||
return {
|
return {
|
||||||
height: Math.max(...itemsOfCurrentSection.map((item) => item.yOffset + item.height)),
|
height: Math.max(...itemsOfCurrentSection.map((item) => item.yOffset + item.height)),
|
||||||
items: newItems.concat(normalizedItems),
|
items: newItems.concat(normalizedItems),
|
||||||
|
|||||||
@@ -99,11 +99,12 @@ qlllmmmmmnnrrrst
|
|||||||
ulllmmmmmnnrrrvw
|
ulllmmmmmnnrrrvw
|
||||||
xyz äö`;
|
xyz äö`;
|
||||||
|
|
||||||
// Just add two empty columns to the right
|
const eighteenColumns = `
|
||||||
const eighteenColumns = sixteenColumns
|
abbccccddddeeefghi
|
||||||
.split("\n")
|
jbbccccddddeeeklll
|
||||||
.map((line, index) => (index === 0 ? line : `${line} `))
|
mmmmmnnopqseeetlll
|
||||||
.join("\n");
|
mmmmmnnrrruvwxylll
|
||||||
|
mmmmmnnrrrzäö `;
|
||||||
|
|
||||||
const tenColumns = `
|
const tenColumns = `
|
||||||
abbcccceee
|
abbcccceee
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { z } from "zod/v4";
|
|||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import { createIntegrationAsync } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
|
import { mediaRequestStatusConfiguration } from "@homarr/integrations/types";
|
||||||
import type { MediaRequest } from "@homarr/integrations/types";
|
import type { MediaRequest } from "@homarr/integrations/types";
|
||||||
import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list";
|
import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list";
|
||||||
import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats";
|
import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats";
|
||||||
@@ -35,7 +36,10 @@ export const mediaRequestsRouter = createTRPCRouter({
|
|||||||
return dataB.createdAt.getTime() - dataA.createdAt.getTime();
|
return dataB.createdAt.getTime() - dataA.createdAt.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataA.status - dataB.status;
|
return (
|
||||||
|
mediaRequestStatusConfiguration[dataA.status].position -
|
||||||
|
mediaRequestStatusConfiguration[dataB.status].position
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
subscribeToLatestRequests: publicProcedure
|
subscribeToLatestRequests: publicProcedure
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"cookies": "^0.9.1",
|
"cookies": "^0.9.1",
|
||||||
"ldapts": "8.0.9",
|
"ldapts": "8.0.9",
|
||||||
"next": "16.0.1",
|
"next": "16.0.3",
|
||||||
"next-auth": "5.0.0-beta.30",
|
"next-auth": "5.0.0-beta.30",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { usePathname } from "next/navigation";
|
|||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
|
||||||
|
import { useEditMode } from "./edit-mode";
|
||||||
import { updateBoardName } from "./updater";
|
import { updateBoardName } from "./updater";
|
||||||
|
|
||||||
const BoardContext = createContext<{
|
const BoardContext = createContext<{
|
||||||
@@ -83,10 +84,12 @@ export const getCurrentLayout = (board: RouterOutputs["board"]["getBoardByName"]
|
|||||||
export const useCurrentLayout = () => {
|
export const useCurrentLayout = () => {
|
||||||
const board = useRequiredBoard();
|
const board = useRequiredBoard();
|
||||||
const [currentLayout, setCurrentLayout] = useState(getCurrentLayout(board));
|
const [currentLayout, setCurrentLayout] = useState(getCurrentLayout(board));
|
||||||
|
const [isEditMode] = useEditMode();
|
||||||
|
|
||||||
const onResize = useCallback(() => {
|
const onResize = useCallback(() => {
|
||||||
|
if (isEditMode && board.layoutMode === "auto") return;
|
||||||
setCurrentLayout(getCurrentLayout(board));
|
setCurrentLayout(getCurrentLayout(board));
|
||||||
}, [board]);
|
}, [board, isEditMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
@@ -97,6 +100,10 @@ export const useCurrentLayout = () => {
|
|||||||
};
|
};
|
||||||
}, [onResize]);
|
}, [onResize]);
|
||||||
|
|
||||||
|
if (board.layoutMode === "auto" && isEditMode && board.baseLayoutId !== null) {
|
||||||
|
return board.baseLayoutId;
|
||||||
|
}
|
||||||
|
|
||||||
return currentLayout;
|
return currentLayout;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,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",
|
||||||
"esbuild": "^0.26.0",
|
"esbuild": "^0.27.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,13 +32,13 @@
|
|||||||
"@paralleldrive/cuid2": "^3.1.0",
|
"@paralleldrive/cuid2": "^3.1.0",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"dns-caching": "^0.2.7",
|
"dns-caching": "^0.2.7",
|
||||||
"next": "16.0.1",
|
"next": "16.0.3",
|
||||||
"octokit": "^5.0.5",
|
"octokit": "^5.0.5",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"undici": "7.16.0",
|
"undici": "7.16.0",
|
||||||
"zod": "^4.1.12",
|
"zod": "^4.1.12",
|
||||||
"zod-validation-error": "^4.0.2"
|
"zod-validation-error": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"@homarr/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@tanstack/react-query": "^5.90.7",
|
"@tanstack/react-query": "^5.90.10",
|
||||||
"@trpc/client": "^11.7.1",
|
"@trpc/client": "^11.7.1",
|
||||||
"@trpc/server": "^11.7.1",
|
"@trpc/server": "^11.7.1",
|
||||||
"@trpc/tanstack-react-query": "^11.7.1",
|
"@trpc/tanstack-react-query": "^11.7.1",
|
||||||
@@ -42,7 +42,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/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/react": "19.2.2",
|
"@types/react": "19.2.6",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE `board` ADD `layout_mode` varchar(32) DEFAULT 'custom' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE `board` ADD `base_layout_id` varchar(64);--> statement-breakpoint
|
||||||
|
ALTER TABLE `board` ADD CONSTRAINT `board_base_layout_id_layout_id_fk` FOREIGN KEY (`base_layout_id`) REFERENCES `layout`(`id`) ON DELETE set null ON UPDATE no action;
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -260,6 +260,13 @@
|
|||||||
"when": 1760968518445,
|
"when": 1760968518445,
|
||||||
"tag": "0036_add_app_reference_to_integration",
|
"tag": "0036_add_app_reference_to_integration",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 37,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1764190331048,
|
||||||
|
"tag": "0037_add_auto_layout_mode",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "board" ADD COLUMN "layout_mode" varchar(32) DEFAULT 'custom' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "board" ADD COLUMN "base_layout_id" varchar(64);--> statement-breakpoint
|
||||||
|
ALTER TABLE "board" ADD CONSTRAINT "board_base_layout_id_layout_id_fk" FOREIGN KEY ("base_layout_id") REFERENCES "public"."layout"("id") ON DELETE set null ON UPDATE no action;
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -22,6 +22,13 @@
|
|||||||
"when": 1760968530084,
|
"when": 1760968530084,
|
||||||
"tag": "0002_add_app_reference_to_integration",
|
"tag": "0002_add_app_reference_to_integration",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1764190350662,
|
||||||
|
"tag": "0003_add_auto_layout_mode",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `board` ADD `layout_mode` text DEFAULT 'custom' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE `board` ADD `base_layout_id` text REFERENCES layout(id);
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -246,6 +246,13 @@
|
|||||||
"when": 1760968503571,
|
"when": 1760968503571,
|
||||||
"tag": "0034_add_app_reference_to_integration",
|
"tag": "0034_add_app_reference_to_integration",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 35,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1764190313293,
|
||||||
|
"tag": "0035_add_auto_layout_mode",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -49,13 +49,13 @@
|
|||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.7",
|
"@mantine/core": "^8.3.8",
|
||||||
"@paralleldrive/cuid2": "^3.1.0",
|
"@paralleldrive/cuid2": "^3.1.0",
|
||||||
"@testcontainers/mysql": "^11.8.0",
|
"@testcontainers/mysql": "^11.8.1",
|
||||||
"@testcontainers/postgresql": "^11.8.0",
|
"@testcontainers/postgresql": "^11.8.1",
|
||||||
"better-sqlite3": "^12.4.1",
|
"better-sqlite3": "^12.4.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"drizzle-kit": "^0.31.6",
|
"drizzle-kit": "^0.31.7",
|
||||||
"drizzle-orm": "^0.44.7",
|
"drizzle-orm": "^0.44.7",
|
||||||
"drizzle-zod": "^0.8.3",
|
"drizzle-zod": "^0.8.3",
|
||||||
"mysql2": "3.15.3",
|
"mysql2": "3.15.3",
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
"@types/better-sqlite3": "7.6.13",
|
"@types/better-sqlite3": "7.6.13",
|
||||||
"@types/pg": "^8.15.6",
|
"@types/pg": "^8.15.6",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"esbuild": "^0.26.0",
|
"esbuild": "^0.27.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"tsx": "4.20.4",
|
"tsx": "4.20.4",
|
||||||
|
|||||||
@@ -17,16 +17,11 @@ import {
|
|||||||
varchar,
|
varchar,
|
||||||
} from "drizzle-orm/mysql-core";
|
} from "drizzle-orm/mysql-core";
|
||||||
|
|
||||||
import {
|
|
||||||
backgroundImageAttachments,
|
|
||||||
backgroundImageRepeats,
|
|
||||||
backgroundImageSizes,
|
|
||||||
emptySuperJSON,
|
|
||||||
} from "@homarr/definitions";
|
|
||||||
import type {
|
import type {
|
||||||
BackgroundImageAttachment,
|
BackgroundImageAttachment,
|
||||||
BackgroundImageRepeat,
|
BackgroundImageRepeat,
|
||||||
BackgroundImageSize,
|
BackgroundImageSize,
|
||||||
|
BoardLayoutMode,
|
||||||
BoardPermission,
|
BoardPermission,
|
||||||
ColorScheme,
|
ColorScheme,
|
||||||
GroupPermissionKey,
|
GroupPermissionKey,
|
||||||
@@ -39,6 +34,12 @@ import type {
|
|||||||
SupportedAuthProvider,
|
SupportedAuthProvider,
|
||||||
WidgetKind,
|
WidgetKind,
|
||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
|
import {
|
||||||
|
backgroundImageAttachments,
|
||||||
|
backgroundImageRepeats,
|
||||||
|
backgroundImageSizes,
|
||||||
|
emptySuperJSON,
|
||||||
|
} from "@homarr/definitions";
|
||||||
|
|
||||||
const customBlob = customType<{ data: Buffer }>({
|
const customBlob = customType<{ data: Buffer }>({
|
||||||
dataType() {
|
dataType() {
|
||||||
@@ -289,6 +290,11 @@ export const boards = mysqlTable("board", {
|
|||||||
iconColor: text(),
|
iconColor: text(),
|
||||||
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
|
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
|
||||||
disableStatus: boolean().default(false).notNull(),
|
disableStatus: boolean().default(false).notNull(),
|
||||||
|
// This has to be custom as the previous behaviour was custom
|
||||||
|
layoutMode: varchar({ length: 32 }).$type<BoardLayoutMode>().default("custom").notNull(),
|
||||||
|
baseLayoutId: varchar({ length: 64 }).references((): AnyMySqlColumn => layouts.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const boardUserPermissions = mysqlTable(
|
export const boardUserPermissions = mysqlTable(
|
||||||
@@ -678,6 +684,10 @@ export const boardRelations = relations(boards, ({ many, one }) => ({
|
|||||||
userPermissions: many(boardUserPermissions),
|
userPermissions: many(boardUserPermissions),
|
||||||
groupPermissions: many(boardGroupPermissions),
|
groupPermissions: many(boardGroupPermissions),
|
||||||
layouts: many(layouts),
|
layouts: many(layouts),
|
||||||
|
baseLayout: one(layouts, {
|
||||||
|
fields: [boards.baseLayoutId],
|
||||||
|
references: [layouts.id],
|
||||||
|
}),
|
||||||
groupHomes: many(groups, {
|
groupHomes: many(groups, {
|
||||||
relationName: "groupRelations__board__homeBoardId",
|
relationName: "groupRelations__board__homeBoardId",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -16,16 +16,11 @@ import {
|
|||||||
varchar,
|
varchar,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
import {
|
|
||||||
backgroundImageAttachments,
|
|
||||||
backgroundImageRepeats,
|
|
||||||
backgroundImageSizes,
|
|
||||||
emptySuperJSON,
|
|
||||||
} from "@homarr/definitions";
|
|
||||||
import type {
|
import type {
|
||||||
BackgroundImageAttachment,
|
BackgroundImageAttachment,
|
||||||
BackgroundImageRepeat,
|
BackgroundImageRepeat,
|
||||||
BackgroundImageSize,
|
BackgroundImageSize,
|
||||||
|
BoardLayoutMode,
|
||||||
BoardPermission,
|
BoardPermission,
|
||||||
ColorScheme,
|
ColorScheme,
|
||||||
GroupPermissionKey,
|
GroupPermissionKey,
|
||||||
@@ -38,6 +33,12 @@ import type {
|
|||||||
SupportedAuthProvider,
|
SupportedAuthProvider,
|
||||||
WidgetKind,
|
WidgetKind,
|
||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
|
import {
|
||||||
|
backgroundImageAttachments,
|
||||||
|
backgroundImageRepeats,
|
||||||
|
backgroundImageSizes,
|
||||||
|
emptySuperJSON,
|
||||||
|
} from "@homarr/definitions";
|
||||||
|
|
||||||
const customBlob = customType<{ data: Buffer }>({
|
const customBlob = customType<{ data: Buffer }>({
|
||||||
dataType() {
|
dataType() {
|
||||||
@@ -288,6 +289,11 @@ export const boards = pgTable("board", {
|
|||||||
iconColor: text(),
|
iconColor: text(),
|
||||||
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
|
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
|
||||||
disableStatus: boolean().default(false).notNull(),
|
disableStatus: boolean().default(false).notNull(),
|
||||||
|
// This has to be custom as the previous behaviour was custom
|
||||||
|
layoutMode: varchar({ length: 32 }).$type<BoardLayoutMode>().default("custom").notNull(),
|
||||||
|
baseLayoutId: varchar({ length: 64 }).references((): AnyPgColumn => layouts.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const boardUserPermissions = pgTable(
|
export const boardUserPermissions = pgTable(
|
||||||
@@ -677,6 +683,10 @@ export const boardRelations = relations(boards, ({ many, one }) => ({
|
|||||||
userPermissions: many(boardUserPermissions),
|
userPermissions: many(boardUserPermissions),
|
||||||
groupPermissions: many(boardGroupPermissions),
|
groupPermissions: many(boardGroupPermissions),
|
||||||
layouts: many(layouts),
|
layouts: many(layouts),
|
||||||
|
baseLayout: one(layouts, {
|
||||||
|
fields: [boards.baseLayoutId],
|
||||||
|
references: [layouts.id],
|
||||||
|
}),
|
||||||
groupHomes: many(groups, {
|
groupHomes: many(groups, {
|
||||||
relationName: "groupRelations__board__homeBoardId",
|
relationName: "groupRelations__board__homeBoardId",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -5,16 +5,11 @@ import { relations, sql } from "drizzle-orm";
|
|||||||
import type { AnySQLiteColumn } from "drizzle-orm/sqlite-core";
|
import type { AnySQLiteColumn } from "drizzle-orm/sqlite-core";
|
||||||
import { blob, index, int, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
import { blob, index, int, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
import {
|
|
||||||
backgroundImageAttachments,
|
|
||||||
backgroundImageRepeats,
|
|
||||||
backgroundImageSizes,
|
|
||||||
emptySuperJSON,
|
|
||||||
} from "@homarr/definitions";
|
|
||||||
import type {
|
import type {
|
||||||
BackgroundImageAttachment,
|
BackgroundImageAttachment,
|
||||||
BackgroundImageRepeat,
|
BackgroundImageRepeat,
|
||||||
BackgroundImageSize,
|
BackgroundImageSize,
|
||||||
|
BoardLayoutMode,
|
||||||
BoardPermission,
|
BoardPermission,
|
||||||
ColorScheme,
|
ColorScheme,
|
||||||
GroupPermissionKey,
|
GroupPermissionKey,
|
||||||
@@ -27,6 +22,12 @@ import type {
|
|||||||
SupportedAuthProvider,
|
SupportedAuthProvider,
|
||||||
WidgetKind,
|
WidgetKind,
|
||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
|
import {
|
||||||
|
backgroundImageAttachments,
|
||||||
|
backgroundImageRepeats,
|
||||||
|
backgroundImageSizes,
|
||||||
|
emptySuperJSON,
|
||||||
|
} from "@homarr/definitions";
|
||||||
|
|
||||||
export const apiKeys = sqliteTable("apiKey", {
|
export const apiKeys = sqliteTable("apiKey", {
|
||||||
id: text().notNull().primaryKey(),
|
id: text().notNull().primaryKey(),
|
||||||
@@ -274,6 +275,11 @@ export const boards = sqliteTable("board", {
|
|||||||
iconColor: text(),
|
iconColor: text(),
|
||||||
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
|
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
|
||||||
disableStatus: int({ mode: "boolean" }).default(false).notNull(),
|
disableStatus: int({ mode: "boolean" }).default(false).notNull(),
|
||||||
|
// This has to be custom as the previous behaviour was custom
|
||||||
|
layoutMode: text().$type<BoardLayoutMode>().default("custom").notNull(),
|
||||||
|
baseLayoutId: text().references((): AnySQLiteColumn => layouts.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const boardUserPermissions = sqliteTable(
|
export const boardUserPermissions = sqliteTable(
|
||||||
@@ -663,6 +669,10 @@ export const boardRelations = relations(boards, ({ many, one }) => ({
|
|||||||
userPermissions: many(boardUserPermissions),
|
userPermissions: many(boardUserPermissions),
|
||||||
groupPermissions: many(boardGroupPermissions),
|
groupPermissions: many(boardGroupPermissions),
|
||||||
layouts: many(layouts),
|
layouts: many(layouts),
|
||||||
|
baseLayout: one(layouts, {
|
||||||
|
fields: [boards.baseLayoutId],
|
||||||
|
references: [layouts.id],
|
||||||
|
}),
|
||||||
groupHomes: many(groups, {
|
groupHomes: many(groups, {
|
||||||
relationName: "groupRelations__board__homeBoardId",
|
relationName: "groupRelations__board__homeBoardId",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"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.3.1",
|
"fast-xml-parser": "^5.3.2",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ export const backgroundImageRepeats = createDefinition(["repeat", "repeat-x", "r
|
|||||||
export const backgroundImageSizes = createDefinition(["cover", "contain"], {
|
export const backgroundImageSizes = createDefinition(["cover", "contain"], {
|
||||||
defaultValue: "cover",
|
defaultValue: "cover",
|
||||||
});
|
});
|
||||||
|
export const boardLayoutModes = createDefinition(["auto", "custom"], { defaultValue: "auto" });
|
||||||
|
|
||||||
export type BackgroundImageAttachment = inferDefinitionType<typeof backgroundImageAttachments>;
|
export type BackgroundImageAttachment = inferDefinitionType<typeof backgroundImageAttachments>;
|
||||||
export type BackgroundImageRepeat = inferDefinitionType<typeof backgroundImageRepeats>;
|
export type BackgroundImageRepeat = inferDefinitionType<typeof backgroundImageRepeats>;
|
||||||
export type BackgroundImageSize = inferDefinitionType<typeof backgroundImageSizes>;
|
export type BackgroundImageSize = inferDefinitionType<typeof backgroundImageSizes>;
|
||||||
|
export type BoardLayoutMode = inferDefinitionType<typeof boardLayoutModes>;
|
||||||
@@ -145,7 +145,7 @@ export const integrationDefs = {
|
|||||||
},
|
},
|
||||||
piHole: {
|
piHole: {
|
||||||
name: "Pi-hole",
|
name: "Pi-hole",
|
||||||
secretKinds: [["apiKey"]],
|
secretKinds: [["apiKey"], []],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/pi-hole.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/pi-hole.svg",
|
||||||
category: ["dnsHole"],
|
category: ["dnsHole"],
|
||||||
documentationUrl: createDocumentationLink("/docs/integrations/pi-hole"),
|
documentationUrl: createDocumentationLink("/docs/integrations/pi-hole"),
|
||||||
|
|||||||
@@ -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",
|
||||||
"@types/dockerode": "^3.3.45",
|
"@types/dockerode": "^3.3.47",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ export type { Docker };
|
|||||||
export const containerStates = ["created", "running", "paused", "restarting", "exited", "removing", "dead"] as const;
|
export const containerStates = ["created", "running", "paused", "restarting", "exited", "removing", "dead"] as const;
|
||||||
|
|
||||||
export type ContainerState = (typeof containerStates)[number];
|
export type ContainerState = (typeof containerStates)[number];
|
||||||
|
export * from "./labels";
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export const dockerLabels = {
|
||||||
|
// Label to hide a container from Homarrs docker integration
|
||||||
|
hide: "homarr.hide",
|
||||||
|
} as const;
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@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.3.7",
|
"@mantine/form": "^8.3.8",
|
||||||
"mantine-form-zod-resolver": "^1.3.0",
|
"mantine-form-zod-resolver": "^1.3.0",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@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.3.7",
|
"@mantine/core": "^8.3.8",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export type {
|
|||||||
FirewallMemorySummary,
|
FirewallMemorySummary,
|
||||||
} from "./interfaces/firewall-summary/firewall-summary-types";
|
} from "./interfaces/firewall-summary/firewall-summary-types";
|
||||||
export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types";
|
export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types";
|
||||||
export { MediaRequestStatus } from "./interfaces/media-requests/media-request-types";
|
export { UpstreamMediaRequestStatus } from "./interfaces/media-requests/media-request-types";
|
||||||
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types";
|
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types";
|
||||||
export type { StreamSession } from "./interfaces/media-server/media-server-types";
|
export type { StreamSession } from "./interfaces/media-server/media-server-types";
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { objectKeys } from "@homarr/common";
|
||||||
|
|
||||||
interface SerieSeason {
|
interface SerieSeason {
|
||||||
id: number;
|
id: number;
|
||||||
seasonNumber: number;
|
seasonNumber: number;
|
||||||
@@ -34,6 +36,64 @@ export interface MediaRequest {
|
|||||||
requestedBy?: Omit<RequestUser, "requestCount">;
|
requestedBy?: Omit<RequestUser, "requestCount">;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const mediaAvailabilityConfiguration = {
|
||||||
|
available: {
|
||||||
|
color: "green",
|
||||||
|
},
|
||||||
|
partiallyAvailable: {
|
||||||
|
color: "yellow",
|
||||||
|
},
|
||||||
|
processing: {
|
||||||
|
color: "blue",
|
||||||
|
},
|
||||||
|
requested: {
|
||||||
|
color: "violet",
|
||||||
|
},
|
||||||
|
pending: {
|
||||||
|
color: "violet",
|
||||||
|
},
|
||||||
|
unknown: {
|
||||||
|
color: "orange",
|
||||||
|
},
|
||||||
|
deleted: {
|
||||||
|
color: "red",
|
||||||
|
},
|
||||||
|
blacklisted: {
|
||||||
|
color: "gray",
|
||||||
|
},
|
||||||
|
} satisfies Record<string, { color: string }>;
|
||||||
|
|
||||||
|
export const mediaAvailabilities = objectKeys(mediaAvailabilityConfiguration);
|
||||||
|
|
||||||
|
export type MediaAvailability = (typeof mediaAvailabilities)[number];
|
||||||
|
|
||||||
|
export const mediaRequestStatusConfiguration = {
|
||||||
|
pending: {
|
||||||
|
color: "blue",
|
||||||
|
position: 1,
|
||||||
|
},
|
||||||
|
approved: {
|
||||||
|
color: "green",
|
||||||
|
position: 2,
|
||||||
|
},
|
||||||
|
declined: {
|
||||||
|
color: "red",
|
||||||
|
position: 3,
|
||||||
|
},
|
||||||
|
failed: {
|
||||||
|
color: "red",
|
||||||
|
position: 4,
|
||||||
|
},
|
||||||
|
completed: {
|
||||||
|
color: "green",
|
||||||
|
position: 5,
|
||||||
|
},
|
||||||
|
} satisfies Record<string, { color: string; position: number }>;
|
||||||
|
|
||||||
|
export const mediaRequestStatuses = objectKeys(mediaRequestStatusConfiguration);
|
||||||
|
|
||||||
|
export type MediaRequestStatus = (typeof mediaRequestStatuses)[number];
|
||||||
|
|
||||||
export interface MediaRequestList {
|
export interface MediaRequestList {
|
||||||
integration: {
|
integration: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -66,7 +126,7 @@ export interface MediaRequestStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L1
|
// https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L1
|
||||||
export enum MediaRequestStatus {
|
export enum UpstreamMediaRequestStatus {
|
||||||
PendingApproval = 1,
|
PendingApproval = 1,
|
||||||
Approved = 2,
|
Approved = 2,
|
||||||
Declined = 3,
|
Declined = 3,
|
||||||
@@ -75,12 +135,12 @@ export enum MediaRequestStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L14
|
// https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L14
|
||||||
export enum MediaAvailability {
|
export enum UpstreamMediaAvailability {
|
||||||
Unknown = 1,
|
Unknown = 1,
|
||||||
Pending = 2,
|
Pending = 2,
|
||||||
Processing = 3,
|
Processing = 3,
|
||||||
PartiallyAvailable = 4,
|
PartiallyAvailable = 4,
|
||||||
Available = 5,
|
Available = 5,
|
||||||
Blacklisted = 6,
|
JellyseerrBlacklistedOrOverseerrDeleted = 6,
|
||||||
Deleted = 7,
|
JellyseerrDeleted = 7,
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,12 @@
|
|||||||
|
import type { MediaAvailability } from "../interfaces/media-requests/media-request-types";
|
||||||
|
import { UpstreamMediaAvailability } from "../interfaces/media-requests/media-request-types";
|
||||||
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
|
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
|
||||||
|
|
||||||
export class JellyseerrIntegration extends OverseerrIntegration {}
|
export class JellyseerrIntegration extends OverseerrIntegration {
|
||||||
|
protected override mapAvailability(availability: UpstreamMediaAvailability, inProgress: boolean): MediaAvailability {
|
||||||
|
// Availability statuses are not exactly the same between Jellyseerr and Overseerr (Jellyseerr has "blacklisted" additionally (deleted is the same value in overseerr))
|
||||||
|
if (availability === UpstreamMediaAvailability.JellyseerrBlacklistedOrOverseerrDeleted) return "blacklisted";
|
||||||
|
if (availability === UpstreamMediaAvailability.JellyseerrDeleted) return "deleted";
|
||||||
|
return super.mapAvailability(availability, inProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
import { objectEntries } from "@homarr/common";
|
|
||||||
|
|
||||||
import type { IMediaRequestIntegration } from "../../interfaces/media-requests/media-request-integration";
|
import type { IMediaRequestIntegration } from "../../interfaces/media-requests/media-request-integration";
|
||||||
import type { MediaInformation, MediaRequest, RequestStats, RequestUser } from "../../types";
|
import type {
|
||||||
import { MediaAvailability, MediaRequestStatus } from "../../types";
|
MediaAvailability,
|
||||||
|
MediaInformation,
|
||||||
|
MediaRequest,
|
||||||
|
MediaRequestStatus,
|
||||||
|
RequestStats,
|
||||||
|
RequestUser,
|
||||||
|
} from "../../types";
|
||||||
|
import { mediaAvailabilities, mediaRequestStatuses } from "../../types";
|
||||||
|
|
||||||
export class MediaRequestMockService implements IMediaRequestIntegration {
|
export class MediaRequestMockService implements IMediaRequestIntegration {
|
||||||
public async getSeriesInformationAsync(mediaType: "movie" | "tv", id: number): Promise<MediaInformation> {
|
public async getSeriesInformationAsync(mediaType: "movie" | "tv", id: number): Promise<MediaInformation> {
|
||||||
@@ -86,12 +91,10 @@ export class MediaRequestMockService implements IMediaRequestIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static randomAvailability(): MediaAvailability {
|
private static randomAvailability(): MediaAvailability {
|
||||||
const values = objectEntries(MediaAvailability).filter(([key]) => typeof key === "number");
|
return mediaAvailabilities.at(Math.floor(Math.random() * mediaAvailabilities.length)) ?? "unknown";
|
||||||
return values[Math.floor(Math.random() * values.length)]?.[1] ?? MediaAvailability.Available;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static randomStatus(): MediaRequestStatus {
|
private static randomStatus(): MediaRequestStatus {
|
||||||
const values = objectEntries(MediaRequestStatus).filter(([key]) => typeof key === "number");
|
return mediaRequestStatuses.at(Math.floor(Math.random() * mediaRequestStatuses.length)) ?? "pending";
|
||||||
return values[Math.floor(Math.random() * values.length)]?.[1] ?? MediaRequestStatus.PendingApproval;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,8 +9,17 @@ import type { ISearchableIntegration } from "../base/searchable-integration";
|
|||||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||||
import type { IMediaRequestIntegration } from "../interfaces/media-requests/media-request-integration";
|
import type { IMediaRequestIntegration } from "../interfaces/media-requests/media-request-integration";
|
||||||
import type { MediaRequest, RequestStats, RequestUser } from "../interfaces/media-requests/media-request-types";
|
import type {
|
||||||
import { MediaAvailability, MediaRequestStatus } from "../interfaces/media-requests/media-request-types";
|
MediaAvailability,
|
||||||
|
MediaRequest,
|
||||||
|
MediaRequestStatus,
|
||||||
|
RequestStats,
|
||||||
|
RequestUser,
|
||||||
|
} from "../interfaces/media-requests/media-request-types";
|
||||||
|
import {
|
||||||
|
UpstreamMediaAvailability,
|
||||||
|
UpstreamMediaRequestStatus,
|
||||||
|
} from "../interfaces/media-requests/media-request-types";
|
||||||
|
|
||||||
interface OverseerrSearchResult {
|
interface OverseerrSearchResult {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -128,7 +137,7 @@ export class OverseerrIntegration
|
|||||||
|
|
||||||
if (pendingResults.length > 0 && allResults.length > 0) {
|
if (pendingResults.length > 0 && allResults.length > 0) {
|
||||||
requests = pendingResults.concat(
|
requests = pendingResults.concat(
|
||||||
allResults.filter(({ status }) => status !== MediaRequestStatus.PendingApproval),
|
allResults.filter(({ status }) => status !== UpstreamMediaRequestStatus.PendingApproval),
|
||||||
);
|
);
|
||||||
} else if (pendingResults.length > 0) requests = pendingResults;
|
} else if (pendingResults.length > 0) requests = pendingResults;
|
||||||
else if (allResults.length > 0) requests = allResults;
|
else if (allResults.length > 0) requests = allResults;
|
||||||
@@ -137,11 +146,15 @@ export class OverseerrIntegration
|
|||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
requests.map(async (request): Promise<MediaRequest> => {
|
requests.map(async (request): Promise<MediaRequest> => {
|
||||||
const information = await this.getItemInformationAsync(request.media.tmdbId, request.type);
|
const information = await this.getItemInformationAsync(request.media.tmdbId, request.type);
|
||||||
|
|
||||||
|
// See https://github.com/seerr-team/seerr/blob/af083a3cd5c3e3d5d7917fdf4fdd67fe3f39c46b/src/components/StatusBadge/index.tsx#L40
|
||||||
|
const inProgress = (request.media.downloadStatus ?? []).length >= 1;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: request.id,
|
id: request.id,
|
||||||
name: information.name,
|
name: information.name,
|
||||||
status: request.status,
|
status: this.mapRequestStatus(request.status),
|
||||||
availability: request.media.status,
|
availability: this.mapAvailability(request.media.status, inProgress),
|
||||||
backdropImageUrl: `https://image.tmdb.org/t/p/original/${information.backdropPath}`,
|
backdropImageUrl: `https://image.tmdb.org/t/p/original/${information.backdropPath}`,
|
||||||
posterImagePath: `https://image.tmdb.org/t/p/w600_and_h900_bestv2/${information.posterPath}`,
|
posterImagePath: `https://image.tmdb.org/t/p/w600_and_h900_bestv2/${information.posterPath}`,
|
||||||
href: this.externalUrl(`/${request.type}/${request.media.tmdbId}`).toString(),
|
href: this.externalUrl(`/${request.type}/${request.media.tmdbId}`).toString(),
|
||||||
@@ -161,6 +174,42 @@ export class OverseerrIntegration
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected mapRequestStatus(status: UpstreamMediaRequestStatus): MediaRequestStatus {
|
||||||
|
switch (status) {
|
||||||
|
case UpstreamMediaRequestStatus.PendingApproval:
|
||||||
|
return "pending";
|
||||||
|
case UpstreamMediaRequestStatus.Approved:
|
||||||
|
return "approved";
|
||||||
|
case UpstreamMediaRequestStatus.Declined:
|
||||||
|
return "declined";
|
||||||
|
case UpstreamMediaRequestStatus.Failed:
|
||||||
|
return "failed";
|
||||||
|
case UpstreamMediaRequestStatus.Completed:
|
||||||
|
return "completed";
|
||||||
|
default:
|
||||||
|
return "failed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://github.com/seerr-team/seerr/blob/af083a3cd5c3e3d5d7917fdf4fdd67fe3f39c46b/src/components/StatusBadge/index.tsx#L153-L387
|
||||||
|
protected mapAvailability(availability: UpstreamMediaAvailability, inProgress: boolean): MediaAvailability {
|
||||||
|
switch (availability) {
|
||||||
|
case UpstreamMediaAvailability.Available:
|
||||||
|
return inProgress ? "processing" : "available";
|
||||||
|
case UpstreamMediaAvailability.PartiallyAvailable:
|
||||||
|
return inProgress ? "processing" : "partiallyAvailable";
|
||||||
|
case UpstreamMediaAvailability.Processing:
|
||||||
|
return inProgress ? "processing" : "requested";
|
||||||
|
case UpstreamMediaAvailability.Pending:
|
||||||
|
return "pending";
|
||||||
|
case UpstreamMediaAvailability.JellyseerrBlacklistedOrOverseerrDeleted:
|
||||||
|
return "deleted";
|
||||||
|
case UpstreamMediaAvailability.Unknown:
|
||||||
|
default:
|
||||||
|
return inProgress ? "processing" : "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async getStatsAsync(): Promise<RequestStats> {
|
public async getStatsAsync(): Promise<RequestStats> {
|
||||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/request/count"), {
|
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/request/count"), {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -339,11 +388,12 @@ const getRequestsSchema = z.object({
|
|||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
status: z.nativeEnum(MediaRequestStatus),
|
status: z.enum(UpstreamMediaRequestStatus),
|
||||||
createdAt: z.string().transform((value) => new Date(value)),
|
createdAt: z.string().transform((value) => new Date(value)),
|
||||||
media: z.object({
|
media: z.object({
|
||||||
status: z.nativeEnum(MediaAvailability),
|
status: z.enum(UpstreamMediaAvailability),
|
||||||
tmdbId: z.number(),
|
tmdbId: z.number(),
|
||||||
|
downloadStatus: z.array(z.unknown()).optional(),
|
||||||
}),
|
}),
|
||||||
type: z.enum(["movie", "tv"]),
|
type: z.enum(["movie", "tv"]),
|
||||||
requestedBy: z
|
requestedBy: z
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { dnsBlockingGetSchema, sessionResponseSchema, statsSummaryGetSchema } fr
|
|||||||
const localLogger = logger.child({ module: "PiHoleIntegrationV6" });
|
const localLogger = logger.child({ module: "PiHoleIntegrationV6" });
|
||||||
|
|
||||||
export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIntegration {
|
export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIntegration {
|
||||||
private readonly sessionStore: SessionStore<string>;
|
private readonly sessionStore: SessionStore<{ sid: string | null }>;
|
||||||
|
|
||||||
constructor(integration: IntegrationInput) {
|
constructor(integration: IntegrationInput) {
|
||||||
super(integration);
|
super(integration);
|
||||||
@@ -28,7 +28,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
|
|||||||
const response = await this.withAuthAsync(async (sessionId) => {
|
const response = await this.withAuthAsync(async (sessionId) => {
|
||||||
return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), {
|
return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), {
|
||||||
headers: {
|
headers: {
|
||||||
sid: sessionId,
|
sid: sessionId ?? undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -46,7 +46,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
|
|||||||
const response = await this.withAuthAsync(async (sessionId) => {
|
const response = await this.withAuthAsync(async (sessionId) => {
|
||||||
return fetchWithTrustedCertificatesAsync(this.url("/api/stats/summary"), {
|
return fetchWithTrustedCertificatesAsync(this.url("/api/stats/summary"), {
|
||||||
headers: {
|
headers: {
|
||||||
sid: sessionId,
|
sid: sessionId ?? undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -85,7 +85,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
|
|||||||
const response = await this.withAuthAsync(async (sessionId) => {
|
const response = await this.withAuthAsync(async (sessionId) => {
|
||||||
return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), {
|
return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), {
|
||||||
headers: {
|
headers: {
|
||||||
sid: sessionId,
|
sid: sessionId ?? undefined,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ blocking: true }),
|
body: JSON.stringify({ blocking: true }),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -101,7 +101,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
|
|||||||
const response = await this.withAuthAsync(async (sessionId) => {
|
const response = await this.withAuthAsync(async (sessionId) => {
|
||||||
return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), {
|
return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), {
|
||||||
headers: {
|
headers: {
|
||||||
sid: sessionId,
|
sid: sessionId ?? undefined,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ blocking: false, timer: duration }),
|
body: JSON.stringify({ blocking: false, timer: duration }),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -118,12 +118,16 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
|
|||||||
* @param callback
|
* @param callback
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
private async withAuthAsync(callback: (sessionId: string) => Promise<UndiciResponse>) {
|
private async withAuthAsync(callback: (sessionId: string | null) => Promise<UndiciResponse>) {
|
||||||
|
if (!super.hasSecretValue("apiKey")) {
|
||||||
|
return await callback(null);
|
||||||
|
}
|
||||||
|
|
||||||
const storedSession = await this.sessionStore.getAsync();
|
const storedSession = await this.sessionStore.getAsync();
|
||||||
|
|
||||||
if (storedSession) {
|
if (storedSession) {
|
||||||
localLogger.debug("Using stored session for request", { integrationId: this.integration.id });
|
localLogger.debug("Using stored session for request", { integrationId: this.integration.id });
|
||||||
const response = await callback(storedSession);
|
const response = await callback(storedSession.sid);
|
||||||
if (response.status !== 401) {
|
if (response.status !== 401) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -132,7 +136,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sessionId = await this.getSessionAsync();
|
const sessionId = await this.getSessionAsync();
|
||||||
await this.sessionStore.setAsync(sessionId);
|
await this.sessionStore.setAsync({ sid: sessionId });
|
||||||
const response = await callback(sessionId);
|
const response = await callback(sessionId);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -141,11 +145,13 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
|
|||||||
* Get a session id from the Pi-hole server
|
* Get a session id from the Pi-hole server
|
||||||
* @returns The session id
|
* @returns The session id
|
||||||
*/
|
*/
|
||||||
private async getSessionAsync(fetchAsync: typeof undiciFetch = fetchWithTrustedCertificatesAsync): Promise<string> {
|
private async getSessionAsync(
|
||||||
const apiKey = super.getSecretValue("apiKey");
|
fetchAsync: typeof undiciFetch = fetchWithTrustedCertificatesAsync,
|
||||||
|
): Promise<string | null> {
|
||||||
|
const apiKey = super.hasSecretValue("apiKey") ? super.getSecretValue("apiKey") : null;
|
||||||
const response = await fetchAsync(this.url("/api/auth"), {
|
const response = await fetchAsync(this.url("/api/auth"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ password: apiKey }),
|
body: JSON.stringify({ password: apiKey ?? "" }),
|
||||||
headers: {
|
headers: {
|
||||||
"User-Agent": "Homarr",
|
"User-Agent": "Homarr",
|
||||||
},
|
},
|
||||||
@@ -156,8 +162,13 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const result = await sessionResponseSchema.parseAsync(data);
|
const result = await sessionResponseSchema.parseAsync(data);
|
||||||
|
|
||||||
if (!result.session.sid) {
|
if (!result.session.valid) {
|
||||||
throw new ResponseError({ status: 401, url: response.url });
|
throw new ResponseError(
|
||||||
|
{ status: 401, url: response.url },
|
||||||
|
{
|
||||||
|
cause: result.session.message ? new Error(result.session.message) : undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
localLogger.info("Received session id successfully", { integrationId: this.integration.id });
|
localLogger.info("Received session id successfully", { integrationId: this.integration.id });
|
||||||
@@ -170,9 +181,14 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
|
|||||||
* @param sessionId The session id to remove
|
* @param sessionId The session id to remove
|
||||||
*/
|
*/
|
||||||
private async clearSessionAsync(
|
private async clearSessionAsync(
|
||||||
sessionId: string,
|
sessionId: string | null,
|
||||||
fetchAsync: typeof undiciFetch = fetchWithTrustedCertificatesAsync,
|
fetchAsync: typeof undiciFetch = fetchWithTrustedCertificatesAsync,
|
||||||
) {
|
) {
|
||||||
|
if (!sessionId) {
|
||||||
|
localLogger.debug("No session id to clear");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetchAsync(this.url("/api/auth"), {
|
const response = await fetchAsync(this.url("/api/auth"), {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { z } from "zod/v4";
|
|||||||
|
|
||||||
export const sessionResponseSchema = z.object({
|
export const sessionResponseSchema = z.object({
|
||||||
session: z.object({
|
session: z.object({
|
||||||
|
valid: z.boolean(),
|
||||||
sid: z.string().nullable(),
|
sid: z.string().nullable(),
|
||||||
message: z.string().nullable(),
|
message: z.string().nullable(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/api": "workspace:^0.1.0",
|
"@homarr/api": "workspace:^0.1.0",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/form": "workspace:^0.1.0",
|
"@homarr/form": "workspace:^0.1.0",
|
||||||
"@homarr/forms-collection": "workspace:^0.1.0",
|
"@homarr/forms-collection": "workspace:^0.1.0",
|
||||||
"@homarr/modals": "workspace:^0.1.0",
|
"@homarr/modals": "workspace:^0.1.0",
|
||||||
@@ -33,10 +34,10 @@
|
|||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@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.3.7",
|
"@mantine/core": "^8.3.8",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"next": "16.0.1",
|
"next": "16.0.3",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
|
|||||||
@@ -1,13 +1,28 @@
|
|||||||
import { Button, Group, InputWrapper, Slider, Stack, Switch, TextInput } from "@mantine/core";
|
import {
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Fieldset,
|
||||||
|
Group,
|
||||||
|
InputWrapper,
|
||||||
|
RadioGroup,
|
||||||
|
SimpleGrid,
|
||||||
|
Slider,
|
||||||
|
Stack,
|
||||||
|
Switch,
|
||||||
|
TextInput,
|
||||||
|
} from "@mantine/core";
|
||||||
import { useDebouncedValue } from "@mantine/hooks";
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
import { IconAlertTriangle, IconCircleCheck } from "@tabler/icons-react";
|
import { IconAlertTriangle, IconCircleCheck } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
|
import { boardLayoutModes } from "@homarr/definitions";
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { createModal } from "@homarr/modals";
|
import { createModal } from "@homarr/modals";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
import { CustomRadioCard } from "@homarr/ui";
|
||||||
import { boardColumnCountSchema, boardCreateSchema, boardNameSchema } from "@homarr/validation/board";
|
import { boardColumnCountSchema, boardCreateSchema, boardNameSchema } from "@homarr/validation/board";
|
||||||
|
|
||||||
export const AddBoardModal = createModal(({ actions }) => {
|
export const AddBoardModal = createModal(({ actions }) => {
|
||||||
@@ -18,6 +33,7 @@ export const AddBoardModal = createModal(({ actions }) => {
|
|||||||
name: "",
|
name: "",
|
||||||
columnCount: 10,
|
columnCount: 10,
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
|
layoutMode: boardLayoutModes.defaultValue,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { mutate, isPending } = clientApi.board.createBoard.useMutation({
|
const { mutate, isPending } = clientApi.board.createBoard.useMutation({
|
||||||
@@ -64,14 +80,38 @@ export const AddBoardModal = createModal(({ actions }) => {
|
|||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<InputWrapper label={t("board.field.columnCount.label")} {...form.getInputProps("columnCount")}>
|
|
||||||
<Slider
|
<Fieldset legend={t("board.setting.section.layout.title")}>
|
||||||
min={boardColumnCountSchema.minValue ?? undefined}
|
<Stack gap="sm">
|
||||||
max={boardColumnCountSchema.maxValue ?? undefined}
|
<RadioGroup {...form.getInputProps("layoutMode")}>
|
||||||
step={1}
|
<SimpleGrid cols={{ sm: 2, xs: 1 }} spacing="md">
|
||||||
{...form.getInputProps("columnCount")}
|
{boardLayoutModes.values.map((mode) => (
|
||||||
/>
|
<Box w="100%" key={mode} pos="relative">
|
||||||
</InputWrapper>
|
<CustomRadioCard
|
||||||
|
value={mode}
|
||||||
|
label={t(`board.field.layoutMode.option.${mode}.label`)}
|
||||||
|
description={t(`board.field.layoutMode.option.${mode}.description`)}
|
||||||
|
/>
|
||||||
|
{mode === boardLayoutModes.defaultValue && (
|
||||||
|
<Badge pos="absolute" top="8px" right="8px" variant="light" size="sm">
|
||||||
|
{t("common.select.badge.recommended")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<InputWrapper label={t("board.field.columnCount.label")} {...form.getInputProps("columnCount")}>
|
||||||
|
<Slider
|
||||||
|
min={boardColumnCountSchema.minValue ?? undefined}
|
||||||
|
max={boardColumnCountSchema.maxValue ?? undefined}
|
||||||
|
step={1}
|
||||||
|
{...form.getInputProps("columnCount")}
|
||||||
|
/>
|
||||||
|
</InputWrapper>
|
||||||
|
</Stack>
|
||||||
|
</Fieldset>
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
label={t("board.field.isPublic.label")}
|
label={t("board.field.isPublic.label")}
|
||||||
@@ -92,6 +132,7 @@ export const AddBoardModal = createModal(({ actions }) => {
|
|||||||
);
|
);
|
||||||
}).withOptions({
|
}).withOptions({
|
||||||
defaultTitle: (t) => t("management.page.board.action.new.label"),
|
defaultTitle: (t) => t("management.page.board.action.new.label"),
|
||||||
|
size: "lg",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useBoardNameStatus = (name: string) => {
|
export const useBoardNameStatus = (name: string) => {
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.7",
|
"@mantine/core": "^8.3.8",
|
||||||
"@mantine/hooks": "^8.3.7",
|
"@mantine/hooks": "^8.3.8",
|
||||||
"react": "19.2.0"
|
"react": "19.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/notifications": "^8.3.7",
|
"@mantine/notifications": "^8.3.8",
|
||||||
"@tabler/icons-react": "^3.35.0"
|
"@tabler/icons-react": "^3.35.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -37,10 +37,10 @@
|
|||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@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.3.7",
|
"@mantine/core": "^8.3.8",
|
||||||
"@mantine/hooks": "^8.3.7",
|
"@mantine/hooks": "^8.3.8",
|
||||||
"adm-zip": "0.5.16",
|
"adm-zip": "0.5.16",
|
||||||
"next": "16.0.1",
|
"next": "16.0.3",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"superjson": "2.2.5",
|
"superjson": "2.2.5",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
|
"@homarr/docker": "workspace:^0.1.0",
|
||||||
"@homarr/integrations": "workspace:^0.1.0",
|
"@homarr/integrations": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import type { ContainerInfo, ContainerStats } from "dockerode";
|
|||||||
|
|
||||||
import { db, like, or } from "@homarr/db";
|
import { db, like, or } from "@homarr/db";
|
||||||
import { icons } from "@homarr/db/schema";
|
import { icons } from "@homarr/db/schema";
|
||||||
|
import type { ContainerState } from "@homarr/docker";
|
||||||
|
import { dockerLabels, DockerSingleton } from "@homarr/docker";
|
||||||
|
|
||||||
import type { ContainerState } from "../../docker/src";
|
|
||||||
import { DockerSingleton } from "../../docker/src";
|
|
||||||
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
|
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
|
||||||
|
|
||||||
export const dockerContainersRequestHandler = createCachedWidgetRequestHandler({
|
export const dockerContainersRequestHandler = createCachedWidgetRequestHandler({
|
||||||
@@ -27,13 +27,11 @@ async function getContainersWithStatsAsync() {
|
|||||||
const containers = await Promise.all(
|
const containers = await Promise.all(
|
||||||
dockerInstances.map(async ({ instance, host }) => {
|
dockerInstances.map(async ({ instance, host }) => {
|
||||||
const instanceContainers = await instance.listContainers({ all: true });
|
const instanceContainers = await instance.listContainers({ all: true });
|
||||||
return instanceContainers.map((container) => ({
|
return instanceContainers
|
||||||
...container,
|
.filter((container) => dockerLabels.hide in container.Labels === false)
|
||||||
instance: host,
|
.map((container) => ({ ...container, instance: host }));
|
||||||
}));
|
|
||||||
}),
|
}),
|
||||||
).then((res) => res.flat());
|
).then((res) => res.flat());
|
||||||
|
|
||||||
const likeQueries = containers.map((container) => like(icons.name, `%${extractImage(container)}%`));
|
const likeQueries = containers.map((container) => like(icons.name, `%${extractImage(container)}%`));
|
||||||
|
|
||||||
const dbIcons =
|
const dbIcons =
|
||||||
|
|||||||
@@ -26,8 +26,8 @@
|
|||||||
"@homarr/api": "workspace:^0.1.0",
|
"@homarr/api": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@mantine/dates": "^8.3.7",
|
"@mantine/dates": "^8.3.8",
|
||||||
"next": "16.0.1",
|
"next": "16.0.3",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0"
|
"react-dom": "19.2.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,12 +33,12 @@
|
|||||||
"@homarr/settings": "workspace:^0.1.0",
|
"@homarr/settings": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.7",
|
"@mantine/core": "^8.3.8",
|
||||||
"@mantine/hooks": "^8.3.7",
|
"@mantine/hooks": "^8.3.8",
|
||||||
"@mantine/spotlight": "^8.3.7",
|
"@mantine/spotlight": "^8.3.8",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"jotai": "^2.15.1",
|
"jotai": "^2.15.1",
|
||||||
"next": "16.0.1",
|
"next": "16.0.3",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"use-deep-compare-effect": "^1.8.1"
|
"use-deep-compare-effect": "^1.8.1"
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"deepmerge": "4.3.1",
|
"deepmerge": "4.3.1",
|
||||||
"mantine-react-table": "2.0.0-beta.9",
|
"mantine-react-table": "2.0.0-beta.9",
|
||||||
"next": "16.0.1",
|
"next": "16.0.3",
|
||||||
"next-intl": "4.5.0",
|
"next-intl": "4.5.5",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0"
|
"react-dom": "19.2.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "",
|
"unknown": "",
|
||||||
"pending": "",
|
"pending": "",
|
||||||
"processing": "",
|
"processing": "",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "",
|
"partiallyAvailable": "",
|
||||||
"available": "",
|
"available": "",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
@@ -649,14 +649,14 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"option": {
|
"option": {
|
||||||
"existing": {
|
"existing": {
|
||||||
"title": "",
|
"title": "现有",
|
||||||
"label": ""
|
"label": "选择现有应用"
|
||||||
},
|
},
|
||||||
"new": {
|
"new": {
|
||||||
"title": "",
|
"title": "新建",
|
||||||
"url": {
|
"url": {
|
||||||
"label": "",
|
"label": "应用网址",
|
||||||
"description": ""
|
"description": "当从面板访问时应用将打开的网址"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -676,9 +676,9 @@
|
|||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"action": {
|
"action": {
|
||||||
"add": "",
|
"add": "链接应用",
|
||||||
"remove": "",
|
"remove": "取消连接",
|
||||||
"select": ""
|
"select": "选择要链接的应用"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -709,7 +709,7 @@
|
|||||||
"description": "集成“{kind}”可以与搜索引擎一起使用。勾选此项可自动配置搜索引擎。"
|
"description": "集成“{kind}”可以与搜索引擎一起使用。勾选此项可自动配置搜索引擎。"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"sectionTitle": ""
|
"sectionTitle": "关联的应用"
|
||||||
},
|
},
|
||||||
"createApp": {
|
"createApp": {
|
||||||
"label": "创建应用",
|
"label": "创建应用",
|
||||||
@@ -1051,7 +1051,7 @@
|
|||||||
"add": "添加",
|
"add": "添加",
|
||||||
"apply": "应用",
|
"apply": "应用",
|
||||||
"backToOverview": "返回概览",
|
"backToOverview": "返回概览",
|
||||||
"change": "",
|
"change": "更改",
|
||||||
"create": "创建",
|
"create": "创建",
|
||||||
"createAnother": "创建并重新开始",
|
"createAnother": "创建并重新开始",
|
||||||
"edit": "编辑",
|
"edit": "编辑",
|
||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "未知",
|
"unknown": "未知",
|
||||||
"pending": "等待处理",
|
"pending": "等待处理",
|
||||||
"processing": "处理中",
|
"processing": "处理中",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "部分",
|
"partiallyAvailable": "部分",
|
||||||
"available": "可用",
|
"available": "可用",
|
||||||
"blacklisted": "已列入黑名单",
|
"blacklisted": "已列入黑名单",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "crwdns1314:0crwdne1314:0",
|
"unknown": "crwdns1314:0crwdne1314:0",
|
||||||
"pending": "crwdns1316:0crwdne1316:0",
|
"pending": "crwdns1316:0crwdne1316:0",
|
||||||
"processing": "crwdns1318:0crwdne1318:0",
|
"processing": "crwdns1318:0crwdne1318:0",
|
||||||
|
"requested": "crwdns3806:0crwdne3806:0",
|
||||||
"partiallyAvailable": "crwdns1320:0crwdne1320:0",
|
"partiallyAvailable": "crwdns1320:0crwdne1320:0",
|
||||||
"available": "crwdns1322:0crwdne1322:0",
|
"available": "crwdns1322:0crwdne1322:0",
|
||||||
"blacklisted": "crwdns3468:0crwdne3468:0",
|
"blacklisted": "crwdns3468:0crwdne3468:0",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Neznámý",
|
"unknown": "Neznámý",
|
||||||
"pending": "",
|
"pending": "",
|
||||||
"processing": "",
|
"processing": "",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Částečně dostupné",
|
"partiallyAvailable": "Částečně dostupné",
|
||||||
"available": "K dispozici",
|
"available": "K dispozici",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Ukendt",
|
"unknown": "Ukendt",
|
||||||
"pending": "Afventende",
|
"pending": "Afventende",
|
||||||
"processing": "Behandler",
|
"processing": "Behandler",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Delvis",
|
"partiallyAvailable": "Delvis",
|
||||||
"available": "Tilgængelig",
|
"available": "Tilgængelig",
|
||||||
"blacklisted": "Blacklistet",
|
"blacklisted": "Blacklistet",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Unbekannt",
|
"unknown": "Unbekannt",
|
||||||
"pending": "Ausstehend",
|
"pending": "Ausstehend",
|
||||||
"processing": "In Bearbeitung",
|
"processing": "In Bearbeitung",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Teilweise",
|
"partiallyAvailable": "Teilweise",
|
||||||
"available": "Verfügbar",
|
"available": "Verfügbar",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Unbekannt",
|
"unknown": "Unbekannt",
|
||||||
"pending": "Ausstehend",
|
"pending": "Ausstehend",
|
||||||
"processing": "In Bearbeitung",
|
"processing": "In Bearbeitung",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Teilweise",
|
"partiallyAvailable": "Teilweise",
|
||||||
"available": "Verfügbar",
|
"available": "Verfügbar",
|
||||||
"blacklisted": "Gesperrt",
|
"blacklisted": "Gesperrt",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Άγνωστο",
|
"unknown": "Άγνωστο",
|
||||||
"pending": "",
|
"pending": "",
|
||||||
"processing": "",
|
"processing": "",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Μερικώς",
|
"partiallyAvailable": "Μερικώς",
|
||||||
"available": "Διαθέσιμο",
|
"available": "Διαθέσιμο",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "",
|
"unknown": "",
|
||||||
"pending": "",
|
"pending": "",
|
||||||
"processing": "",
|
"processing": "",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "",
|
"partiallyAvailable": "",
|
||||||
"available": "",
|
"available": "",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
"processing": "Processing",
|
"processing": "Processing",
|
||||||
|
"requested": "Requested",
|
||||||
"partiallyAvailable": "Partial",
|
"partiallyAvailable": "Partial",
|
||||||
"available": "Available",
|
"available": "Available",
|
||||||
"blacklisted": "Blacklisted",
|
"blacklisted": "Blacklisted",
|
||||||
@@ -2792,6 +2793,23 @@
|
|||||||
"label": "Disable app status",
|
"label": "Disable app status",
|
||||||
"description": "Disables the status check for all apps on this board"
|
"description": "Disables the status check for all apps on this board"
|
||||||
},
|
},
|
||||||
|
"layoutMode": {
|
||||||
|
"label": "Layout mode",
|
||||||
|
"option": {
|
||||||
|
"auto": {
|
||||||
|
"label": "Auto",
|
||||||
|
"description": "Position and size of all layouts are calculated from the base layout."
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"label": "Custom",
|
||||||
|
"description": "Position and size must be configured for all layouts separately."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"baseLayout": {
|
||||||
|
"label": "Base layout",
|
||||||
|
"description": "The layout will be used to calculate positions and sizes for other layouts."
|
||||||
|
},
|
||||||
"columnCount": {
|
"columnCount": {
|
||||||
"label": "Column count"
|
"label": "Column count"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Desconocido",
|
"unknown": "Desconocido",
|
||||||
"pending": "Pendiente",
|
"pending": "Pendiente",
|
||||||
"processing": "Procesando",
|
"processing": "Procesando",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Parcial",
|
"partiallyAvailable": "Parcial",
|
||||||
"available": "Disponible",
|
"available": "Disponible",
|
||||||
"blacklisted": "Lista negra",
|
"blacklisted": "Lista negra",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "",
|
"unknown": "",
|
||||||
"pending": "",
|
"pending": "",
|
||||||
"processing": "",
|
"processing": "",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "",
|
"partiallyAvailable": "",
|
||||||
"available": "",
|
"available": "",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Inconnu",
|
"unknown": "Inconnu",
|
||||||
"pending": "En attente",
|
"pending": "En attente",
|
||||||
"processing": "Traitement en cours",
|
"processing": "Traitement en cours",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Partiel",
|
"partiallyAvailable": "Partiel",
|
||||||
"available": "Disponible",
|
"available": "Disponible",
|
||||||
"blacklisted": "Sur la liste noire",
|
"blacklisted": "Sur la liste noire",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "לא ידוע",
|
"unknown": "לא ידוע",
|
||||||
"pending": "בהמתנה",
|
"pending": "בהמתנה",
|
||||||
"processing": "מעבד",
|
"processing": "מעבד",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "חלקי",
|
"partiallyAvailable": "חלקי",
|
||||||
"available": "זמין",
|
"available": "זמין",
|
||||||
"blacklisted": "ברשימה השחורה",
|
"blacklisted": "ברשימה השחורה",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Nepoznato",
|
"unknown": "Nepoznato",
|
||||||
"pending": "",
|
"pending": "",
|
||||||
"processing": "",
|
"processing": "",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "",
|
"partiallyAvailable": "",
|
||||||
"available": "",
|
"available": "",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Ismeretlen",
|
"unknown": "Ismeretlen",
|
||||||
"pending": "",
|
"pending": "",
|
||||||
"processing": "Feldolgozás",
|
"processing": "Feldolgozás",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Részleges",
|
"partiallyAvailable": "Részleges",
|
||||||
"available": "Elérhető",
|
"available": "Elérhető",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
@@ -730,7 +730,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"common": {
|
"common": {
|
||||||
"cause": {
|
"cause": {
|
||||||
"title": ""
|
"title": "Causa con più dettagli"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"unknown": {
|
"unknown": {
|
||||||
@@ -738,7 +738,7 @@
|
|||||||
"description": "Si è verificato un errore sconosciuto, apri la causa sottostante per vedere maggiori dettagli"
|
"description": "Si è verificato un errore sconosciuto, apri la causa sottostante per vedere maggiori dettagli"
|
||||||
},
|
},
|
||||||
"parse": {
|
"parse": {
|
||||||
"title": "",
|
"title": "Errore di analisi",
|
||||||
"description": "La risposta non può essere analizzata. Si prega di verificare che l'URL stia puntando all'URL di base del servizio."
|
"description": "La risposta non può essere analizzata. Si prega di verificare che l'URL stia puntando all'URL di base del servizio."
|
||||||
},
|
},
|
||||||
"authorization": {
|
"authorization": {
|
||||||
@@ -959,8 +959,8 @@
|
|||||||
"newLabel": "Nuovo ID token"
|
"newLabel": "Nuovo ID token"
|
||||||
},
|
},
|
||||||
"realm": {
|
"realm": {
|
||||||
"label": "",
|
"label": "Dominio",
|
||||||
"newLabel": ""
|
"newLabel": "Nuovo dominio"
|
||||||
},
|
},
|
||||||
"personalAccessToken": {
|
"personalAccessToken": {
|
||||||
"label": "Token d'Accesso Personale",
|
"label": "Token d'Accesso Personale",
|
||||||
@@ -1053,7 +1053,7 @@
|
|||||||
"backToOverview": "Torna alla panoramica",
|
"backToOverview": "Torna alla panoramica",
|
||||||
"change": "Cambia",
|
"change": "Cambia",
|
||||||
"create": "Crea",
|
"create": "Crea",
|
||||||
"createAnother": "",
|
"createAnother": "Crea e ricomincia",
|
||||||
"edit": "Modifica",
|
"edit": "Modifica",
|
||||||
"import": "Importa",
|
"import": "Importa",
|
||||||
"insert": "Inserisci",
|
"insert": "Inserisci",
|
||||||
@@ -1389,8 +1389,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dnsHoleSummary": {
|
"dnsHoleSummary": {
|
||||||
"name": "",
|
"name": "Riepilogo del filtraggio DNS",
|
||||||
"description": "",
|
"description": "Mostra il riepilogo del tuo filtro DNS",
|
||||||
"option": {
|
"option": {
|
||||||
"layout": {
|
"layout": {
|
||||||
"label": "Layout",
|
"label": "Layout",
|
||||||
@@ -1411,7 +1411,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"internalServerError": "",
|
"internalServerError": "Impossibile recuperare il riepilogo del filtro DNS",
|
||||||
"integrationsDisconnected": "Nessun dato disponibile, tutte le integrazioni disconnesse"
|
"integrationsDisconnected": "Nessun dato disponibile, tutte le integrazioni disconnesse"
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
@@ -1423,7 +1423,7 @@
|
|||||||
"domainsTooltip": "A causa di più integrazioni, Homarr non può calcolare il numero esatto di domini bloccati"
|
"domainsTooltip": "A causa di più integrazioni, Homarr non può calcolare il numero esatto di domini bloccati"
|
||||||
},
|
},
|
||||||
"dnsHoleControls": {
|
"dnsHoleControls": {
|
||||||
"name": "",
|
"name": "Controlli del filtro DNS",
|
||||||
"description": "Controlla PiHole o AdGuard dalla tua dashboard",
|
"description": "Controlla PiHole o AdGuard dalla tua dashboard",
|
||||||
"option": {
|
"option": {
|
||||||
"layout": {
|
"layout": {
|
||||||
@@ -1445,7 +1445,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"internalServerError": ""
|
"internalServerError": "Impossibile controllare il DNS Hole"
|
||||||
},
|
},
|
||||||
"controls": {
|
"controls": {
|
||||||
"enableAll": "Abilita tutto",
|
"enableAll": "Abilita tutto",
|
||||||
@@ -1514,7 +1514,7 @@
|
|||||||
"label": "Indirizzo del server"
|
"label": "Indirizzo del server"
|
||||||
},
|
},
|
||||||
"isBedrockServer": {
|
"isBedrockServer": {
|
||||||
"label": ""
|
"label": "Server Bedrock"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
@@ -1625,7 +1625,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"noUrl": "",
|
"noUrl": "Nessun URL iFrame fornito",
|
||||||
"unsupportedProtocol": "L'URL fornito sta usando un protocollo non supportato. Si prega di utilizzare uno di ({supportedProtocols})",
|
"unsupportedProtocol": "L'URL fornito sta usando un protocollo non supportato. Si prega di utilizzare uno di ({supportedProtocols})",
|
||||||
"noBrowerSupport": "Il tuo browser non supporta iframes. Aggiorna il tuo browser."
|
"noBrowerSupport": "Il tuo browser non supporta iframes. Aggiorna il tuo browser."
|
||||||
}
|
}
|
||||||
@@ -1664,7 +1664,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stockPrice": {
|
"stockPrice": {
|
||||||
"name": "",
|
"name": "Prezzo delle azioni",
|
||||||
"description": "Visualizza il prezzo attuale delle azioni di una società",
|
"description": "Visualizza il prezzo attuale delle azioni di una società",
|
||||||
"option": {
|
"option": {
|
||||||
"stock": {
|
"stock": {
|
||||||
@@ -1835,7 +1835,7 @@
|
|||||||
"title": "Index manager",
|
"title": "Index manager",
|
||||||
"testAll": "Prova tutto",
|
"testAll": "Prova tutto",
|
||||||
"error": {
|
"error": {
|
||||||
"internalServerError": ""
|
"internalServerError": "Impossibile recuperare lo stato degli indicizzatori"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"healthMonitoring": {
|
"healthMonitoring": {
|
||||||
@@ -2020,7 +2020,7 @@
|
|||||||
"name": "Nome",
|
"name": "Nome",
|
||||||
"id": "Id",
|
"id": "Id",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"title": "",
|
"title": "Statistiche per nerd",
|
||||||
"video": {
|
"video": {
|
||||||
"title": "Video",
|
"title": "Video",
|
||||||
"resolution": "Risoluzione"
|
"resolution": "Risoluzione"
|
||||||
@@ -2034,7 +2034,7 @@
|
|||||||
"title": "Transcodifica",
|
"title": "Transcodifica",
|
||||||
"container": "Container",
|
"container": "Container",
|
||||||
"resolution": "Risoluzione",
|
"resolution": "Risoluzione",
|
||||||
"target": ""
|
"target": "Codec di destinazione"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2098,7 +2098,7 @@
|
|||||||
"detailsTitle": "Categorie (Informazioni aggiuntive)"
|
"detailsTitle": "Categorie (Informazioni aggiuntive)"
|
||||||
},
|
},
|
||||||
"downSpeed": {
|
"downSpeed": {
|
||||||
"columnTitle": "",
|
"columnTitle": "Giù",
|
||||||
"detailsTitle": "Velocità Di Download"
|
"detailsTitle": "Velocità Di Download"
|
||||||
},
|
},
|
||||||
"index": {
|
"index": {
|
||||||
@@ -2112,22 +2112,22 @@
|
|||||||
"columnTitle": "Integrazione"
|
"columnTitle": "Integrazione"
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"columnTitle": ""
|
"columnTitle": "Nome del job"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"columnTitle": "Avanzamento",
|
"columnTitle": "Avanzamento",
|
||||||
"detailsTitle": ""
|
"detailsTitle": "Avanzamento del download"
|
||||||
},
|
},
|
||||||
"ratio": {
|
"ratio": {
|
||||||
"columnTitle": "Rapporto",
|
"columnTitle": "Rapporto",
|
||||||
"detailsTitle": "Rapporto torrent (ricevuti/inviati)"
|
"detailsTitle": "Rapporto torrent (ricevuti/inviati)"
|
||||||
},
|
},
|
||||||
"received": {
|
"received": {
|
||||||
"columnTitle": "",
|
"columnTitle": "Totale fuori servizio",
|
||||||
"detailsTitle": "Totale scaricati"
|
"detailsTitle": "Totale scaricati"
|
||||||
},
|
},
|
||||||
"sent": {
|
"sent": {
|
||||||
"columnTitle": "",
|
"columnTitle": "Totale operativo",
|
||||||
"detailsTitle": "Totale Caricati"
|
"detailsTitle": "Totale Caricati"
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
@@ -2136,7 +2136,7 @@
|
|||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"columnTitle": "Stato",
|
"columnTitle": "Stato",
|
||||||
"detailsTitle": ""
|
"detailsTitle": "Stato job"
|
||||||
},
|
},
|
||||||
"time": {
|
"time": {
|
||||||
"columnTitle": "Ora di fine",
|
"columnTitle": "Ora di fine",
|
||||||
@@ -2158,7 +2158,7 @@
|
|||||||
"completed": "Completato",
|
"completed": "Completato",
|
||||||
"failed": "Non riuscito",
|
"failed": "Non riuscito",
|
||||||
"processing": "In lavorazione",
|
"processing": "In lavorazione",
|
||||||
"leeching": "",
|
"leeching": "Leeching",
|
||||||
"stalled": "In stallo",
|
"stalled": "In stallo",
|
||||||
"unknown": "Sconosciuto",
|
"unknown": "Sconosciuto",
|
||||||
"seeding": "Seeding"
|
"seeding": "Seeding"
|
||||||
@@ -2205,7 +2205,7 @@
|
|||||||
"label": "Mostra suggerimento descrizione"
|
"label": "Mostra suggerimento descrizione"
|
||||||
},
|
},
|
||||||
"showType": {
|
"showType": {
|
||||||
"label": ""
|
"label": "Mostra il badge del tipo di media"
|
||||||
},
|
},
|
||||||
"showSource": {
|
"showSource": {
|
||||||
"label": "Mostra sorgente integrazione"
|
"label": "Mostra sorgente integrazione"
|
||||||
@@ -2216,7 +2216,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mediaRequests-requestList": {
|
"mediaRequests-requestList": {
|
||||||
"name": "",
|
"name": "Elenco Richieste Media",
|
||||||
"description": "Visualizza un elenco di tutte le richieste media dalla tua istanza Overseerr o Jellyseerr",
|
"description": "Visualizza un elenco di tutte le richieste media dalla tua istanza Overseerr o Jellyseerr",
|
||||||
"option": {
|
"option": {
|
||||||
"linksTargetNewTab": {
|
"linksTargetNewTab": {
|
||||||
@@ -2232,9 +2232,10 @@
|
|||||||
"unknown": "Sconosciuto",
|
"unknown": "Sconosciuto",
|
||||||
"pending": "In sospeso",
|
"pending": "In sospeso",
|
||||||
"processing": "In lavorazione",
|
"processing": "In lavorazione",
|
||||||
|
"requested": "Richiesto",
|
||||||
"partiallyAvailable": "Parziale",
|
"partiallyAvailable": "Parziale",
|
||||||
"available": "Disponibile",
|
"available": "Disponibile",
|
||||||
"blacklisted": "",
|
"blacklisted": "In blacklist",
|
||||||
"deleted": "Eliminato"
|
"deleted": "Eliminato"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
@@ -2247,7 +2248,7 @@
|
|||||||
"toBeDetermined": "TBD"
|
"toBeDetermined": "TBD"
|
||||||
},
|
},
|
||||||
"mediaRequests-requestStats": {
|
"mediaRequests-requestStats": {
|
||||||
"name": "",
|
"name": "Statistiche Richiesta Media",
|
||||||
"description": "Statistiche sulle richieste dei media",
|
"description": "Statistiche sulle richieste dei media",
|
||||||
"option": {},
|
"option": {},
|
||||||
"titles": {
|
"titles": {
|
||||||
@@ -2338,7 +2339,7 @@
|
|||||||
"label": "Abilita RTL"
|
"label": "Abilita RTL"
|
||||||
},
|
},
|
||||||
"textLinesClamp": {
|
"textLinesClamp": {
|
||||||
"label": ""
|
"label": "Limita le righe della descrizione"
|
||||||
},
|
},
|
||||||
"maximumAmountPosts": {
|
"maximumAmountPosts": {
|
||||||
"label": "Limite di post"
|
"label": "Limite di post"
|
||||||
@@ -2353,11 +2354,11 @@
|
|||||||
"description": "Visualizza un elenco della versione corrente dei repository dati con la versione regex specificata.",
|
"description": "Visualizza un elenco della versione corrente dei repository dati con la versione regex specificata.",
|
||||||
"option": {
|
"option": {
|
||||||
"newReleaseWithin": {
|
"newReleaseWithin": {
|
||||||
"label": "",
|
"label": "Nuova versione entro",
|
||||||
"description": "Esempio di utilizzo: 1w (1 settimana), 10M (10 mesi). Tipi di unità accettati h (ore), d (giorni), w (settimane), M (mesi), y (anni). Lasciare vuoto per non evidenziare le nuove versioni."
|
"description": "Esempio di utilizzo: 1w (1 settimana), 10M (10 mesi). Tipi di unità accettati h (ore), d (giorni), w (settimane), M (mesi), y (anni). Lasciare vuoto per non evidenziare le nuove versioni."
|
||||||
},
|
},
|
||||||
"staleReleaseWithin": {
|
"staleReleaseWithin": {
|
||||||
"label": "",
|
"label": "Rilascio obsoleto entro",
|
||||||
"description": "Esempio di utilizzo: 1w (1 settimana), 10M (10 mesi). Tipi di unità accettati h (ore), d (giorni), w (settimane), M (mesi), y (anni). Lasciare vuoto per non evidenziare le release obsolete."
|
"description": "Esempio di utilizzo: 1w (1 settimana), 10M (10 mesi). Tipi di unità accettati h (ore), d (giorni), w (settimane), M (mesi), y (anni). Lasciare vuoto per non evidenziare le release obsolete."
|
||||||
},
|
},
|
||||||
"showOnlyHighlighted": {
|
"showOnlyHighlighted": {
|
||||||
@@ -2702,7 +2703,7 @@
|
|||||||
"label": "Titolo della pagina"
|
"label": "Titolo della pagina"
|
||||||
},
|
},
|
||||||
"metaTitle": {
|
"metaTitle": {
|
||||||
"label": ""
|
"label": "Meta title"
|
||||||
},
|
},
|
||||||
"logoImageUrl": {
|
"logoImageUrl": {
|
||||||
"label": "URL immagine del logo"
|
"label": "URL immagine del logo"
|
||||||
@@ -2948,7 +2949,7 @@
|
|||||||
"label": "Numero di colonne"
|
"label": "Numero di colonne"
|
||||||
},
|
},
|
||||||
"breakpoint": {
|
"breakpoint": {
|
||||||
"label": "",
|
"label": "Breakpoint",
|
||||||
"description": "Il layout verrà utilizzato su tutti gli schermi più grandi di questo breakpoint fino al breakpoint più grande successivo."
|
"description": "Il layout verrà utilizzato su tutti gli schermi più grandi di questo breakpoint fino al breakpoint più grande successivo."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3322,7 +3323,7 @@
|
|||||||
"label": "Analytics"
|
"label": "Analytics"
|
||||||
},
|
},
|
||||||
"smartHomeEntityState": {
|
"smartHomeEntityState": {
|
||||||
"label": ""
|
"label": "Stato dell'entità Smart Home"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"label": "Ping"
|
"label": "Ping"
|
||||||
@@ -3337,10 +3338,10 @@
|
|||||||
"label": "Scaricamenti"
|
"label": "Scaricamenti"
|
||||||
},
|
},
|
||||||
"mediaRequestStats": {
|
"mediaRequestStats": {
|
||||||
"label": ""
|
"label": "Statistiche richieste media"
|
||||||
},
|
},
|
||||||
"mediaRequestList": {
|
"mediaRequestList": {
|
||||||
"label": ""
|
"label": "Elenco richieste media"
|
||||||
},
|
},
|
||||||
"rssFeeds": {
|
"rssFeeds": {
|
||||||
"label": "Feeds RSS"
|
"label": "Feeds RSS"
|
||||||
@@ -3349,10 +3350,10 @@
|
|||||||
"label": "Gestione Indicizzatori"
|
"label": "Gestione Indicizzatori"
|
||||||
},
|
},
|
||||||
"healthMonitoring": {
|
"healthMonitoring": {
|
||||||
"label": ""
|
"label": "Monitoraggio integrità del sistema"
|
||||||
},
|
},
|
||||||
"dnsHole": {
|
"dnsHole": {
|
||||||
"label": ""
|
"label": "Dati di filtraggio DNS"
|
||||||
},
|
},
|
||||||
"updateChecker": {
|
"updateChecker": {
|
||||||
"label": "Controllo aggiornamenti"
|
"label": "Controllo aggiornamenti"
|
||||||
@@ -3435,10 +3436,10 @@
|
|||||||
},
|
},
|
||||||
"trigger": {
|
"trigger": {
|
||||||
"success": {
|
"success": {
|
||||||
"message": ""
|
"message": "Attività attivata con successo"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"message": ""
|
"message": "Impossibile attivare l'attività"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enable": {
|
"enable": {
|
||||||
@@ -3453,7 +3454,7 @@
|
|||||||
},
|
},
|
||||||
"toggle": {
|
"toggle": {
|
||||||
"error": {
|
"error": {
|
||||||
"message": ""
|
"message": "Impossibile attivare lo stato dell'attività"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3517,12 +3518,12 @@
|
|||||||
"action": "Azione"
|
"action": "Azione"
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"toggleBoardEdit": "",
|
"toggleBoardEdit": "Attiva la modalità di modifica della board",
|
||||||
"toggleColorScheme": "",
|
"toggleColorScheme": "Attiva tema chiaro/scuro",
|
||||||
"saveNotebook": "",
|
"saveNotebook": "Salva notebook (solo nel widget notebook)",
|
||||||
"openSpotlight": "Apri ricerca"
|
"openSpotlight": "Apri ricerca"
|
||||||
},
|
},
|
||||||
"note": ""
|
"note": "Suggerimento: 'Mod' si riferisce sia al tasto Ctrl che al tasto ⌘ su macOS"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3534,7 +3535,7 @@
|
|||||||
"updated": "Aggiornato {when}",
|
"updated": "Aggiornato {when}",
|
||||||
"search": "Cerca {count} container",
|
"search": "Cerca {count} container",
|
||||||
"selected": "{selectCount} di {totalCount} containers selezionati",
|
"selected": "{selectCount} di {totalCount} containers selezionati",
|
||||||
"footer": ""
|
"footer": "Totale container {count}"
|
||||||
},
|
},
|
||||||
"field": {
|
"field": {
|
||||||
"name": {
|
"name": {
|
||||||
@@ -3672,11 +3673,11 @@
|
|||||||
"resources": {
|
"resources": {
|
||||||
"title": "Risorse",
|
"title": "Risorse",
|
||||||
"nodes": "Nodi",
|
"nodes": "Nodi",
|
||||||
"namespaces": "",
|
"namespaces": "Spazi dei nomi",
|
||||||
"ingresses": "Ingressi",
|
"ingresses": "Ingressi",
|
||||||
"services": "Servizi",
|
"services": "Servizi",
|
||||||
"pods": "Pods",
|
"pods": "Pods",
|
||||||
"configmaps": "",
|
"configmaps": "ConfigMaps",
|
||||||
"secrets": "Secrets",
|
"secrets": "Secrets",
|
||||||
"volumes": "Volumi"
|
"volumes": "Volumi"
|
||||||
}
|
}
|
||||||
@@ -3721,7 +3722,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"namespaces": {
|
"namespaces": {
|
||||||
"label": "",
|
"label": "Spazi dei nomi",
|
||||||
"field": {
|
"field": {
|
||||||
"name": {
|
"name": {
|
||||||
"label": "Nome"
|
"label": "Nome"
|
||||||
@@ -3730,7 +3731,7 @@
|
|||||||
"label": "Stato",
|
"label": "Stato",
|
||||||
"option": {
|
"option": {
|
||||||
"active": "Attivo",
|
"active": "Attivo",
|
||||||
"terminating": ""
|
"terminating": "Terminare"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"creationTimestamp": {
|
"creationTimestamp": {
|
||||||
@@ -3738,7 +3739,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"search": ""
|
"search": "Cerca {count} spazi dei nomi"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ingresses": {
|
"ingresses": {
|
||||||
@@ -3748,7 +3749,7 @@
|
|||||||
"label": "Nome"
|
"label": "Nome"
|
||||||
},
|
},
|
||||||
"namespace": {
|
"namespace": {
|
||||||
"label": ""
|
"label": "Spazio dei nomi"
|
||||||
},
|
},
|
||||||
"className": {
|
"className": {
|
||||||
"label": "Nome classe"
|
"label": "Nome classe"
|
||||||
@@ -3761,7 +3762,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"search": ""
|
"search": "Cerca {count} ingresses"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
@@ -3771,7 +3772,7 @@
|
|||||||
"label": "Nome"
|
"label": "Nome"
|
||||||
},
|
},
|
||||||
"namespace": {
|
"namespace": {
|
||||||
"label": ""
|
"label": "Spazio del nome"
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"label": "Tipo"
|
"label": "Tipo"
|
||||||
@@ -3800,7 +3801,7 @@
|
|||||||
"label": "Nome"
|
"label": "Nome"
|
||||||
},
|
},
|
||||||
"namespace": {
|
"namespace": {
|
||||||
"label": ""
|
"label": "Spazio del nome"
|
||||||
},
|
},
|
||||||
"image": {
|
"image": {
|
||||||
"label": "Immagine"
|
"label": "Immagine"
|
||||||
@@ -3826,7 +3827,7 @@
|
|||||||
"label": "Nome"
|
"label": "Nome"
|
||||||
},
|
},
|
||||||
"namespace": {
|
"namespace": {
|
||||||
"label": ""
|
"label": "spazio del nome"
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"label": "tipo"
|
"label": "tipo"
|
||||||
@@ -3836,24 +3837,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"search": ""
|
"search": "Cerca {count} segreti"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"configmaps": {
|
"configmaps": {
|
||||||
"label": "",
|
"label": "ConfigMaps",
|
||||||
"field": {
|
"field": {
|
||||||
"name": {
|
"name": {
|
||||||
"label": "Nome"
|
"label": "Nome"
|
||||||
},
|
},
|
||||||
"namespace": {
|
"namespace": {
|
||||||
"label": ""
|
"label": "namespace"
|
||||||
},
|
},
|
||||||
"creationTimestamp": {
|
"creationTimestamp": {
|
||||||
"label": "Creato"
|
"label": "Creato"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"search": ""
|
"search": "Cerca {count} ConfigMaps"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"volumes": {
|
"volumes": {
|
||||||
@@ -3863,7 +3864,7 @@
|
|||||||
"label": "Nome"
|
"label": "Nome"
|
||||||
},
|
},
|
||||||
"namespace": {
|
"namespace": {
|
||||||
"label": ""
|
"label": "Namespace"
|
||||||
},
|
},
|
||||||
"accessModes": {
|
"accessModes": {
|
||||||
"label": "Modalità Di Accesso"
|
"label": "Modalità Di Accesso"
|
||||||
@@ -3872,7 +3873,7 @@
|
|||||||
"label": "Spazio di archiviazione"
|
"label": "Spazio di archiviazione"
|
||||||
},
|
},
|
||||||
"storageClassName": {
|
"storageClassName": {
|
||||||
"label": ""
|
"label": "Nome Classe Archivio"
|
||||||
},
|
},
|
||||||
"volumeMode": {
|
"volumeMode": {
|
||||||
"label": "Modalità Volume"
|
"label": "Modalità Volume"
|
||||||
@@ -3888,7 +3889,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"search": ""
|
"search": "Cerca {count} volumi"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -3989,7 +3990,7 @@
|
|||||||
"label": "Nodi"
|
"label": "Nodi"
|
||||||
},
|
},
|
||||||
"namespaces": {
|
"namespaces": {
|
||||||
"label": ""
|
"label": "Namespaces"
|
||||||
},
|
},
|
||||||
"ingresses": {
|
"ingresses": {
|
||||||
"label": "Ingressi"
|
"label": "Ingressi"
|
||||||
@@ -4001,7 +4002,7 @@
|
|||||||
"label": "pods"
|
"label": "pods"
|
||||||
},
|
},
|
||||||
"configmaps": {
|
"configmaps": {
|
||||||
"label": ""
|
"label": "ConfigMaps"
|
||||||
},
|
},
|
||||||
"secrets": {
|
"secrets": {
|
||||||
"label": "Secrets"
|
"label": "Secrets"
|
||||||
@@ -4092,7 +4093,7 @@
|
|||||||
"option": {
|
"option": {
|
||||||
"colorScheme": {
|
"colorScheme": {
|
||||||
"light": "Passa al tema chiaro",
|
"light": "Passa al tema chiaro",
|
||||||
"dark": "Passa alla modalità scura"
|
"dark": "Passa al tema scuro"
|
||||||
},
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"label": "Cambia lingua",
|
"label": "Cambia lingua",
|
||||||
|
|||||||
@@ -649,14 +649,14 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"option": {
|
"option": {
|
||||||
"existing": {
|
"existing": {
|
||||||
"title": "",
|
"title": "既存",
|
||||||
"label": ""
|
"label": "既存のアプリを選択"
|
||||||
},
|
},
|
||||||
"new": {
|
"new": {
|
||||||
"title": "",
|
"title": "新規作成",
|
||||||
"url": {
|
"url": {
|
||||||
"label": "",
|
"label": "アプリURL",
|
||||||
"description": ""
|
"description": "ダッシュボードからアクセスしたときにアプリが開くURL"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -676,9 +676,9 @@
|
|||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"action": {
|
"action": {
|
||||||
"add": "",
|
"add": "アプリにリンクする",
|
||||||
"remove": "",
|
"remove": "リンクを解除する",
|
||||||
"select": ""
|
"select": "リンクするアプリを選択する"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -709,7 +709,7 @@
|
|||||||
"description": "連携機能 \"{kind}\" は、検索エンジンで使用できます。検索エンジンを自動的に設定するには、これにチェックを入れてください。"
|
"description": "連携機能 \"{kind}\" は、検索エンジンで使用できます。検索エンジンを自動的に設定するには、これにチェックを入れてください。"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"sectionTitle": ""
|
"sectionTitle": "リンクされたアプリ"
|
||||||
},
|
},
|
||||||
"createApp": {
|
"createApp": {
|
||||||
"label": "アプリの作成",
|
"label": "アプリの作成",
|
||||||
@@ -1051,7 +1051,7 @@
|
|||||||
"add": "追加",
|
"add": "追加",
|
||||||
"apply": "適用",
|
"apply": "適用",
|
||||||
"backToOverview": "概要に戻る",
|
"backToOverview": "概要に戻る",
|
||||||
"change": "",
|
"change": "変更する",
|
||||||
"create": "作成",
|
"create": "作成",
|
||||||
"createAnother": "作成・新規入力",
|
"createAnother": "作成・新規入力",
|
||||||
"edit": "編集",
|
"edit": "編集",
|
||||||
@@ -1174,8 +1174,8 @@
|
|||||||
},
|
},
|
||||||
"unit": {
|
"unit": {
|
||||||
"speed": {
|
"speed": {
|
||||||
"kilometersPerHour": "",
|
"kilometersPerHour": "km/h",
|
||||||
"milesPerHour": ""
|
"milesPerHour": "mph"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1190,7 +1190,7 @@
|
|||||||
"label": "タイトル"
|
"label": "タイトル"
|
||||||
},
|
},
|
||||||
"customCssClasses": {
|
"customCssClasses": {
|
||||||
"label": ""
|
"label": "カスタム css クラス"
|
||||||
},
|
},
|
||||||
"borderColor": {
|
"borderColor": {
|
||||||
"label": "境界線の色"
|
"label": "境界線の色"
|
||||||
@@ -1780,7 +1780,7 @@
|
|||||||
"description": "現在の天気のみ表示"
|
"description": "現在の天気のみ表示"
|
||||||
},
|
},
|
||||||
"useImperialSpeed": {
|
"useImperialSpeed": {
|
||||||
"label": ""
|
"label": "風速にmphを使用する"
|
||||||
},
|
},
|
||||||
"location": {
|
"location": {
|
||||||
"label": "天候の場所"
|
"label": "天候の場所"
|
||||||
@@ -1800,12 +1800,12 @@
|
|||||||
"description": "日付がどのように見えるか"
|
"description": "日付がどのように見えるか"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"currentWindSpeed": "時速 {currentWindSpeed} km",
|
"currentWindSpeed": "時速 {currentWindSpeed} {unit}",
|
||||||
"dailyForecast": {
|
"dailyForecast": {
|
||||||
"sunrise": "日出",
|
"sunrise": "日出",
|
||||||
"sunset": "日没",
|
"sunset": "日没",
|
||||||
"maxWindSpeed": "最大風速: {maxWindSpeed} km/h",
|
"maxWindSpeed": "最大風速: {maxWindSpeed} {unit}",
|
||||||
"maxWindGusts": "最大瞬間風速: {maxWindGusts} km/h"
|
"maxWindGusts": "最大瞬間風速: {maxWindGusts} {unit}"
|
||||||
},
|
},
|
||||||
"kind": {
|
"kind": {
|
||||||
"clear": "晴れ",
|
"clear": "晴れ",
|
||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "不明",
|
"unknown": "不明",
|
||||||
"pending": "保留中",
|
"pending": "保留中",
|
||||||
"processing": "処理中",
|
"processing": "処理中",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "一部",
|
"partiallyAvailable": "一部",
|
||||||
"available": "利用可能",
|
"available": "利用可能",
|
||||||
"blacklisted": "ブラックリスト",
|
"blacklisted": "ブラックリスト",
|
||||||
@@ -3013,8 +3014,8 @@
|
|||||||
"integration": "連携機能",
|
"integration": "連携機能",
|
||||||
"app": "アプリ",
|
"app": "アプリ",
|
||||||
"group": "グループ",
|
"group": "グループ",
|
||||||
"searchEngine": "",
|
"searchEngine": "検索エンジン",
|
||||||
"media": ""
|
"media": "メディア"
|
||||||
},
|
},
|
||||||
"statisticLabel": {
|
"statisticLabel": {
|
||||||
"boards": "ボード",
|
"boards": "ボード",
|
||||||
@@ -3023,8 +3024,8 @@
|
|||||||
"authorization": "認可"
|
"authorization": "認可"
|
||||||
},
|
},
|
||||||
"heroBanner": {
|
"heroBanner": {
|
||||||
"title": "",
|
"title": "おかえりなさい",
|
||||||
"subtitle": ""
|
"subtitle": "{app} ボード"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"board": {
|
"board": {
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "알 수 없음",
|
"unknown": "알 수 없음",
|
||||||
"pending": "",
|
"pending": "",
|
||||||
"processing": "",
|
"processing": "",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "",
|
"partiallyAvailable": "",
|
||||||
"available": "",
|
"available": "",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Nežinoma",
|
"unknown": "Nežinoma",
|
||||||
"pending": "",
|
"pending": "",
|
||||||
"processing": "",
|
"processing": "",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Dalis",
|
"partiallyAvailable": "Dalis",
|
||||||
"available": "Galima",
|
"available": "Galima",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Nezināms",
|
"unknown": "Nezināms",
|
||||||
"pending": "",
|
"pending": "",
|
||||||
"processing": "",
|
"processing": "",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Daļējs",
|
"partiallyAvailable": "Daļējs",
|
||||||
"available": "Pieejams",
|
"available": "Pieejams",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Onbekend",
|
"unknown": "Onbekend",
|
||||||
"pending": "In afwachting",
|
"pending": "In afwachting",
|
||||||
"processing": "Verwerken",
|
"processing": "Verwerken",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Gedeeltelijk",
|
"partiallyAvailable": "Gedeeltelijk",
|
||||||
"available": "Beschikbaar",
|
"available": "Beschikbaar",
|
||||||
"blacklisted": "Geblacklist",
|
"blacklisted": "Geblacklist",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Ukjent",
|
"unknown": "Ukjent",
|
||||||
"pending": "Pågår",
|
"pending": "Pågår",
|
||||||
"processing": "Prosesserer",
|
"processing": "Prosesserer",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Delvis",
|
"partiallyAvailable": "Delvis",
|
||||||
"available": "Tilgjengelig",
|
"available": "Tilgjengelig",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Nieznany",
|
"unknown": "Nieznany",
|
||||||
"pending": "Oczekujący",
|
"pending": "Oczekujący",
|
||||||
"processing": "Przetwarzanie",
|
"processing": "Przetwarzanie",
|
||||||
|
"requested": "Żądanie",
|
||||||
"partiallyAvailable": "Częściowy",
|
"partiallyAvailable": "Częściowy",
|
||||||
"available": "Dostępne",
|
"available": "Dostępne",
|
||||||
"blacklisted": "Zablokowany",
|
"blacklisted": "Zablokowany",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"description": "Para começar, por favor selecione como você deseja configurar sua instância Homarr.",
|
"description": "Para começar, por favor selecione como você deseja configurar sua instância Homarr.",
|
||||||
"action": {
|
"action": {
|
||||||
"scratch": "Começar do zero",
|
"scratch": "Começar do zero",
|
||||||
"importOldmarr": "Importe de uma instância Homarr anterior a 1,0"
|
"importOldmarr": "Importe de uma instância Homarr anterior a 1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
@@ -307,12 +307,12 @@
|
|||||||
"name": "Nome",
|
"name": "Nome",
|
||||||
"members": "Membros",
|
"members": "Membros",
|
||||||
"homeBoard": {
|
"homeBoard": {
|
||||||
"label": "",
|
"label": "Quadro inicial",
|
||||||
"description": ""
|
"description": "Somente quadros acessíveis ao grupo podem ser selecionados"
|
||||||
},
|
},
|
||||||
"mobileBoard": {
|
"mobileBoard": {
|
||||||
"label": "",
|
"label": "Quadro para celular",
|
||||||
"description": ""
|
"description": "Somente quadros acessíveis ao grupo podem ser selecionados"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"permission": {
|
"permission": {
|
||||||
@@ -320,8 +320,8 @@
|
|||||||
"title": "Administrador",
|
"title": "Administrador",
|
||||||
"item": {
|
"item": {
|
||||||
"admin": {
|
"admin": {
|
||||||
"label": "",
|
"label": "Administrador",
|
||||||
"description": ""
|
"description": "Membros com essa permissão têm acesso total a todos os recursos e configurações"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -329,15 +329,15 @@
|
|||||||
"title": "Aplicativos",
|
"title": "Aplicativos",
|
||||||
"item": {
|
"item": {
|
||||||
"create": {
|
"create": {
|
||||||
"label": "",
|
"label": "Criar aplicativo",
|
||||||
"description": ""
|
"description": "Permitir que membros criem aplicativos"
|
||||||
},
|
},
|
||||||
"use-all": {
|
"use-all": {
|
||||||
"label": "",
|
"label": "Usar todas aplicações",
|
||||||
"description": ""
|
"description": "Permitir que os membros adicionem qualquer aplicativo aos seus quadros"
|
||||||
},
|
},
|
||||||
"modify-all": {
|
"modify-all": {
|
||||||
"label": "",
|
"label": "Modificar todos os aplicativos",
|
||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
"full-all": {
|
"full-all": {
|
||||||
@@ -410,7 +410,7 @@
|
|||||||
"item": {
|
"item": {
|
||||||
"view-logs": {
|
"view-logs": {
|
||||||
"label": "",
|
"label": "",
|
||||||
"description": ""
|
"description": "Permitir que os membros visualizem os registros"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -419,7 +419,7 @@
|
|||||||
"item": {
|
"item": {
|
||||||
"create": {
|
"create": {
|
||||||
"label": "",
|
"label": "",
|
||||||
"description": ""
|
"description": "Permitir que os membros criem mecanismos de busca"
|
||||||
},
|
},
|
||||||
"modify-all": {
|
"modify-all": {
|
||||||
"label": "",
|
"label": "",
|
||||||
@@ -547,19 +547,19 @@
|
|||||||
"title": "Aplicativos",
|
"title": "Aplicativos",
|
||||||
"noResults": {
|
"noResults": {
|
||||||
"title": "",
|
"title": "",
|
||||||
"action": ""
|
"action": "Crie seu primeiro aplicativo"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "",
|
"title": "Novo Aplicativo",
|
||||||
"notification": {
|
"notification": {
|
||||||
"success": {
|
"success": {
|
||||||
"title": "",
|
"title": "Criado com sucesso",
|
||||||
"message": ""
|
"message": "O aplicativo foi criado com sucesso"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"title": "",
|
"title": "Falha ao criar",
|
||||||
"message": ""
|
"message": "Não foi possível criar o aplicativo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -618,9 +618,9 @@
|
|||||||
"title": ""
|
"title": ""
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "",
|
"title": "Criar aplicativo",
|
||||||
"description": "",
|
"description": "Criar um aplicativo",
|
||||||
"action": ""
|
"action": "Abrir criação de aplicativo"
|
||||||
},
|
},
|
||||||
"add": ""
|
"add": ""
|
||||||
}
|
}
|
||||||
@@ -649,14 +649,14 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"option": {
|
"option": {
|
||||||
"existing": {
|
"existing": {
|
||||||
"title": "",
|
"title": "Existente",
|
||||||
"label": ""
|
"label": "Selecione um aplicativo existente"
|
||||||
},
|
},
|
||||||
"new": {
|
"new": {
|
||||||
"title": "",
|
"title": "Novo",
|
||||||
"url": {
|
"url": {
|
||||||
"label": "",
|
"label": "URL da aplicação",
|
||||||
"description": ""
|
"description": "A URL do aplicativo será aberta quando acessada a partir do painel de controle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -712,8 +712,8 @@
|
|||||||
"sectionTitle": ""
|
"sectionTitle": ""
|
||||||
},
|
},
|
||||||
"createApp": {
|
"createApp": {
|
||||||
"label": "",
|
"label": "Criar aplicativo",
|
||||||
"description": ""
|
"description": "Crie um aplicativo com o mesmo nome e ícone da integração. Deixe o campo de entrada abaixo vazio para criar o aplicativo com a URL de integração."
|
||||||
},
|
},
|
||||||
"appHref": {
|
"appHref": {
|
||||||
"placeholder": ""
|
"placeholder": ""
|
||||||
@@ -1973,8 +1973,8 @@
|
|||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"noData": "",
|
"noData": "",
|
||||||
"description": "",
|
"description": "Clique <here></here> para criar um aplicativo",
|
||||||
"quickCreate": ""
|
"quickCreate": "Criar aplicativo em tempo real"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"noIntegration": "",
|
"noIntegration": "",
|
||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Desconhecido",
|
"unknown": "Desconhecido",
|
||||||
"pending": "",
|
"pending": "",
|
||||||
"processing": "",
|
"processing": "",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Parcial",
|
"partiallyAvailable": "Parcial",
|
||||||
"available": "Disponível",
|
"available": "Disponível",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
@@ -2659,7 +2660,7 @@
|
|||||||
},
|
},
|
||||||
"onlyImportApps": {
|
"onlyImportApps": {
|
||||||
"label": "",
|
"label": "",
|
||||||
"description": ""
|
"description": "Só adiciona os aplicativos, o quadro precisa ser recriado manualmente"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
@@ -2692,8 +2693,8 @@
|
|||||||
},
|
},
|
||||||
"quickCreateApp": {
|
"quickCreateApp": {
|
||||||
"modal": {
|
"modal": {
|
||||||
"title": "",
|
"title": "Criar aplicativo em tempo real",
|
||||||
"createAndUse": ""
|
"createAndUse": "Criar e utilizar"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2911,7 +2912,7 @@
|
|||||||
"title": "",
|
"title": "",
|
||||||
"description": "",
|
"description": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"notice": ""
|
"notice": "Para fazer esta página desaparecer, crie um quadro e defina-o como quadro inicial"
|
||||||
},
|
},
|
||||||
"notFound": {
|
"notFound": {
|
||||||
"title": "",
|
"title": "",
|
||||||
@@ -4109,7 +4110,7 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"newApp": {
|
"newApp": {
|
||||||
"label": ""
|
"label": "Criar aplicativo"
|
||||||
},
|
},
|
||||||
"newIntegration": {
|
"newIntegration": {
|
||||||
"label": "",
|
"label": "",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Necunoscut",
|
"unknown": "Necunoscut",
|
||||||
"pending": "",
|
"pending": "",
|
||||||
"processing": "",
|
"processing": "",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Parțial",
|
"partiallyAvailable": "Parțial",
|
||||||
"available": "Disponibil",
|
"available": "Disponibil",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Неизвестно",
|
"unknown": "Неизвестно",
|
||||||
"pending": "В ожидании",
|
"pending": "В ожидании",
|
||||||
"processing": "Обработка",
|
"processing": "Обработка",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Частично доступно",
|
"partiallyAvailable": "Частично доступно",
|
||||||
"available": "Доступно",
|
"available": "Доступно",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Neznámy",
|
"unknown": "Neznámy",
|
||||||
"pending": "Čakajúce",
|
"pending": "Čakajúce",
|
||||||
"processing": "Spracovanie",
|
"processing": "Spracovanie",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Čiastočný",
|
"partiallyAvailable": "Čiastočný",
|
||||||
"available": "K dispozícii",
|
"available": "K dispozícii",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Neznano",
|
"unknown": "Neznano",
|
||||||
"pending": "",
|
"pending": "",
|
||||||
"processing": "",
|
"processing": "",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "",
|
"partiallyAvailable": "",
|
||||||
"available": "",
|
"available": "",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Okänd",
|
"unknown": "Okänd",
|
||||||
"pending": "",
|
"pending": "",
|
||||||
"processing": "",
|
"processing": "",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Delmängd",
|
"partiallyAvailable": "Delmängd",
|
||||||
"available": "Tillgänglig",
|
"available": "Tillgänglig",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Bilinmeyen",
|
"unknown": "Bilinmeyen",
|
||||||
"pending": "Bekleyen",
|
"pending": "Bekleyen",
|
||||||
"processing": "İşlemde",
|
"processing": "İşlemde",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Kısmi",
|
"partiallyAvailable": "Kısmi",
|
||||||
"available": "Mevcut",
|
"available": "Mevcut",
|
||||||
"blacklisted": "Engellenenler",
|
"blacklisted": "Engellenenler",
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Невідомо",
|
"unknown": "Невідомо",
|
||||||
"pending": "В очікуванні",
|
"pending": "В очікуванні",
|
||||||
"processing": "В обробці",
|
"processing": "В обробці",
|
||||||
|
"requested": "",
|
||||||
"partiallyAvailable": "Частково доступно",
|
"partiallyAvailable": "Частково доступно",
|
||||||
"available": "Доступно",
|
"available": "Доступно",
|
||||||
"blacklisted": "",
|
"blacklisted": "",
|
||||||
|
|||||||
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user