Compare commits

..
Author SHA1 Message Date
Meier Lukas 0b94219002 perf: try to improve nextjs dev performance 2025-05-27 22:46:53 +02:00
106 changed files with 2559 additions and 3802 deletions

No files matched your search

-2
View File
@@ -31,8 +31,6 @@ body:
label: Version label: Version
description: What version of Homarr are you running? description: What version of Homarr are you running?
options: options:
- 1.23.0
- 1.22.0
- 1.21.0 - 1.21.0
- 1.20.0 - 1.20.0
- 1.19.1 - 1.19.1
+4 -4
View File
@@ -6,8 +6,8 @@
**Thank you for your contribution. Please ensure that your pull request meets the following pull request:** **Thank you for your contribution. Please ensure that your pull request meets the following pull request:**
- [ ] Builds without warnings or errors (`pnpm build`, autofix with `pnpm format:fix`) - [ ] Builds without warnings or errors (``pnpm build``, autofix with ``pnpm format:fix``)
- [ ] Pull request targets `dev` branch - [ ] Pull request targets ``dev`` branch
- [ ] Commits follow the [conventional commits guideline](https://www.conventionalcommits.org/en/v1.0.0/) - [ ] Commits follow the [conventional commits guideline](https://www.conventionalcommits.org/en/v1.0.0/)
- [ ] No shorthand variable names are used (eg. `x`, `y`, `i` or any abbrevation) - [ ] No shorthand variable names are used (eg. ``x``, ``y``, ``i`` or any abbrevation)
- [ ] Documentation is up to date. Create a pull request [here](https://github.com/homarr-labs/documentation/).
-102
View File
@@ -1,105 +1,3 @@
## [1.23.0](https://github.com/homarr-labs/homarr/compare/v1.22.0...v1.23.0) (2025-06-06)
### Features
* **releases-widget:** add `import from docker` functionality ([#3130](https://github.com/homarr-labs/homarr/issues/3130)) ([7071d76](https://github.com/homarr-labs/homarr/commit/7071d76c526f1b59a955abcc595894975d574218))
### Bug Fixes
* Allow nullable on cpuModelName and default to "Unknown CPU" if null in OMV Integration ([#3310](https://github.com/homarr-labs/homarr/issues/3310)) ([3e7bf62](https://github.com/homarr-labs/homarr/commit/3e7bf62500f4ce0a8662104e2a656ea98cf2558f))
* csp prevents external stylesheets ([#3337](https://github.com/homarr-labs/homarr/issues/3337)) ([c7a21b1](https://github.com/homarr-labs/homarr/commit/c7a21b1a5f4e7c9d994d81b75a3fd4fa7c013daf))
* **deps:** update dependency @tabler/icons-react to ^3.34.0 ([#3303](https://github.com/homarr-labs/homarr/issues/3303)) ([5b354ac](https://github.com/homarr-labs/homarr/commit/5b354aca2047e8316b331e327cf6031690dcfef9))
* **deps:** update dependency dockerode to ^4.0.7 ([#3331](https://github.com/homarr-labs/homarr/issues/3331)) ([07f81ce](https://github.com/homarr-labs/homarr/commit/07f81cee92627e3b6ee4b2bbf084657f9a83ac34))
* **deps:** update dependency drizzle-orm to ^0.44.1 ([#3280](https://github.com/homarr-labs/homarr/issues/3280)) ([9895c1b](https://github.com/homarr-labs/homarr/commit/9895c1b3ff028a7ea7273ab2625620c9132f5213))
* **deps:** update dependency drizzle-orm to ^0.44.2 ([#3315](https://github.com/homarr-labs/homarr/issues/3315)) ([9ac9042](https://github.com/homarr-labs/homarr/commit/9ac90425647bf1f10d3f299f043c5d40067347bc))
* **deps:** update dependency fast-xml-parser to ^5.2.4 ([#3336](https://github.com/homarr-labs/homarr/issues/3336)) ([3ad72e6](https://github.com/homarr-labs/homarr/commit/3ad72e63b1adecd18c53897a6023f09d6f5d011c))
* **deps:** update dependency node-cron to ^4.1.0 ([#3286](https://github.com/homarr-labs/homarr/issues/3286)) ([1f40868](https://github.com/homarr-labs/homarr/commit/1f408685e69397fa9e3a69bd4331a8240da3713b))
* **deps:** update dependency sass to ^1.89.1 ([#3285](https://github.com/homarr-labs/homarr/issues/3285)) ([b608ffe](https://github.com/homarr-labs/homarr/commit/b608ffe4900a5dd97ceb2669464c477404d090e7))
* **deps:** update dependency swagger-ui-react to ^5.24.0 ([#3325](https://github.com/homarr-labs/homarr/issues/3325)) ([cf7336b](https://github.com/homarr-labs/homarr/commit/cf7336ba87c617578ab52e2ece94fd84df3fc623))
* **deps:** update dependency typescript-eslint to ^8.33.1 ([#3302](https://github.com/homarr-labs/homarr/issues/3302)) ([124e8de](https://github.com/homarr-labs/homarr/commit/124e8de6c584b0efca91d92079f8b3c400bccab2))
* **deps:** update dependency video.js to ^8.23.3 ([#3320](https://github.com/homarr-labs/homarr/issues/3320)) ([81509fd](https://github.com/homarr-labs/homarr/commit/81509fd96dfe0561964eb58d3d21c02400db2e8c))
* **deps:** update dependency zod to ^3.25.43 ([#3290](https://github.com/homarr-labs/homarr/issues/3290)) ([161fc86](https://github.com/homarr-labs/homarr/commit/161fc867703c4cf634eaaeec1b73f5e3cbfa7fec))
* **deps:** update dependency zod to ^3.25.45 ([#3291](https://github.com/homarr-labs/homarr/issues/3291)) ([3337cfa](https://github.com/homarr-labs/homarr/commit/3337cfa3c7e34debcccdbb95ee0bc39f2af98a91))
* **deps:** update dependency zod to ^3.25.46 ([#3292](https://github.com/homarr-labs/homarr/issues/3292)) ([1b1671e](https://github.com/homarr-labs/homarr/commit/1b1671e0e6d808469ba7f8185c4590cf2f2f6f44))
* **deps:** update dependency zod to ^3.25.47 ([#3296](https://github.com/homarr-labs/homarr/issues/3296)) ([94f176d](https://github.com/homarr-labs/homarr/commit/94f176d544272e0378e65255aba58b05d56019b2))
* **deps:** update dependency zod to ^3.25.48 ([#3297](https://github.com/homarr-labs/homarr/issues/3297)) ([ccd8a85](https://github.com/homarr-labs/homarr/commit/ccd8a85e0fcd2cf40dbfb6058787d24a02680b93))
* **deps:** update dependency zod to ^3.25.49 ([#3304](https://github.com/homarr-labs/homarr/issues/3304)) ([0362487](https://github.com/homarr-labs/homarr/commit/0362487156457ae825cac501e7d53db58c31ab0b))
* **deps:** update dependency zod to ^3.25.50 ([#3312](https://github.com/homarr-labs/homarr/issues/3312)) ([a0a0449](https://github.com/homarr-labs/homarr/commit/a0a0449c704ff57725ab5e0b459eb209fc929446))
* **deps:** update dependency zod to ^3.25.51 ([#3316](https://github.com/homarr-labs/homarr/issues/3316)) ([a542ee0](https://github.com/homarr-labs/homarr/commit/a542ee079871c7609ed97c76aa2017305adbfdc9))
* **deps:** update dependency zod to ^3.25.55 ([#3332](https://github.com/homarr-labs/homarr/issues/3332)) ([ed3f848](https://github.com/homarr-labs/homarr/commit/ed3f848e8afb57cc1b9249551f0260eb52bbd4a6))
* **deps:** update tanstack-query monorepo to ^5.79.1 ([#3300](https://github.com/homarr-labs/homarr/issues/3300)) ([a3deed3](https://github.com/homarr-labs/homarr/commit/a3deed3b090aed0df0f45a9bd04c006640691e05))
* **deps:** update tanstack-query monorepo to ^5.79.2 ([#3301](https://github.com/homarr-labs/homarr/issues/3301)) ([a0585e3](https://github.com/homarr-labs/homarr/commit/a0585e3dd7d9cfbccf8ff5535ae2c753a8b92764))
* **deps:** update tanstack-query monorepo to ^5.80.0 ([#3307](https://github.com/homarr-labs/homarr/issues/3307)) ([df2a9a5](https://github.com/homarr-labs/homarr/commit/df2a9a5329e06847904af56a0d058c0da59f6380))
* **deps:** update tanstack-query monorepo to ^5.80.2 ([#3308](https://github.com/homarr-labs/homarr/issues/3308)) ([cae0cad](https://github.com/homarr-labs/homarr/commit/cae0cad92f68f716887e077194d4e30402466d57))
* **deps:** update tanstack-query monorepo to ^5.80.3 ([#3317](https://github.com/homarr-labs/homarr/issues/3317)) ([c051a42](https://github.com/homarr-labs/homarr/commit/c051a42791de4eae4baf20360efe5b0c8925c22b))
* **deps:** update tanstack-query monorepo to ^5.80.5 ([#3321](https://github.com/homarr-labs/homarr/issues/3321)) ([cb77a7d](https://github.com/homarr-labs/homarr/commit/cb77a7dc6d60560250765d129aac9bd236693e2b))
* **deps:** update tanstack-query monorepo to ^5.80.6 ([#3326](https://github.com/homarr-labs/homarr/issues/3326)) ([61553cf](https://github.com/homarr-labs/homarr/commit/61553cfc8e48a1179a148007fb970b1f4e6d509c))
* **deps:** update testcontainers-node monorepo to v11 (major) ([#3250](https://github.com/homarr-labs/homarr/issues/3250)) ([0d1e3a6](https://github.com/homarr-labs/homarr/commit/0d1e3a625a04abd5f667640ab40e01d28e017488))
* **deps:** update tiptap monorepo to v2.13.0 ([#3323](https://github.com/homarr-labs/homarr/issues/3323)) ([dac8234](https://github.com/homarr-labs/homarr/commit/dac82347e86dd7faacb24adbfc61ace9c917b8cd))
* **deps:** update tiptap monorepo to v2.14.0 ([#3334](https://github.com/homarr-labs/homarr/issues/3334)) ([6274a66](https://github.com/homarr-labs/homarr/commit/6274a66048163513e3faaf7011d7d6aa6ad9f161))
* **deps:** update trpc monorepo to ^11.2.0 ([#3293](https://github.com/homarr-labs/homarr/issues/3293)) ([f0ba693](https://github.com/homarr-labs/homarr/commit/f0ba693077a0c7f7897c81f02ad66b77fce52f1c))
* **deps:** update trpc monorepo to ^11.3.0 ([#3322](https://github.com/homarr-labs/homarr/issues/3322)) ([74aa561](https://github.com/homarr-labs/homarr/commit/74aa561f470f0f76421f356998caa91dc8bb65a1))
* **deps:** update trpc monorepo to ^11.3.1 ([#3329](https://github.com/homarr-labs/homarr/issues/3329)) ([8484daa](https://github.com/homarr-labs/homarr/commit/8484daaf7516ad72dd9e7de3117b5e0d50415dbb))
* **nextcloud:** correct calendar event date mapping and support showU… ([#3330](https://github.com/homarr-labs/homarr/issues/3330)) ([ffd2f80](https://github.com/homarr-labs/homarr/commit/ffd2f802bb777f13eac1ee45e1b5f0ca2b596672))
## [1.22.0](https://github.com/homarr-labs/homarr/compare/v1.21.0...v1.22.0) (2025-05-30)
### Features
* allow custom protocols for app href ([#2575](https://github.com/homarr-labs/homarr/issues/2575)) ([09af0a7](https://github.com/homarr-labs/homarr/commit/09af0a76f64c2fec512ba30726827208d5c3ad95))
* **cluster-health:** add visibility options ([#3210](https://github.com/homarr-labs/homarr/issues/3210)) ([939df8f](https://github.com/homarr-labs/homarr/commit/939df8f6d11bb36f71854425e4fdc4ca0724da31))
* **downloads:** add option to limit amount of items ([#3205](https://github.com/homarr-labs/homarr/issues/3205)) ([2dc871e](https://github.com/homarr-labs/homarr/commit/2dc871e531b58aee25db26ab7c5e94626517db36))
### Bug Fixes
* **boards:** app select modal images rendering issue ([#3218](https://github.com/homarr-labs/homarr/issues/3218)) ([85e98d2](https://github.com/homarr-labs/homarr/commit/85e98d2b603abe18b37fa74bf50836422b72c9e1))
* **calendar:** load events for all visible days of calendar month ([#3216](https://github.com/homarr-labs/homarr/issues/3216)) ([437ecd1](https://github.com/homarr-labs/homarr/commit/437ecd10239c70e5a8c4d34119076ab155a2159e))
* **calendar:** remove scrollbar for 2x2 ([#3227](https://github.com/homarr-labs/homarr/issues/3227)) ([1d27da2](https://github.com/homarr-labs/homarr/commit/1d27da22303b2133bb1a7c59102b7c292bc34833))
* **cluster-health:** storage items multiply ([#3206](https://github.com/homarr-labs/homarr/issues/3206)) ([1114934](https://github.com/homarr-labs/homarr/commit/11149348fcbe9002bba33763e0b03aed50d20f71))
* csp prevents fonts to load ([#3225](https://github.com/homarr-labs/homarr/issues/3225)) ([05e159f](https://github.com/homarr-labs/homarr/commit/05e159f5662f04dd298eb13816fe1768d7ef9312))
* **deps:** update dependency @ianvs/prettier-plugin-sort-imports to ^4.4.2 ([#3258](https://github.com/homarr-labs/homarr/issues/3258)) ([4a184d2](https://github.com/homarr-labs/homarr/commit/4a184d23f25a8a06b36cd8c6d9b2a65118399d9f))
* **deps:** update dependency @kubernetes/client-node to ^1.3.0 ([#3261](https://github.com/homarr-labs/homarr/issues/3261)) ([635c9a2](https://github.com/homarr-labs/homarr/commit/635c9a2687cf2b9a09a3093eb638ef772eddfb03))
* **deps:** update dependency @t3-oss/env-nextjs to ^0.13.6 ([#3244](https://github.com/homarr-labs/homarr/issues/3244)) ([8966e2a](https://github.com/homarr-labs/homarr/commit/8966e2ad8a2025dfc91934ccf825bd76c08dc333))
* **deps:** update dependency drizzle-orm to ^0.44.0 ([#3256](https://github.com/homarr-labs/homarr/issues/3256)) ([09b49a2](https://github.com/homarr-labs/homarr/commit/09b49a242dd5d3d34bab758f190ab948b6fc7566))
* **deps:** update dependency flag-icons to ^7.4.0 ([#3265](https://github.com/homarr-labs/homarr/issues/3265)) ([a0eda74](https://github.com/homarr-labs/homarr/commit/a0eda74fa472446c5b93c1b65829e65670620ab6))
* **deps:** update dependency flag-icons to ^7.5.0 ([#3268](https://github.com/homarr-labs/homarr/issues/3268)) ([2320d3c](https://github.com/homarr-labs/homarr/commit/2320d3c5674413e18e9bb42fd004ada016c5e605))
* **deps:** update dependency jotai to ^2.12.5 ([#3243](https://github.com/homarr-labs/homarr/issues/3243)) ([9c32385](https://github.com/homarr-labs/homarr/commit/9c323850bc49df5f6fc3437678faab0c7eab2e53))
* **deps:** update dependency ldapts to v8.0.1 ([#3264](https://github.com/homarr-labs/homarr/issues/3264)) ([bac4f64](https://github.com/homarr-labs/homarr/commit/bac4f642574caf5f170d20353c036396698e5963))
* **deps:** update dependency node-cron to v4 ([#3075](https://github.com/homarr-labs/homarr/issues/3075)) ([109f9e7](https://github.com/homarr-labs/homarr/commit/109f9e79793b36b46ab1987a56f7c206e21d32e4))
* **deps:** update dependency octokit to ^5.0.3 ([#3249](https://github.com/homarr-labs/homarr/issues/3249)) ([3253b2f](https://github.com/homarr-labs/homarr/commit/3253b2f138a9e911b50c952e37fb4afb1f71446d))
* **deps:** update dependency trpc-to-openapi to ^2.3.1 ([#3046](https://github.com/homarr-labs/homarr/issues/3046)) ([640bf7b](https://github.com/homarr-labs/homarr/commit/640bf7b939b201d9905f5119af0dc0da6120b98c))
* **deps:** update dependency typescript-eslint to ^8.33.0 ([#3247](https://github.com/homarr-labs/homarr/issues/3247)) ([8b553b8](https://github.com/homarr-labs/homarr/commit/8b553b80e61ab608d01d890d26a8ea5bae3f38b8))
* **deps:** update dependency zod to ^3.25.24 ([#3195](https://github.com/homarr-labs/homarr/issues/3195)) ([0b224e8](https://github.com/homarr-labs/homarr/commit/0b224e8835d4fc5a292a624c8af8d874216b703a))
* **deps:** update dependency zod to ^3.25.28 ([#3201](https://github.com/homarr-labs/homarr/issues/3201)) ([e0a9e4f](https://github.com/homarr-labs/homarr/commit/e0a9e4f97c9dddb95fbc0b2a65acfd802dac9c19))
* **deps:** update dependency zod to ^3.25.29 ([#3237](https://github.com/homarr-labs/homarr/issues/3237)) ([81ca6ef](https://github.com/homarr-labs/homarr/commit/81ca6ef7b2767fb0bc285ffa53998e2c586225ec))
* **deps:** update dependency zod to ^3.25.30 ([#3239](https://github.com/homarr-labs/homarr/issues/3239)) ([3062948](https://github.com/homarr-labs/homarr/commit/30629489396668125dec7d93fcb0e733099c35fe))
* **deps:** update dependency zod to ^3.25.31 ([#3254](https://github.com/homarr-labs/homarr/issues/3254)) ([e2e4656](https://github.com/homarr-labs/homarr/commit/e2e46560010a94603852ea90a0fc63fabd473198))
* **deps:** update dependency zod to ^3.25.32 ([#3255](https://github.com/homarr-labs/homarr/issues/3255)) ([9c3a46c](https://github.com/homarr-labs/homarr/commit/9c3a46c071e169459ec7b8c9ba1647b4b354cbea))
* **deps:** update dependency zod to ^3.25.34 ([#3259](https://github.com/homarr-labs/homarr/issues/3259)) ([c1324ea](https://github.com/homarr-labs/homarr/commit/c1324eab76795dabc0a41315191f9ab6a73ba501))
* **deps:** update dependency zod to ^3.25.36 ([#3262](https://github.com/homarr-labs/homarr/issues/3262)) ([48ce2af](https://github.com/homarr-labs/homarr/commit/48ce2af4c182985a7721b65535bcf1b41f1cdcba))
* **deps:** update dependency zod to ^3.25.39 ([#3270](https://github.com/homarr-labs/homarr/issues/3270)) ([862d07d](https://github.com/homarr-labs/homarr/commit/862d07db68e7624342cb81241ef9924e6407fda8))
* **deps:** update dependency zod to ^3.25.41 ([#3272](https://github.com/homarr-labs/homarr/issues/3272)) ([5bf20aa](https://github.com/homarr-labs/homarr/commit/5bf20aa3538503de1d4f4d04f9de3e3b92442d75))
* **deps:** update dependency zod to ^3.25.42 ([#3274](https://github.com/homarr-labs/homarr/issues/3274)) ([608280e](https://github.com/homarr-labs/homarr/commit/608280e670432098ce466cfb8fa2b315bf35ead2))
* **deps:** update mantine monorepo to ^8.0.2 ([#3245](https://github.com/homarr-labs/homarr/issues/3245)) ([816544a](https://github.com/homarr-labs/homarr/commit/816544a996656ccf7cbacf76aff83d013ba21ad1))
* **deps:** update nextjs monorepo to v15.3.3 ([#3269](https://github.com/homarr-labs/homarr/issues/3269)) ([d72a804](https://github.com/homarr-labs/homarr/commit/d72a804ad9feb24a2b067712128a6566930990a6))
* **deps:** update tanstack-query monorepo to ^5.77.0 ([#3204](https://github.com/homarr-labs/homarr/issues/3204)) ([ec42f26](https://github.com/homarr-labs/homarr/commit/ec42f265a04f0f96105d6b1318becd5cc94aa8fb))
* **deps:** update tanstack-query monorepo to ^5.77.1 ([#3229](https://github.com/homarr-labs/homarr/issues/3229)) ([5ea7a20](https://github.com/homarr-labs/homarr/commit/5ea7a206772924faf1f842c80033954c853b3a02))
* **deps:** update tanstack-query monorepo to ^5.77.2 ([#3232](https://github.com/homarr-labs/homarr/issues/3232)) ([7118690](https://github.com/homarr-labs/homarr/commit/71186908b96f717c6f8c528b0f983e1792a7a885))
* **deps:** update tanstack-query monorepo to ^5.79.0 ([#3263](https://github.com/homarr-labs/homarr/issues/3263)) ([1f9983f](https://github.com/homarr-labs/homarr/commit/1f9983f16db8677b8c244b63306ea4afd2195d94))
* **deps:** update trpc monorepo to ^11.1.3 ([#3240](https://github.com/homarr-labs/homarr/issues/3240)) ([78f98ed](https://github.com/homarr-labs/homarr/commit/78f98edc4145e27e2daa269121173905c642c139))
* **deps:** update trpc monorepo to ^11.1.4 ([#3260](https://github.com/homarr-labs/homarr/issues/3260)) ([bfd7b70](https://github.com/homarr-labs/homarr/commit/bfd7b706182340eaa1db70a3c6cdcc935aee8df4))
* **deps:** update turbo monorepo to ^2.5.4 ([#3279](https://github.com/homarr-labs/homarr/issues/3279)) ([c8202ab](https://github.com/homarr-labs/homarr/commit/c8202ab3c676bf796c6e7aff2ebfef224a0124c8))
* **downloads:** upspeed font size wrong ([#3214](https://github.com/homarr-labs/homarr/issues/3214)) ([c67916f](https://github.com/homarr-labs/homarr/commit/c67916f72eded64133fd458184a599f32d8fc63c))
* **media-request-stats:** users squeezed together ([#3213](https://github.com/homarr-labs/homarr/issues/3213)) ([25bd387](https://github.com/homarr-labs/homarr/commit/25bd3879db9260bbd24a1338cc5f26652ed78d72))
* navigating back results in navigation back to first page opened ([#3224](https://github.com/homarr-labs/homarr/issues/3224)) ([cbbf0c8](https://github.com/homarr-labs/homarr/commit/cbbf0c8dcf685e31e70bb62f04b1065dab90e18d))
* reduce logging ([#3200](https://github.com/homarr-labs/homarr/issues/3200)) ([be8b332](https://github.com/homarr-labs/homarr/commit/be8b332f942beb3af181ee6f39558882ed640a0e))
* **search:** removing server search engine causes loading error ([#3252](https://github.com/homarr-labs/homarr/issues/3252)) ([10f720d](https://github.com/homarr-labs/homarr/commit/10f720d74c6cc70cbf217e8daf8cc7878ac6d712))
* **visual:** check for creator board permission is wrong ([#3212](https://github.com/homarr-labs/homarr/issues/3212)) ([f7e5e82](https://github.com/homarr-labs/homarr/commit/f7e5e823d5dee36ea11f243a0da7d84a1f363419))
### Performance Improvements
* cache middleware onboarding and language checks ([#3197](https://github.com/homarr-labs/homarr/issues/3197)) ([2afa9b6](https://github.com/homarr-labs/homarr/commit/2afa9b67274edd79b52598dc0d22437048a67e2c))
## [1.21.0](https://github.com/homarr-labs/homarr/compare/v1.20.0...v1.21.0) (2025-05-23) ## [1.21.0](https://github.com/homarr-labs/homarr/compare/v1.20.0...v1.21.0) (2025-05-23)
### Features ### Features
+31 -7
View File
@@ -17,20 +17,45 @@ const withNextIntl = createNextIntlPlugin({
requestConfig: "../../packages/translation/src/request.ts", requestConfig: "../../packages/translation/src/request.ts",
}); });
interface _WebpackConfig {
module: {
rules: {
test: RegExp;
loader: string;
}[];
};
}
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: "standalone", output: "standalone",
reactStrictMode: true, reactStrictMode: true,
/** We already do linting and typechecking as separate tasks in CI */ /** We already do linting and typechecking as separate tasks in CI */
eslint: { ignoreDuringBuilds: true }, eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true }, typescript: { ignoreBuildErrors: true },
/** /*webpack: (config: WebpackConfig, { isServer }) => {
* dockerode is required in the external server packages because of https://github.com/homarr-labs/homarr/issues/612 if (isServer) {
*/ config.module.rules.push({
serverExternalPackages: ["dockerode"], test: /\.node$/,
loader: "node-loader",
});
}
return config;
},*/
experimental: { experimental: {
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"], optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
}, },
transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"], serverExternalPackages: [
"dockerode",
"@kubernetes/client-node",
"@ctrl/deluge",
"@ctrl/qbittorrent",
"@ctrl/transmission",
"node-ical",
"tsdav",
"proxmox-api",
"@jellyfin/sdk",
],
images: { images: {
domains: ["cdn.jsdelivr.net"], domains: ["cdn.jsdelivr.net"],
}, },
@@ -47,13 +72,12 @@ const nextConfig: NextConfig = {
script-src * 'unsafe-inline' 'unsafe-eval'; script-src * 'unsafe-inline' 'unsafe-eval';
base-uri 'self'; base-uri 'self';
connect-src *; connect-src *;
style-src * 'unsafe-inline'; style-src 'self' 'unsafe-inline';
frame-ancestors *; frame-ancestors *;
frame-src *; frame-src *;
form-action 'self'; form-action 'self';
img-src * data:; img-src * data:;
font-src * data:; font-src * data:;
media-src * data:;
` `
.replace(/\s{2,}/g, " ") .replace(/\s{2,}/g, " ")
.trim(), .trim(),
+23 -23
View File
@@ -48,21 +48,21 @@
"@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.1.0", "@mantine/colors-generator": "^8.0.2",
"@mantine/core": "^8.1.0", "@mantine/core": "^8.0.2",
"@mantine/dropzone": "^8.1.0", "@mantine/dropzone": "^8.0.2",
"@mantine/hooks": "^8.1.0", "@mantine/hooks": "^8.0.2",
"@mantine/modals": "^8.1.0", "@mantine/modals": "^8.0.2",
"@mantine/tiptap": "^8.1.0", "@mantine/tiptap": "^8.0.2",
"@million/lint": "1.0.14", "@million/lint": "1.0.14",
"@tabler/icons-react": "^3.34.0", "@tabler/icons-react": "^3.33.0",
"@tanstack/react-query": "^5.80.6", "@tanstack/react-query": "^5.77.2",
"@tanstack/react-query-devtools": "^5.80.6", "@tanstack/react-query-devtools": "^5.77.2",
"@tanstack/react-query-next-experimental": "^5.80.6", "@tanstack/react-query-next-experimental": "^5.77.2",
"@trpc/client": "^11.3.1", "@trpc/client": "^11.1.3",
"@trpc/next": "^11.3.1", "@trpc/next": "^11.1.3",
"@trpc/react-query": "^11.3.1", "@trpc/react-query": "^11.1.3",
"@trpc/server": "^11.3.1", "@trpc/server": "^11.1.3",
"@xterm/addon-canvas": "^0.7.0", "@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "0.10.0", "@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
@@ -70,35 +70,35 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"flag-icons": "^7.5.0", "flag-icons": "^7.3.2",
"glob": "^11.0.2", "glob": "^11.0.2",
"jotai": "^2.12.5", "jotai": "^2.12.5",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "15.3.3", "next": "15.3.2",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.17.0",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-simple-code-editor": "^0.14.1", "react-simple-code-editor": "^0.14.1",
"sass": "^1.89.2", "sass": "^1.89.0",
"superjson": "2.2.2", "superjson": "2.2.2",
"swagger-ui-react": "^5.24.1", "swagger-ui-react": "^5.22.0",
"use-deep-compare-effect": "^1.8.1", "use-deep-compare-effect": "^1.8.1",
"zod": "^3.25.62" "zod": "^3.25.30"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.1", "@types/chroma-js": "3.1.1",
"@types/node": "^22.15.31", "@types/node": "^22.15.21",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"@types/react": "19.1.8", "@types/react": "19.1.6",
"@types/react-dom": "19.1.6", "@types/react-dom": "19.1.5",
"@types/swagger-ui-react": "^5.18.0", "@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"node-loader": "^2.1.0", "node-loader": "^2.1.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
+1 -37
View File
@@ -1,16 +1,13 @@
import Link from "next/link"; import Link from "next/link";
import { Card, Group, SimpleGrid, Space, Stack, Text, Title } from "@mantine/core"; import { Card, Group, SimpleGrid, Space, Stack, Text } from "@mantine/core";
import { IconArrowRight } from "@tabler/icons-react"; import { IconArrowRight } from "@tabler/icons-react";
import { api } from "@homarr/api/server"; import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next"; import { auth } from "@homarr/auth/next";
import { isProviderEnabled } from "@homarr/auth/server"; import { isProviderEnabled } from "@homarr/auth/server";
import { getIntegrationName } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server"; import { getScopedI18n } from "@homarr/translation/server";
import { IntegrationAvatar } from "@homarr/ui";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
import { createMetaTitle } from "~/metadata"; import { createMetaTitle } from "~/metadata";
import { HeroBanner } from "./_components/hero-banner"; import { HeroBanner } from "./_components/hero-banner";
@@ -33,8 +30,6 @@ export async function generateMetadata() {
export default async function ManagementPage() { export default async function ManagementPage() {
const statistics = await api.home.getStats(); const statistics = await api.home.getStats();
const session = await auth(); const session = await auth();
const isAdmin = session?.user.permissions.includes("admin") ?? false;
const suggestions = isAdmin ? await api.integration.suggestedIntegrations().catch(catchTrpcNotFound) : [];
const t = await getScopedI18n("management.page.home"); const t = await getScopedI18n("management.page.home");
const links: LinkProps[] = [ const links: LinkProps[] = [
@@ -107,37 +102,6 @@ export default async function ManagementPage() {
), ),
)} )}
</SimpleGrid> </SimpleGrid>
{suggestions.length >= 1 && (
<>
<Space h="md" />
<Stack gap="sm">
<Stack gap={0}>
<Title order={3}>Suggested integrations</Title>
<Text c="dimmed">Based on your Docker containers</Text>
</Stack>
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3 }}>
{suggestions.slice(0, 6).map((suggestion) => (
<Card key={suggestion.kind} component={Link} href="#" radius="lg">
<Group justify="space-between">
<Group>
<IntegrationAvatar kind={suggestion.kind} size="sm" />
<Stack gap={0}>
<Text size="md" fw={500}>
{getIntegrationName(suggestion.kind)}
</Text>
<Text c="dimmed" size="sm">
Show active media streams
</Text>
</Stack>
</Group>
<IconArrowRight />
</Group>
</Card>
))}
</SimpleGrid>
</Stack>
</>
)}
</> </>
); );
} }
+3 -3
View File
@@ -44,12 +44,12 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.15.31", "@types/node": "^22.15.21",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"esbuild": "^0.25.5", "esbuild": "^0.25.5",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"tsx": "4.20.1", "tsx": "4.19.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+2 -2
View File
@@ -26,7 +26,7 @@
"@homarr/redis": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"tsx": "4.20.1", "tsx": "4.19.4",
"ws": "^8.18.2" "ws": "^8.18.2"
}, },
"devDependencies": { "devDependencies": {
@@ -35,7 +35,7 @@
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"esbuild": "^0.25.5", "esbuild": "^0.25.5",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
+9 -9
View File
@@ -1,6 +1,6 @@
{ {
"name": "homarr", "name": "homarr",
"version": "1.23.0", "version": "1.21.0",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "cross-env CI=true turbo build", "build": "cross-env CI=true turbo build",
@@ -38,22 +38,22 @@
"@semantic-release/github": "^11.0.3", "@semantic-release/github": "^11.0.3",
"@semantic-release/npm": "^12.0.1", "@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.3", "@semantic-release/release-notes-generator": "^14.0.3",
"@turbo/gen": "^2.5.4", "@turbo/gen": "^2.5.3",
"@vitejs/plugin-react": "^4.5.2", "@vitejs/plugin-react": "^4.5.0",
"@vitest/coverage-v8": "^3.2.3", "@vitest/coverage-v8": "^3.1.4",
"@vitest/ui": "^3.2.3", "@vitest/ui": "^3.1.4",
"conventional-changelog-conventionalcommits": "^9.0.0", "conventional-changelog-conventionalcommits": "^9.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"jsdom": "^26.1.0", "jsdom": "^26.1.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"semantic-release": "^24.2.5", "semantic-release": "^24.2.5",
"testcontainers": "^11.0.3", "testcontainers": "^10.28.0",
"turbo": "^2.5.4", "turbo": "^2.5.3",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.3" "vitest": "^3.1.4"
}, },
"packageManager": "pnpm@10.12.1", "packageManager": "pnpm@10.11.0",
"engines": { "engines": {
"node": ">=22.16.0" "node": ">=22.16.0"
}, },
+1 -1
View File
@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+9 -9
View File
@@ -40,25 +40,25 @@
"@homarr/request-handler": "workspace:^0.1.0", "@homarr/request-handler": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.3.0", "@kubernetes/client-node": "^1.2.0",
"@tanstack/react-query": "^5.80.6", "@tanstack/react-query": "^5.77.2",
"@trpc/client": "^11.3.1", "@trpc/client": "^11.1.3",
"@trpc/react-query": "^11.3.1", "@trpc/react-query": "^11.1.3",
"@trpc/server": "^11.3.1", "@trpc/server": "^11.1.3",
"@trpc/tanstack-react-query": "^11.3.1", "@trpc/tanstack-react-query": "^11.1.3",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"next": "15.3.3", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"superjson": "2.2.2", "superjson": "2.2.2",
"trpc-to-openapi": "^2.3.1", "trpc-to-openapi": "^2.3.1",
"zod": "^3.25.62" "zod": "^3.25.30"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
+25 -46
View File
@@ -1,52 +1,31 @@
import { apiKeysRouter } from "./router/apiKeys"; import { lazy } from "@trpc/server";
import { appRouter as innerAppRouter } from "./router/app";
import { boardRouter } from "./router/board";
import { certificateRouter } from "./router/certificates/certificate-router";
import { cronJobsRouter } from "./router/cron-jobs";
import { dockerRouter } from "./router/docker/docker-router";
import { groupRouter } from "./router/group";
import { homeRouter } from "./router/home";
import { iconsRouter } from "./router/icons";
import { importRouter } from "./router/import/import-router";
import { integrationRouter } from "./router/integration/integration-router";
import { inviteRouter } from "./router/invite";
import { kubernetesRouter } from "./router/kubernetes/router/kubernetes-router";
import { locationRouter } from "./router/location";
import { logRouter } from "./router/log";
import { mediaRouter } from "./router/medias/media-router";
import { onboardRouter } from "./router/onboard/onboard-router";
import { searchEngineRouter } from "./router/search-engine/search-engine-router";
import { sectionRouter } from "./router/section/section-router";
import { serverSettingsRouter } from "./router/serverSettings";
import { updateCheckerRouter } from "./router/update-checker";
import { userRouter } from "./router/user";
import { widgetRouter } from "./router/widgets";
import { createTRPCRouter } from "./trpc"; import { createTRPCRouter } from "./trpc";
export const appRouter = createTRPCRouter({ export const appRouter = createTRPCRouter({
user: userRouter, user: lazy(() => import("./router/user").then((mod) => mod.userRouter)),
group: groupRouter, group: lazy(() => import("./router/group").then((mod) => mod.groupRouter)),
invite: inviteRouter, invite: lazy(() => import("./router/invite").then((mod) => mod.inviteRouter)),
integration: integrationRouter, integration: lazy(() => import("./router/integration/integration-router").then((mod) => mod.integrationRouter)),
board: boardRouter, board: lazy(() => import("./router/board").then((mod) => mod.boardRouter)),
section: sectionRouter, section: lazy(() => import("./router/section/section-router").then((mod) => mod.sectionRouter)),
app: innerAppRouter, app: lazy(() => import("./router/app").then((mod) => mod.appRouter)),
searchEngine: searchEngineRouter, searchEngine: lazy(() => import("./router/search-engine/search-engine-router").then((mod) => mod.searchEngineRouter)),
widget: widgetRouter, widget: lazy(() => import("./router/widgets").then((mod) => mod.widgetRouter)),
location: locationRouter, location: lazy(() => import("./router/location").then((mod) => mod.locationRouter)),
log: logRouter, log: lazy(() => import("./router/log").then((mod) => mod.logRouter)),
icon: iconsRouter, icon: lazy(() => import("./router/icons").then((mod) => mod.iconsRouter)),
import: importRouter, import: lazy(() => import("./router/import/import-router").then((mod) => mod.importRouter)),
onboard: onboardRouter, onboard: lazy(() => import("./router/onboard/onboard-router").then((mod) => mod.onboardRouter)),
home: homeRouter, home: lazy(() => import("./router/home").then((mod) => mod.homeRouter)),
docker: dockerRouter, docker: lazy(() => import("./router/docker/docker-router").then((mod) => mod.dockerRouter)),
kubernetes: kubernetesRouter, kubernetes: lazy(() => import("./router/kubernetes/router/kubernetes-router").then((mod) => mod.kubernetesRouter)),
serverSettings: serverSettingsRouter, serverSettings: lazy(() => import("./router/serverSettings").then((mod) => mod.serverSettingsRouter)),
cronJobs: cronJobsRouter, cronJobs: lazy(() => import("./router/cron-jobs").then((mod) => mod.cronJobsRouter)),
apiKeys: apiKeysRouter, apiKeys: lazy(() => import("./router/apiKeys").then((mod) => mod.apiKeysRouter)),
media: mediaRouter, media: lazy(() => import("./router/medias/media-router").then((mod) => mod.mediaRouter)),
updateChecker: updateCheckerRouter, updateChecker: lazy(() => import("./router/update-checker").then((mod) => mod.updateCheckerRouter)),
certificates: certificateRouter, certificates: lazy(() => import("./router/certificates/certificate-router").then((mod) => mod.certificateRouter)),
}); });
// export type definition of API // export type definition of API
@@ -14,19 +14,15 @@ import {
integrationUserPermissions, integrationUserPermissions,
searchEngines, searchEngines,
} from "@homarr/db/schema"; } from "@homarr/db/schema";
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions"; import type { IntegrationSecretKind } from "@homarr/definitions";
import { import {
getIconUrl, getIconUrl,
getIntegrationKindsByCategory, getIntegrationKindsByCategory,
getIntegrationName,
getPermissionsWithParents, getPermissionsWithParents,
integrationDefs, integrationDefs,
integrationKinds, integrationKinds,
integrationSecretKindObject, integrationSecretKindObject,
} from "@homarr/definitions"; } from "@homarr/definitions";
import type { ContainerInfo, Port } from "@homarr/docker";
import { DockerSingleton } from "@homarr/docker";
import { env } from "@homarr/docker/env";
import { createIntegrationAsync } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
import { byIdSchema } from "@homarr/validation/common"; import { byIdSchema } from "@homarr/validation/common";
@@ -531,169 +527,8 @@ export const integrationRouter = createTRPCRouter({
const integrationInstance = await createIntegrationAsync(ctx.integration); const integrationInstance = await createIntegrationAsync(ctx.integration);
return await integrationInstance.searchAsync(encodeURI(input.query)); return await integrationInstance.searchAsync(encodeURI(input.query));
}), }),
suggestedIntegrations: permissionRequiredProcedure.requiresPermission("admin").query(async ({ ctx }) => {
if (!env.ENABLE_DOCKER)
throw new TRPCError({
code: "NOT_FOUND",
message: "Docker is not enabled",
});
const dockerInstances = DockerSingleton.getInstances();
if (dockerInstances.length === 0) {
throw new TRPCError({
code: "NOT_FOUND",
message: "No Docker instances found",
});
}
// TODO: remove those that were already added
const containers = await Promise.all(
dockerInstances.map(({ instance, host, isSocket }) =>
instance.listContainers().then((containers) =>
containers.map((containerInfo) => ({
info: containerInfo,
host,
isSocket,
})),
),
),
).then((results) => results.flat());
// Default docker hostname consist out of first 12 characters of the container id
const homarrContainer = containers.find((container) => container.info.Id.slice(0, 12) === process.env.HOSTNAME);
const suggestions = containers
.map((container) => getSuggestionFromContainer(container, homarrContainer))
.filter((suggestion) => suggestion !== null);
const groupedSuggestions = suggestions.reduce(
(acc, suggestion) => {
acc[suggestion.kind] ??= [];
// is defined above
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
acc[suggestion.kind]!.push(suggestion);
return acc;
},
{} as Partial<Record<IntegrationKind, IntegrationSuggestion[]>>,
);
return Object.entries(groupedSuggestions)
.map(([kind, suggestions]) => ({
kind: kind as IntegrationKind,
suggestions,
}))
.sort((itemA, itemB) => {
if (itemA.suggestions.length !== itemB.suggestions.length)
return itemB.suggestions.length - itemA.suggestions.length;
const createdAtA = Math.max(...itemA.suggestions.map((suggestion) => suggestion.createdAt?.getTime() ?? 0));
const createdAtB = Math.max(...itemB.suggestions.map((suggestion) => suggestion.createdAt?.getTime() ?? 0));
return createdAtB - createdAtA;
});
}),
}); });
interface IntegrationSuggestion {
containerId: string;
name: string;
internalUrl: string | null;
externalUrl: string | null;
kind: IntegrationKind;
createdAt?: Date;
}
interface ContainerInfoWithHost {
info: ContainerInfo;
host: string;
isSocket: boolean | undefined;
}
const getSuggestionFromContainer = (
container: ContainerInfoWithHost,
homarrContainer: ContainerInfoWithHost | undefined,
): IntegrationSuggestion | null => {
const integrationKind = getSuggestionKind(container.info);
if (!integrationKind) return null;
return {
containerId: container.info.Id,
name: getIntegrationName(integrationKind),
kind: integrationKind,
internalUrl: getInternalUrl(container, homarrContainer),
externalUrl: getExternalUrl(container),
createdAt: container.info.Created ? new Date(container.info.Created * 1000) : undefined,
};
};
const getSuggestionKind = (container: ContainerInfo): IntegrationKind | null => {
const [image, _tag] = container.Image.split(":");
if (!image) return null;
return (
objectEntries(integrationDefs).find(
([, integration]) =>
"containerImages" in integration &&
integration.containerImages.some((containerImage) => containerImage.toLowerCase() === image.toLowerCase()),
)?.[0] ?? null
);
};
const calculatePortScore = (port: Port): number => {
let score = 0;
if (port.PublicPort) score += 10; // Public ports are more likely to be used
if (port.PrivatePort % 1000 === 80) score += 5; // Default http ports are most likely suffixed with 80 like 8080 or 80
if (port.PrivatePort % 1000 === 443) score += 10; // Default https ports are most likely suffixed with 443 like 8443 or 443
return score;
};
const getPort = (container: ContainerInfoWithHost): Port | null => {
const ports = container.info.Ports.filter((port) => port.Type === "tcp");
return (
ports
.map((port) => ({ port, score: calculatePortScore(port) }))
.sort((portA, portB) => portB.score - portA.score)
.at(0)?.port ?? null
);
};
const getInternalUrl = (
container: ContainerInfoWithHost,
homarrContainer: ContainerInfoWithHost | undefined,
): string | null => {
const port = getPort(container);
if (!port) return null;
const protocol = port.PrivatePort % 1000 === 443 ? "https" : "http";
if (homarrContainer) {
const matchingNetwork = Object.entries(homarrContainer.info.NetworkSettings.Networks).find(
([_key, homarrNetwork]) =>
Object.entries(container.info.NetworkSettings.Networks).some(
([_name, network]) => homarrNetwork.NetworkID === network.NetworkID,
),
)?.[1];
if (matchingNetwork) return `${protocol}://${matchingNetwork.IPAddress}:${port.PrivatePort}`;
}
// TODO: Let user specify the internal ip-address for integrations --> use as default before falling back to host.docker.internal
if (container.host === homarrContainer?.host && container.isSocket === homarrContainer.isSocket) {
return `${protocol}://host.docker.internal:${port.PublicPort}`;
}
// TODO: Change this
return `${protocol}://localhost:${port.PublicPort}`;
};
const getExternalUrl = (container: ContainerInfoWithHost): string | null => {
const port = getPort(container);
if (!port) return null;
const protocol = port.PrivatePort % 1000 === 443 ? "https" : "http";
// TODO: Use some labels like "traefik.host" to determine the external url
// For now we just use localhost as external url
return `${protocol}://localhost:${port.PublicPort}`;
};
interface UpdateSecretInput { interface UpdateSecretInput {
integrationId: string; integrationId: string;
value: string; value: string;
@@ -2,10 +2,9 @@ import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { asc, createId, eq, like } from "@homarr/db"; import { asc, createId, eq, like } from "@homarr/db";
import { getServerSettingByKeyAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries"; import { getServerSettingByKeyAsync } from "@homarr/db/queries";
import { searchEngines, users } from "@homarr/db/schema"; import { searchEngines, users } from "@homarr/db/schema";
import { createIntegrationAsync } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations";
import { logger } from "@homarr/log";
import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common"; import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common";
import { searchEngineEditSchema, searchEngineManageSchema } from "@homarr/validation/search-engine"; import { searchEngineEditSchema, searchEngineManageSchema } from "@homarr/validation/search-engine";
import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request"; import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request";
@@ -97,35 +96,23 @@ export const searchEngineRouter = createTRPCRouter({
}); });
} }
const searchSettings = await getServerSettingByKeyAsync(ctx.db, "search"); const serverDefaultId = await getServerSettingByKeyAsync(ctx.db, "search").then(
(setting) => setting.defaultSearchEngineId,
);
if (!searchSettings.defaultSearchEngineId) return null; if (serverDefaultId) {
return await ctx.db.query.searchEngines.findFirst({
const serverDefault = await ctx.db.query.searchEngines.findFirst({ where: eq(searchEngines.id, serverDefaultId),
where: eq(searchEngines.id, searchSettings.defaultSearchEngineId), with: {
with: { integration: {
integration: { columns: {
columns: { kind: true,
kind: true, url: true,
url: true, id: true,
id: true, },
}, },
}, },
},
});
if (serverDefault) return serverDefault;
// Remove the default search engine ID from settings if it does not longer exist
try {
await updateServerSettingByKeyAsync(ctx.db, "search", {
...searchSettings,
defaultSearchEngineId: null,
}); });
} catch (error) {
logger.warn(
new Error("Failed to update search settings after default search engine not found", { cause: error }),
);
} }
return null; return null;
+20 -36
View File
@@ -1,40 +1,24 @@
import { lazy } from "@trpc/server";
import { createTRPCRouter } from "../../trpc"; import { createTRPCRouter } from "../../trpc";
import { appRouter } from "./app";
import { calendarRouter } from "./calendar";
import { dnsHoleRouter } from "./dns-hole";
import { downloadsRouter } from "./downloads";
import { healthMonitoringRouter } from "./health-monitoring";
import { indexerManagerRouter } from "./indexer-manager";
import { mediaRequestsRouter } from "./media-requests";
import { mediaServerRouter } from "./media-server";
import { mediaTranscodingRouter } from "./media-transcoding";
import { minecraftRouter } from "./minecraft";
import { networkControllerRouter } from "./network-controller";
import { notebookRouter } from "./notebook";
import { optionsRouter } from "./options";
import { releasesRouter } from "./releases";
import { rssFeedRouter } from "./rssFeed";
import { smartHomeRouter } from "./smart-home";
import { stockPriceRouter } from "./stocks";
import { weatherRouter } from "./weather";
export const widgetRouter = createTRPCRouter({ export const widgetRouter = createTRPCRouter({
notebook: notebookRouter, notebook: lazy(() => import("./notebook").then((mod) => mod.notebookRouter)),
weather: weatherRouter, weather: lazy(() => import("./weather").then((mod) => mod.weatherRouter)),
app: appRouter, app: lazy(() => import("./app").then((mod) => mod.appRouter)),
dnsHole: dnsHoleRouter, dnsHole: lazy(() => import("./dns-hole").then((mod) => mod.dnsHoleRouter)),
smartHome: smartHomeRouter, smartHome: lazy(() => import("./smart-home").then((mod) => mod.smartHomeRouter)),
stockPrice: stockPriceRouter, stockPrice: lazy(() => import("./stocks").then((mod) => mod.stockPriceRouter)),
mediaServer: mediaServerRouter, mediaServer: lazy(() => import("./media-server").then((mod) => mod.mediaServerRouter)),
calendar: calendarRouter, calendar: lazy(() => import("./calendar").then((mod) => mod.calendarRouter)),
downloads: downloadsRouter, downloads: lazy(() => import("./downloads").then((mod) => mod.downloadsRouter)),
mediaRequests: mediaRequestsRouter, mediaRequests: lazy(() => import("./media-requests").then((mod) => mod.mediaRequestsRouter)),
rssFeed: rssFeedRouter, rssFeed: lazy(() => import("./rssFeed").then((mod) => mod.rssFeedRouter)),
indexerManager: indexerManagerRouter, indexerManager: lazy(() => import("./indexer-manager").then((mod) => mod.indexerManagerRouter)),
healthMonitoring: healthMonitoringRouter, healthMonitoring: lazy(() => import("./health-monitoring").then((mod) => mod.healthMonitoringRouter)),
mediaTranscoding: mediaTranscodingRouter, mediaTranscoding: lazy(() => import("./media-transcoding").then((mod) => mod.mediaTranscodingRouter)),
minecraft: minecraftRouter, minecraft: lazy(() => import("./minecraft").then((mod) => mod.minecraftRouter)),
options: optionsRouter, options: lazy(() => import("./options").then((mod) => mod.optionsRouter)),
releases: releasesRouter, releases: lazy(() => import("./releases").then((mod) => mod.releasesRouter)),
networkController: networkControllerRouter, networkController: lazy(() => import("./network-controller").then((mod) => mod.networkControllerRouter)),
}); });
+5 -5
View File
@@ -34,20 +34,20 @@
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"cookies": "^0.9.1", "cookies": "^0.9.1",
"ldapts": "8.0.1", "ldapts": "8.0.0",
"next": "15.3.3", "next": "15.3.2",
"next-auth": "5.0.0-beta.28", "next-auth": "5.0.0-beta.28",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"zod": "^3.25.62" "zod": "^3.25.30"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "5.0.2", "@types/bcrypt": "5.0.2",
"@types/cookies": "0.9.1", "@types/cookies": "0.9.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
+1 -1
View File
@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -1
View File
@@ -30,7 +30,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -1
View File
@@ -35,7 +35,7 @@
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"esbuild": "^0.25.5", "esbuild": "^0.25.5",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+3 -3
View File
@@ -30,18 +30,18 @@
"@homarr/env": "workspace:^0.1.0", "@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"next": "15.3.3", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"undici": "7.10.0", "undici": "7.10.0",
"zod": "^3.25.62", "zod": "^3.25.30",
"zod-validation-error": "^3.4.1" "zod-validation-error": "^3.4.1"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -1
View File
@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -1
View File
@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+2 -2
View File
@@ -25,14 +25,14 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"node-cron": "^4.1.0" "node-cron": "^4.0.7"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -1
View File
@@ -44,7 +44,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
@@ -1,10 +1,10 @@
import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions";
import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole"; import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole";
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
import { createCronJob } from "../../lib"; import { createCronJob } from "../../lib";
export const dnsHoleJob = createCronJob("dnsHole", EVERY_MINUTE).withCallback( export const dnsHoleJob = createCronJob("dnsHole", EVERY_5_SECONDS).withCallback(
createRequestIntegrationJobHandler(dnsHoleRequestHandler.handler, { createRequestIntegrationJobHandler(dnsHoleRequestHandler.handler, {
widgetKinds: ["dnsHoleSummary", "dnsHoleControls"], widgetKinds: ["dnsHoleSummary", "dnsHoleControls"],
getInput: { getInput: {
+2 -2
View File
@@ -1,6 +1,6 @@
import SuperJSON from "superjson"; import SuperJSON from "superjson";
import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions"; import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions";
import { db, eq } from "@homarr/db"; import { db, eq } from "@homarr/db";
import { items } from "@homarr/db/schema"; import { items } from "@homarr/db/schema";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
@@ -10,7 +10,7 @@ import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
import type { WidgetComponentProps } from "../../../widgets"; import type { WidgetComponentProps } from "../../../widgets";
import { createCronJob } from "../lib"; import { createCronJob } from "../lib";
export const rssFeedsJob = createCronJob("rssFeeds", EVERY_10_MINUTES).withCallback(async () => { export const rssFeedsJob = createCronJob("rssFeeds", EVERY_5_MINUTES).withCallback(async () => {
const rssItems = await db.query.items.findMany({ const rssItems = await db.query.items.findMany({
where: eq(items.kind, "rssFeed"), where: eq(items.kind, "rssFeed"),
}); });
+5 -5
View File
@@ -44,13 +44,13 @@
"@homarr/env": "workspace:^0.1.0", "@homarr/env": "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.1.0", "@mantine/core": "^8.0.2",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@testcontainers/mysql": "^11.0.3", "@testcontainers/mysql": "^10.28.0",
"better-sqlite3": "^11.10.0", "better-sqlite3": "^11.10.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"drizzle-kit": "^0.31.1", "drizzle-kit": "^0.31.1",
"drizzle-orm": "^0.44.2", "drizzle-orm": "^0.43.1",
"drizzle-zod": "^0.7.1", "drizzle-zod": "^0.7.1",
"mysql2": "3.14.1" "mysql2": "3.14.1"
}, },
@@ -61,9 +61,9 @@
"@types/better-sqlite3": "7.6.13", "@types/better-sqlite3": "7.6.13",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"esbuild": "^0.25.5", "esbuild": "^0.25.5",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"tsx": "4.20.1", "tsx": "4.19.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -1
View File
@@ -9,7 +9,7 @@ import * as mysqlSchema from "../schema/mysql";
describe("Mysql Migration", () => { describe("Mysql Migration", () => {
test("should add all tables and keys specified in migration files", async () => { test("should add all tables and keys specified in migration files", async () => {
const mysqlContainer = await new MySqlContainer("mysql:latest").start(); const mysqlContainer = await new MySqlContainer().start();
const connection = mysql.createConnection({ const connection = mysql.createConnection({
host: mysqlContainer.getHost(), host: mysqlContainer.getHost(),
+4 -4
View File
@@ -24,15 +24,15 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"fast-xml-parser": "^5.2.5", "fast-xml-parser": "^5.2.3",
"zod": "^3.25.62" "zod": "^3.25.30"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"tsx": "4.20.1", "tsx": "4.19.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
-94
View File
@@ -16,7 +16,6 @@ interface integrationDefinition {
iconUrl: string; iconUrl: string;
secretKinds: AtLeastOneOf<IntegrationSecretKind[]>; // at least one secret kind set is required secretKinds: AtLeastOneOf<IntegrationSecretKind[]>; // at least one secret kind set is required
category: AtLeastOneOf<IntegrationCategory>; category: AtLeastOneOf<IntegrationCategory>;
containerImages?: string[]; // Optional, used for container images to suggest in the UI
} }
export const integrationDefs = { export const integrationDefs = {
@@ -25,45 +24,30 @@ export const integrationDefs = {
secretKinds: [["apiKey"]], secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sabnzbd.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sabnzbd.svg",
category: ["downloadClient", "usenet"], category: ["downloadClient", "usenet"],
containerImages: ["linuxserver/sabnzbd", "lscr.io/linuxserver/sabnzbd", "ghcr.io/linuxserver/sabnzbd"],
}, },
nzbGet: { nzbGet: {
name: "NZBGet", name: "NZBGet",
secretKinds: [["username", "password"]], secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nzbget.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nzbget.svg",
category: ["downloadClient", "usenet"], category: ["downloadClient", "usenet"],
containerImages: [
"nzbgetcom/nzbget",
"ghcr.io/nzbgetcom/nzbget",
"linuxserver/nzbget",
"lscr.io/linuxserver/nzbget",
"ghcr.io/linuxserver/nzbget",
],
}, },
deluge: { deluge: {
name: "Deluge", name: "Deluge",
secretKinds: [["password"]], secretKinds: [["password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/deluge.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/deluge.svg",
category: ["downloadClient", "torrent"], category: ["downloadClient", "torrent"],
containerImages: ["linuxserver/deluge", "lscr.io/linuxserver/deluge", "ghcr.io/linuxserver/deluge"],
}, },
transmission: { transmission: {
name: "Transmission", name: "Transmission",
secretKinds: [["username", "password"]], secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/transmission.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/transmission.svg",
category: ["downloadClient", "torrent"], category: ["downloadClient", "torrent"],
containerImages: [
"linuxserver/transmission",
"lscr.io/linuxserver/transmission",
"ghcr.io/linuxserver/transmission",
],
}, },
qBittorrent: { qBittorrent: {
name: "qBittorrent", name: "qBittorrent",
secretKinds: [["username", "password"]], secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/qbittorrent.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/qbittorrent.svg",
category: ["downloadClient", "torrent"], category: ["downloadClient", "torrent"],
containerImages: ["linuxserver/qbittorrent", "lscr.io/linuxserver/qbittorrent", "ghcr.io/linuxserver/qbittorrent"],
}, },
aria2: { aria2: {
name: "Aria2", name: "Aria2",
@@ -76,144 +60,78 @@ export const integrationDefs = {
secretKinds: [["apiKey"]], secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sonarr.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sonarr.svg",
category: ["calendar"], category: ["calendar"],
containerImages: [
"hotio/sonarr",
"ghcr.io/hotio/sonarr",
"linuxserver/sonarr",
"lscr.io/linuxserver/sonarr",
"ghcr.io/linuxserver/sonarr",
],
}, },
radarr: { radarr: {
name: "Radarr", name: "Radarr",
secretKinds: [["apiKey"]], secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/radarr.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/radarr.svg",
category: ["calendar"], category: ["calendar"],
containerImages: [
"hotio/radarr",
"ghcr.io/hotio/radarr",
"linuxserver/radarr",
"lscr.io/linuxserver/radarr",
"ghcr.io/linuxserver/radarr",
],
}, },
lidarr: { lidarr: {
name: "Lidarr", name: "Lidarr",
secretKinds: [["apiKey"]], secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/lidarr.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/lidarr.svg",
category: ["calendar"], category: ["calendar"],
containerImages: [
"hotio/lidarr",
"ghcr.io/hotio/lidarr",
"linuxserver/lidarr",
"lscr.io/linuxserver/lidarr",
"ghcr.io/linuxserver/lidarr",
],
}, },
readarr: { readarr: {
name: "Readarr", name: "Readarr",
secretKinds: [["apiKey"]], secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/readarr.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/readarr.svg",
category: ["calendar"], category: ["calendar"],
containerImages: [
"hotio/readarr",
"ghcr.io/hotio/readarr",
"linuxserver/readarr",
"lscr.io/linuxserver/readarr",
"ghcr.io/linuxserver/readarr",
],
}, },
prowlarr: { prowlarr: {
name: "Prowlarr", name: "Prowlarr",
secretKinds: [["apiKey"]], secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/prowlarr.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/prowlarr.svg",
category: ["indexerManager"], category: ["indexerManager"],
containerImages: [
"hotio/prowlarr",
"ghcr.io/hotio/prowlarr",
"linuxserver/prowlarr",
"lscr.io/linuxserver/prowlarr",
"ghcr.io/linuxserver/prowlarr",
],
}, },
jellyfin: { jellyfin: {
name: "Jellyfin", name: "Jellyfin",
secretKinds: [["username", "password"], ["apiKey"]], secretKinds: [["username", "password"], ["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg",
category: ["mediaService"], category: ["mediaService"],
containerImages: [
"jellyfin/jellyfin",
"ghcr.io/jellyfin/jellyfin",
"linuxserver/jellyfin",
"lscr.io/linuxserver/jellyfin",
"ghcr.io/linuxserver/jellyfin",
],
}, },
emby: { emby: {
name: "Emby", name: "Emby",
secretKinds: [["apiKey"]], secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/emby.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/emby.svg",
category: ["mediaService"], category: ["mediaService"],
containerImages: [
"emby/embyserver",
"emby/embyserver_arm64v8",
"emby/embyserver_arm32v7",
"linuxserver/emby",
"lscr.io/linuxserver/emby",
"ghcr.io/linuxserver/emby",
],
}, },
plex: { plex: {
name: "Plex", name: "Plex",
secretKinds: [["apiKey"]], secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/plex.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/plex.svg",
category: ["mediaService"], category: ["mediaService"],
containerImages: ["plexinc/pms-docker", "linuxserver/plex", "lscr.io/linuxserver/plex", "ghcr.io/linuxserver/plex"],
}, },
jellyseerr: { jellyseerr: {
name: "Jellyseerr", name: "Jellyseerr",
secretKinds: [["apiKey"]], secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyseerr.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyseerr.svg",
category: ["mediaSearch", "mediaRequest", "search"], category: ["mediaSearch", "mediaRequest", "search"],
containerImages: ["fallenbagel/jellyseerr", "ghcr.io/fallenbagel/jellyseerr"],
}, },
overseerr: { overseerr: {
name: "Overseerr", name: "Overseerr",
secretKinds: [["apiKey"]], secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/overseerr.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/overseerr.svg",
category: ["mediaSearch", "mediaRequest", "search"], category: ["mediaSearch", "mediaRequest", "search"],
containerImages: [
"sct/overseerr",
"linuxserver/overseerr",
"lscr.io/linuxserver/overseerr",
"ghcr.io/linuxserver/overseerr",
],
}, },
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"],
containerImages: ["pihole/pihole"],
}, },
adGuardHome: { adGuardHome: {
name: "AdGuard Home", name: "AdGuard Home",
secretKinds: [["username", "password"]], secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/adguard-home.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/adguard-home.svg",
category: ["dnsHole"], category: ["dnsHole"],
containerImages: ["adguard/adguardhome"],
}, },
homeAssistant: { homeAssistant: {
name: "Home Assistant", name: "Home Assistant",
secretKinds: [["apiKey"]], secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/home-assistant.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/home-assistant.svg",
category: ["smartHomeServer"], category: ["smartHomeServer"],
containerImages: [
"homeassistant/home-assistant",
"linuxserver/home-assistant",
"lscr.io/linuxserver/home-assistant",
"ghcr.io/linuxserver/home-assistant",
],
}, },
openmediavault: { openmediavault: {
name: "OpenMediaVault", name: "OpenMediaVault",
@@ -226,14 +144,12 @@ export const integrationDefs = {
secretKinds: [[]], secretKinds: [[]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/dashdot.png", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/dashdot.png",
category: ["healthMonitoring"], category: ["healthMonitoring"],
containerImages: ["mauricenino/dashdot", "ghcr.io/mauricenino/dashdot"],
}, },
tdarr: { tdarr: {
name: "Tdarr", name: "Tdarr",
secretKinds: [[], ["apiKey"]], secretKinds: [[], ["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/tdarr.png", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/tdarr.png",
category: ["mediaTranscoding"], category: ["mediaTranscoding"],
containerImages: ["haveagitgat/tdarr", "ghcr.io/haveagitgat/tdarr"],
}, },
proxmox: { proxmox: {
name: "Proxmox", name: "Proxmox",
@@ -252,16 +168,6 @@ export const integrationDefs = {
secretKinds: [["username", "password"]], secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png",
category: ["networkController"], category: ["networkController"],
containerImages: [
"jacobalberty/unifi",
"ghcr.io/jacobalberty/unifi",
"linuxserver/unifi-controller",
"lscr.io/linuxserver/unifi-controller",
"ghcr.io/linuxserver/unifi-controller",
"linuxserver/unifi-network-application",
"lscr.io/linuxserver/unifi-network-application",
"ghcr.io/linuxserver/unifi-network-application",
],
}, },
} as const satisfies Record<string, integrationDefinition>; } as const satisfies Record<string, integrationDefinition>;
+3 -3
View File
@@ -26,14 +26,14 @@
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0", "@homarr/env": "workspace:^0.1.0",
"dockerode": "^4.0.7" "dockerode": "^4.0.6"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.40", "@types/dockerode": "^3.3.39",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -2
View File
@@ -5,7 +5,6 @@ import { env } from "./env";
export interface DockerInstance { export interface DockerInstance {
host: string; host: string;
instance: Docker; instance: Docker;
isSocket?: boolean;
} }
export class DockerSingleton { export class DockerSingleton {
@@ -15,7 +14,7 @@ export class DockerSingleton {
const hostVariable = env.DOCKER_HOSTNAMES; const hostVariable = env.DOCKER_HOSTNAMES;
const portVariable = env.DOCKER_PORTS; const portVariable = env.DOCKER_PORTS;
if (hostVariable === undefined || portVariable === undefined) { if (hostVariable === undefined || portVariable === undefined) {
return [{ host: "socket", instance: new Docker(), isSocket: true }]; return [{ host: "socket", instance: new Docker() }];
} }
const hostnames = hostVariable.split(","); const hostnames = hostVariable.split(",");
const ports = portVariable.split(","); const ports = portVariable.split(",");
+3 -3
View File
@@ -23,14 +23,14 @@
}, },
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@t3-oss/env-nextjs": "^0.13.8", "@t3-oss/env-nextjs": "^0.13.6",
"zod": "^3.25.62" "zod": "^3.25.30"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+3 -3
View File
@@ -26,14 +26,14 @@
"@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.1.0", "@mantine/form": "^8.0.2",
"zod": "^3.25.62" "zod": "^3.25.30"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+3 -3
View File
@@ -29,15 +29,15 @@
"@homarr/notifications": "workspace:^0.1.0", "@homarr/notifications": "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/core": "^8.1.0", "@mantine/core": "^8.0.2",
"react": "19.1.0", "react": "19.1.0",
"zod": "^3.25.62" "zod": "^3.25.30"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -1
View File
@@ -31,7 +31,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+2 -2
View File
@@ -44,7 +44,7 @@
"tsdav": "^2.1.4", "tsdav": "^2.1.4",
"undici": "7.10.0", "undici": "7.10.0",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
"zod": "^3.25.62" "zod": "^3.25.30"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
@@ -52,7 +52,7 @@
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-unifi": "^2.5.1", "@types/node-unifi": "^2.5.1",
"@types/xml2js": "^0.4.14", "@types/xml2js": "^0.4.14",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
@@ -1,3 +1,6 @@
import dayjs from "dayjs";
import objectSupport from "dayjs/plugin/objectSupport";
import utc from "dayjs/plugin/utc";
import * as ical from "node-ical"; import * as ical from "node-ical";
import { DAVClient } from "tsdav"; import { DAVClient } from "tsdav";
import type { Dispatcher, RequestInit as UndiciFetchRequestInit } from "undici"; import type { Dispatcher, RequestInit as UndiciFetchRequestInit } from "undici";
@@ -12,6 +15,9 @@ import { Integration } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service"; import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { CalendarEvent } from "../calendar-types"; import type { CalendarEvent } from "../calendar-types";
dayjs.extend(utc);
dayjs.extend(objectSupport);
@HandleIntegrationErrors([integrationTsdavHttpErrorHandler]) @HandleIntegrationErrors([integrationTsdavHttpErrorHandler])
export class NextcloudIntegration extends Integration { export class NextcloudIntegration extends Integration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> { protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
@@ -21,7 +27,7 @@ export class NextcloudIntegration extends Integration {
return { success: true }; return { success: true };
} }
public async getCalendarEventsAsync(start: Date, end: Date, _showUnmonitored?: boolean): Promise<CalendarEvent[]> { public async getCalendarEventsAsync(start: Date, end: Date): Promise<CalendarEvent[]> {
const client = await this.createCalendarClientAsync(); const client = await this.createCalendarClientAsync();
await client.login(); await client.login();
@@ -51,7 +57,14 @@ export class NextcloudIntegration extends Integration {
logger.debug(`Converting VEVENT event to ${event.etag} from Nextcloud: ${JSON.stringify(veventObject)}`); logger.debug(`Converting VEVENT event to ${event.etag} from Nextcloud: ${JSON.stringify(veventObject)}`);
const date = veventObject.start; const date = dayjs.utc({
days: veventObject.start.getDay(),
month: veventObject.start.getMonth(),
year: veventObject.start.getFullYear(),
hours: veventObject.start.getHours(),
minutes: veventObject.start.getMinutes(),
seconds: veventObject.start.getSeconds(),
});
const eventUrlWithoutHost = new URL(event.url).pathname; const eventUrlWithoutHost = new URL(event.url).pathname;
const dateInMillis = veventObject.start.valueOf(); const dateInMillis = veventObject.start.valueOf();
@@ -62,7 +75,7 @@ export class NextcloudIntegration extends Integration {
return { return {
name: veventObject.summary, name: veventObject.summary,
date, date: date.toDate(),
subName: "", subName: "",
description: veventObject.description, description: veventObject.description,
links: [ links: [
@@ -66,7 +66,7 @@ export class OpenMediaVaultIntegration extends Integration {
return { return {
version: systemResult.data.response.version, version: systemResult.data.response.version,
cpuModelName: systemResult.data.response.cpuModelName ?? "Unknown CPU", cpuModelName: systemResult.data.response.cpuModelName,
cpuUtilization: systemResult.data.response.cpuUtilization, cpuUtilization: systemResult.data.response.cpuUtilization,
memUsed: systemResult.data.response.memUsed, memUsed: systemResult.data.response.memUsed,
memAvailable: systemResult.data.response.memAvailable, memAvailable: systemResult.data.response.memAvailable,
@@ -4,7 +4,7 @@ import { z } from "zod";
export const systemInformationSchema = z.object({ export const systemInformationSchema = z.object({
response: z.object({ response: z.object({
version: z.string(), version: z.string(),
cpuModelName: z.string().nullable(), cpuModelName: z.string(),
cpuUtilization: z.number(), cpuUtilization: z.number(),
memUsed: z.string(), memUsed: z.string(),
memAvailable: z.string(), memAvailable: z.string(),
@@ -77,7 +77,6 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
protected async testingAsync({ fetchAsync }: IntegrationTestingInput): Promise<TestingResult> { protected async testingAsync({ fetchAsync }: IntegrationTestingInput): Promise<TestingResult> {
const sessionId = await this.getSessionAsync(fetchAsync); const sessionId = await this.getSessionAsync(fetchAsync);
await this.clearSessionAsync(sessionId, fetchAsync); await this.clearSessionAsync(sessionId, fetchAsync);
await this.sessionStore.clearAsync();
return { success: true }; return { success: true };
} }
@@ -69,7 +69,6 @@ const mapResource = (resource: Proxmox.clusterResourcesResources): Resource | nu
const mapComputeResource = (resource: Proxmox.clusterResourcesResources): Omit<ComputeResourceBase<string>, "type"> => { const mapComputeResource = (resource: Proxmox.clusterResourcesResources): Omit<ComputeResourceBase<string>, "type"> => {
return { return {
id: resource.id,
cpu: { cpu: {
utilization: resource.cpu ?? 0, utilization: resource.cpu ?? 0,
cores: resource.maxcpu ?? 0, cores: resource.maxcpu ?? 0,
@@ -115,7 +114,6 @@ const mapVmResource = (resource: Proxmox.clusterResourcesResources): LxcResource
const mapStorageResource = (resource: Proxmox.clusterResourcesResources): StorageResource => { const mapStorageResource = (resource: Proxmox.clusterResourcesResources): StorageResource => {
return { return {
id: resource.id,
type: "storage", type: "storage",
name: resource.storage ?? "", name: resource.storage ?? "",
node: resource.node ?? "", node: resource.node ?? "",
@@ -7,7 +7,6 @@ interface ResourceBase<TType extends string> {
} }
export interface ComputeResourceBase<TType extends string> extends ResourceBase<TType> { export interface ComputeResourceBase<TType extends string> extends ResourceBase<TType> {
id: string;
cpu: { cpu: {
utilization: number; // previously cpu (0-1) utilization: number; // previously cpu (0-1)
cores: number; // previously cpuCores cores: number; // previously cpuCores
@@ -41,7 +40,6 @@ export interface QemuResource extends ComputeResourceBase<"qemu"> {
} }
export interface StorageResource extends ResourceBase<"storage"> { export interface StorageResource extends ResourceBase<"storage"> {
id: string;
storagePlugin: string; storagePlugin: string;
used: number; // previously disk used: number; // previously disk
total: number; // previously maxDisk total: number; // previously maxDisk
@@ -64,29 +64,26 @@ describe("Home Assistant integration", () => {
const prepareHomeAssistantContainerAsync = async () => { const prepareHomeAssistantContainerAsync = async () => {
const homeAssistantContainer = createHomeAssistantContainer(); const homeAssistantContainer = createHomeAssistantContainer();
const startedContainer = await homeAssistantContainer.start(); const startedContainer = await homeAssistantContainer.start();
await startedContainer.exec(["unzip", "-o", "/tmp/config.zip", "-d", "/config"]); await startedContainer.exec(["unzip", "-o", "/tmp/config.zip", "-d", "/config"]);
await startedContainer.restart(); await startedContainer.restart();
return startedContainer; return startedContainer;
}; };
const createHomeAssistantContainer = () => { const createHomeAssistantContainer = () => {
return ( return new GenericContainer(IMAGE_NAME)
new GenericContainer(IMAGE_NAME) .withCopyFilesToContainer([
.withCopyFilesToContainer([ {
{ source: join(__dirname, "/volumes/home-assistant-config.zip"),
source: join(__dirname, "/volumes/home-assistant-config.zip"), target: "/tmp/config.zip",
target: "/tmp/config.zip", },
}, ])
]) .withPrivilegedMode()
.withPrivilegedMode() .withExposedPorts(8123)
.withExposedPorts(8123) .withWaitStrategy(Wait.forHttp("/", 8123));
// This has to be a page that is not redirected (or a status code has to be defined withStatusCode(statusCode))
.withWaitStrategy(Wait.forHttp("/onboarding.html", 8123))
);
}; };
const createHomeAssistantIntegration = (container: StartedTestContainer, apiKeyOverride?: string) => { const createHomeAssistantIntegration = (container: StartedTestContainer, apiKeyOverride?: string) => {
console.log("Creating Home Assistant integration...");
return new HomeAssistantIntegration({ return new HomeAssistantIntegration({
id: "1", id: "1",
decryptedSecrets: [ decryptedSecrets: [
+2 -2
View File
@@ -145,8 +145,8 @@ describe("Nzbget integration", () => {
// Assert // Assert
await expect(actAsync()).resolves.not.toThrow(); await expect(actAsync()).resolves.not.toThrow();
// NzbGet is slow and we wait for a few seconds before querying the items. Test was flaky without this. // NzbGet is slow and we wait for a second before querying the items. Test was flaky without this.
await new Promise((resolve) => setTimeout(resolve, 5000)); await new Promise((resolve) => setTimeout(resolve, 1000));
const result = await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 }); const result = await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
expect(result.items).toHaveLength(0); expect(result.items).toHaveLength(0);
+6 -9
View File
@@ -211,15 +211,12 @@ const createPiHoleIntegrationV5 = (container: StartedTestContainer, apiKey: stri
}; };
const createPiHoleV6Container = (password: string) => { const createPiHoleV6Container = (password: string) => {
return ( return new GenericContainer("pihole/pihole:latest")
new GenericContainer("pihole/pihole:latest") .withEnvironment({
.withEnvironment({ FTLCONF_webserver_api_password: password,
FTLCONF_webserver_api_password: password, })
}) .withExposedPorts(80)
.withExposedPorts(80) .withWaitStrategy(Wait.forHttp("/admin", 80));
// This has to be a page that is not redirected (or a status code has to be defined withStatusCode(statusCode))
.withWaitStrategy(Wait.forHttp("/admin/login", 80))
);
}; };
const createPiHoleIntegrationV6 = (container: StartedTestContainer, apiKey: string) => { const createPiHoleIntegrationV6 = (container: StartedTestContainer, apiKey: string) => {
+10 -13
View File
@@ -192,19 +192,16 @@ describe("Sabnzbd integration", () => {
}); });
const createSabnzbdContainer = () => { const createSabnzbdContainer = () => {
return ( return new GenericContainer(IMAGE_NAME)
new GenericContainer(IMAGE_NAME) .withCopyFilesToContainer([
.withCopyFilesToContainer([ {
{ source: join(__dirname, "/volumes/usenet/sabnzbd.ini"),
source: join(__dirname, "/volumes/usenet/sabnzbd.ini"), target: "/config/sabnzbd.ini",
target: "/config/sabnzbd.ini", },
}, ])
]) .withExposedPorts(1212)
.withExposedPorts(1212) .withEnvironment({ PUID: "0", PGID: "0" })
.withEnvironment({ PUID: "0", PGID: "0" }) .withWaitStrategy(Wait.forHttp("/", 1212));
// This has to be a page that is not redirected (or a status code has to be defined withStatusCode(statusCode))
.withWaitStrategy(Wait.forHttp("/sabnzbd/wizard/", 1212))
);
}; };
const createSabnzbdIntegration = (container: StartedTestContainer, apiKey: string) => { const createSabnzbdIntegration = (container: StartedTestContainer, apiKey: string) => {
+2 -2
View File
@@ -27,13 +27,13 @@
"ioredis": "5.6.1", "ioredis": "5.6.1",
"superjson": "2.2.2", "superjson": "2.2.2",
"winston": "3.17.0", "winston": "3.17.0",
"zod": "^3.25.62" "zod": "^3.25.30"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+5 -5
View File
@@ -33,19 +33,19 @@
"@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.1.0", "@mantine/core": "^8.0.2",
"@tabler/icons-react": "^3.34.0", "@tabler/icons-react": "^3.33.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"next": "15.3.3", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"zod": "^3.25.62" "zod": "^3.25.30"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+3 -3
View File
@@ -24,15 +24,15 @@
"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.1.0", "@mantine/core": "^8.0.2",
"@mantine/hooks": "^8.1.0", "@mantine/hooks": "^8.0.2",
"react": "19.1.0" "react": "19.1.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+3 -3
View File
@@ -24,14 +24,14 @@
"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.1.0", "@mantine/notifications": "^8.0.2",
"@tabler/icons-react": "^3.34.0" "@tabler/icons-react": "^3.33.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+5 -5
View File
@@ -37,14 +37,14 @@
"@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.1.0", "@mantine/core": "^8.0.2",
"@mantine/hooks": "^8.1.0", "@mantine/hooks": "^8.0.2",
"adm-zip": "0.5.16", "adm-zip": "0.5.16",
"next": "15.3.3", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"superjson": "2.2.2", "superjson": "2.2.2",
"zod": "^3.25.62", "zod": "^3.25.30",
"zod-form-data": "^2.0.7" "zod-form-data": "^2.0.7"
}, },
"devDependencies": { "devDependencies": {
@@ -52,7 +52,7 @@
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/adm-zip": "0.5.7", "@types/adm-zip": "0.5.7",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+2 -2
View File
@@ -23,13 +23,13 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"zod": "^3.25.62" "zod": "^3.25.30"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -1
View File
@@ -30,7 +30,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -1
View File
@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+2 -2
View File
@@ -30,14 +30,14 @@
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"@homarr/redis": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"octokit": "^5.0.3", "octokit": "^5.0.2",
"superjson": "2.2.2" "superjson": "2.2.2"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -1
View File
@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+3 -3
View File
@@ -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.1.0", "@mantine/dates": "^8.0.2",
"next": "15.3.3", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0" "react-dom": "19.1.0"
}, },
@@ -35,7 +35,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+6 -6
View File
@@ -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.1.0", "@mantine/core": "^8.0.2",
"@mantine/hooks": "^8.1.0", "@mantine/hooks": "^8.0.2",
"@mantine/spotlight": "^8.1.0", "@mantine/spotlight": "^8.0.2",
"@tabler/icons-react": "^3.34.0", "@tabler/icons-react": "^3.33.0",
"jotai": "^2.12.5", "jotai": "^2.12.5",
"next": "15.3.3", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"use-deep-compare-effect": "^1.8.1" "use-deep-compare-effect": "^1.8.1"
@@ -47,7 +47,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+2 -2
View File
@@ -32,7 +32,7 @@
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"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": "15.3.3", "next": "15.3.2",
"next-intl": "4.1.0", "next-intl": "4.1.0",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0" "react-dom": "19.1.0"
@@ -41,7 +41,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+170 -182
View File
@@ -263,14 +263,14 @@
} }
}, },
"removeImage": { "removeImage": {
"label": "删除头像", "label": "删除图像",
"confirm": "确定要移除这张图片吗?", "confirm": "确定要移除这张图片吗?",
"notification": { "notification": {
"success": { "success": {
"message": "图像删除成功" "message": "图像删除成功"
}, },
"error": { "error": {
"message": "无法删除头像" "message": "无法删除图像"
} }
} }
} }
@@ -612,17 +612,17 @@
"select": { "select": {
"label": "选择应用", "label": "选择应用",
"notFound": "未找到应用", "notFound": "未找到应用",
"search": "搜索应用", "search": "",
"noResults": "没有结果", "noResults": "",
"action": "选择 {app}", "action": "",
"title": "选择一个应用程序添加到此面板" "title": ""
}, },
"create": { "create": {
"title": "创建应用", "title": "",
"description": "创建应用 ", "description": "",
"action": "打开应用创建" "action": ""
}, },
"add": "添加应用" "add": ""
} }
}, },
"integration": { "integration": {
@@ -705,125 +705,125 @@
"error": { "error": {
"common": { "common": {
"cause": { "cause": {
"title": "更多详情" "title": ""
} }
}, },
"unknown": { "unknown": {
"title": "未知错误", "title": "",
"description": "发生未知错误,打开下面的原因以查看更多详情" "description": ""
}, },
"parse": { "parse": {
"title": "解析错误", "title": "",
"description": "无法解析该响应。请确认URL指向服务的基本URL。" "description": ""
}, },
"authorization": { "authorization": {
"title": "授权错误", "title": "",
"description": "请求未被授权。请验证凭据是否正确,并且您拥有足够的权限。" "description": ""
}, },
"statusCode": { "statusCode": {
"title": "响应错误", "title": "",
"description": "收到来自<url></url>的意外{statusCode} ({reason})响应。请验证URL是否指向集成的基本URL。", "description": "",
"otherDescription": "从<url></url>收到意外的{statusCode}响应。请验证URL是否指向集成的基本URL。", "otherDescription": "",
"reason": { "reason": {
"badRequest": "错误请求", "badRequest": "",
"notFound": "未找到", "notFound": "",
"tooManyRequests": "请求过于频繁", "tooManyRequests": "",
"internalServerError": "服务器内部错误", "internalServerError": "",
"serviceUnavailable": "服务暂时不可用", "serviceUnavailable": "",
"gatewayTimeout": "网关超时" "gatewayTimeout": ""
} }
}, },
"certificate": { "certificate": {
"title": "证书错误", "title": "",
"description": { "description": {
"expired": "此证书已经过期。", "expired": "",
"notYetValid": "此证书尚未生效。", "notYetValid": "",
"untrusted": "此证书不受信任。", "untrusted": "",
"hostnameMismatch": "证书主机名与 URL 不匹配。" "hostnameMismatch": ""
}, },
"alert": { "alert": {
"permission": { "permission": {
"title": "权限不足", "title": "",
"message": "您无权信任或上传证书。请与您的管理员联系上传必要的根证书。" "message": ""
}, },
"hostnameMismatch": { "hostnameMismatch": {
"title": "主机名不匹配", "title": "",
"message": "证书中的主机名与您连接的主机名不匹配。 这可能会显示安全风险,但您仍然可以选择信任此证书。" "message": ""
}, },
"extract": { "extract": {
"title": "CA证书提取失败", "title": "",
"message": "只有自签署证书链可以自动获取。如果使用的是自签名证书,请确保手动上传CA证书。您可以找到有关如何执行此操作的说明<docsLink></docsLink>。" "message": ""
} }
}, },
"action": { "action": {
"retry": { "retry": {
"label": "重试创建" "label": ""
}, },
"trust": { "trust": {
"label": "信任证书" "label": ""
}, },
"upload": { "upload": {
"label": "上传证书" "label": ""
} }
}, },
"hostnameMismatch": { "hostnameMismatch": {
"confirm": { "confirm": {
"title": "信任主机名不匹配", "title": "",
"message": "您确定要信任主机名不匹配的证书吗?" "message": ""
}, },
"notification": { "notification": {
"success": { "success": {
"title": "可信证书", "title": "",
"message": "将主机名添加到受信任证书列表中" "message": ""
}, },
"error": { "error": {
"title": "信任证书失败", "title": "",
"message": "主机名不匹配的证书不可信" "message": ""
} }
} }
}, },
"selfSigned": { "selfSigned": {
"confirm": { "confirm": {
"title": "信任自签名证书", "title": "",
"message": "您确定要信任此自签名证书吗?" "message": ""
}, },
"notification": { "notification": {
"success": { "success": {
"title": "可信证书", "title": "",
"message": "已添加证书到可信证书列表" "message": ""
}, },
"error": { "error": {
"title": "信任证书失败", "title": "",
"message": "向受信任证书列表添加证书失败" "message": ""
} }
} }
}, },
"details": { "details": {
"title": "详情", "title": "",
"description": "在决定信任证书信息之前,请检查该证书信息。", "description": "",
"content": { "content": {
"action": "显示内容", "action": "",
"title": "PEM 证书" "title": ""
} }
} }
}, },
"request": { "request": {
"title": "错误请求", "title": "",
"description": { "description": {
"connection": { "connection": {
"hostUnreachable": "无法连接到服务器。这通常意味着主机离线或无法从您的网络访问。", "hostUnreachable": "",
"networkUnreachable": "网络不可达。请检查网络连接或网络配置。", "networkUnreachable": "",
"refused": "服务器拒绝连接。它可能没有运行或正在拒绝指定端口上的请求。", "refused": "",
"reset": "服务器意外关闭了连接。如果服务器不稳定或重新启动,就会发生这种情况。" "reset": ""
}, },
"dns": { "dns": {
"notFound": "找不到服务器地址。请检查URL是否有拼写错误或无效的域名。", "notFound": "",
"timeout": "DNS查找超时。这可能是暂时的问题,请稍后再试。", "timeout": "",
"noAnswer": "DNS服务器没有返回有效的响应。域可能存在,但没有有效记录。" "noAnswer": ""
}, },
"timeout": { "timeout": {
"aborted": "请求在完成之前被中止。这可能是由于用户操作或系统超时造成的。", "aborted": "",
"timeout": "请求耗时太长。请检查您的网络或稍后再试。" "timeout": ""
} }
} }
} }
@@ -895,8 +895,8 @@
"message": "该路径可能不正确" "message": "该路径可能不正确"
}, },
"tooManyRequests": { "tooManyRequests": {
"title": "指定时间内请求过多", "title": "",
"message": "请求太多了。您可能会受到速率限制或被目标系统拒绝" "message": ""
} }
} }
}, },
@@ -1004,7 +1004,7 @@
"cancel": "取消", "cancel": "取消",
"delete": "删除", "delete": "删除",
"discard": "Discard", "discard": "Discard",
"close": "关闭", "close": "",
"confirm": "确认", "confirm": "确认",
"continue": "继续", "continue": "继续",
"previous": "上一步", "previous": "上一步",
@@ -1220,7 +1220,7 @@
"label": "集成" "label": "集成"
}, },
"title": { "title": {
"label": "标题" "label": ""
}, },
"customCssClasses": { "customCssClasses": {
"label": "自定义 CSS 类" "label": "自定义 CSS 类"
@@ -1758,7 +1758,7 @@
"label": "显示内存信息" "label": "显示内存信息"
}, },
"showUptime": { "showUptime": {
"label": "显示运行时间" "label": ""
}, },
"fileSystem": { "fileSystem": {
"label": "显示文件系统信息" "label": "显示文件系统信息"
@@ -1767,7 +1767,7 @@
"label": "默认标签" "label": "默认标签"
}, },
"visibleClusterSections": { "visibleClusterSections": {
"label": "可见集群部分" "label": ""
}, },
"sectionIndicatorRequirement": { "sectionIndicatorRequirement": {
"label": "部分指标要求" "label": "部分指标要求"
@@ -1839,11 +1839,11 @@
} }
}, },
"dockerContainers": { "dockerContainers": {
"name": "Docker 状态", "name": "",
"description": "您的容器状态 (这个小部件只能用管理员权限添加)", "description": "",
"option": {}, "option": {},
"error": { "error": {
"internalServerError": "获取容器统计信息失败" "internalServerError": ""
} }
}, },
"common": { "common": {
@@ -1912,8 +1912,8 @@
"description": "显示媒体服务器上的当前流", "description": "显示媒体服务器上的当前流",
"option": { "option": {
"showOnlyPlaying": { "showOnlyPlaying": {
"label": "只显示当前正在播放", "label": "",
"description": "禁用此功能对plex不起作用" "description": ""
} }
}, },
"items": { "items": {
@@ -1946,7 +1946,7 @@
"label": "显示标记为已完成的 Torrent 条目" "label": "显示标记为已完成的 Torrent 条目"
}, },
"showCompletedHttp": { "showCompletedHttp": {
"label": "显示标记为已完成的杂项" "label": ""
}, },
"activeTorrentThreshold": { "activeTorrentThreshold": {
"label": "隐藏在此阈值下已完成的 Torrent (kiB/s)" "label": "隐藏在此阈值下已完成的 Torrent (kiB/s)"
@@ -1961,8 +1961,8 @@
"label": "使用过滤器来计算比率" "label": "使用过滤器来计算比率"
}, },
"limitPerIntegration": { "limitPerIntegration": {
"label": "限制每个集成的项目", "label": "",
"description": "这将限制每个集成显示的项目数量,而不是全局项目" "description": ""
} }
}, },
"errors": { "errors": {
@@ -2095,7 +2095,7 @@
"approved": "已批准", "approved": "已批准",
"declined": "已拒绝", "declined": "已拒绝",
"failed": "失败", "failed": "失败",
"completed": "已完成" "completed": ""
}, },
"toBeDetermined": "待定" "toBeDetermined": "待定"
}, },
@@ -2199,106 +2199,94 @@
} }
}, },
"releases": { "releases": {
"name": "发布", "name": "",
"description": "显示特定版本正则表达式中当前版本的列表。", "description": "",
"option": { "option": {
"newReleaseWithin": { "newReleaseWithin": {
"label": "内部新版本", "label": "",
"description": "使用示例:1w(1周),10M(10个月)。支持单位类型h(小时),d(天),w(周),M(月),y(年)。留空不突出显示新版本。" "description": ""
}, },
"staleReleaseWithin": { "staleReleaseWithin": {
"label": "内部过期发布", "label": "",
"description": "使用示例:1w(1周),10M(10个月)。支持单位类型h(小时),d(天),w(周),M(月),y(年)。留空不突出显示过时的版本。" "description": ""
}, },
"showOnlyHighlighted": { "showOnlyHighlighted": {
"label": "只显示高亮显示", "label": "",
"description": "只显示新的或过时的版本。如上所述。" "description": ""
}, },
"showDetails": { "showDetails": {
"label": "展开详情" "label": ""
}, },
"topReleases": { "topReleases": {
"label": "热门发布", "label": "",
"description": "要显示的最新版本的最大数量。零表示没有限制。" "description": ""
}, },
"repositories": { "repositories": {
"label": "仓库", "label": "",
"addRepository": { "addRRepository": {
"label": "添加仓库" "label": ""
},
"importRepositories": {
"label": "从 Docker 导入",
"loading": "正在加载 docker 映像",
"noImagesFound": "没有找到 docker 映像",
"listFoundImages": "找到的映像列表",
"listAlreadyImportedImages": "已导入的映像列表",
"allImagesAlreadyImported": "所有映像已导入",
"onlyAdminCanImport": "只有管理员才能从 docker 导入"
}, },
"provider": { "provider": {
"label": "提供者" "label": ""
}, },
"identifier": { "identifier": {
"label": "标识符", "label": "",
"placeholder": "姓名或所有者名称" "placeholder": ""
}, },
"name": { "name": {
"label": "名称" "label": ""
}, },
"versionFilter": { "versionFilter": {
"label": "版本过滤器", "label": "",
"prefix": { "prefix": {
"label": "前缀" "label": ""
}, },
"precision": { "precision": {
"label": "精度", "label": "",
"options": { "options": {
"none": "空" "none": ""
} }
}, },
"suffix": { "suffix": {
"label": "后缀" "label": ""
}, },
"regex": { "regex": {
"label": "正则表达式" "label": ""
} }
}, },
"edit": { "edit": {
"label": "编辑" "label": ""
}, },
"editForm": { "editForm": {
"title": "编辑仓库", "title": "",
"cancel": { "cancel": {
"label": "取消" "label": ""
}, },
"confirm": { "confirm": {
"label": "确认" "label": ""
} }
}, },
"importForm": {
"title": "从 Docker 导入"
},
"example": { "example": {
"label": "示例" "label": ""
}, },
"invalid": "仓库定义无效,请检查值" "invalid": ""
} }
}, },
"not-found": "未找到", "not-found": "",
"pre-release": "预发行", "pre-release": "",
"archived": "已归档", "archived": "",
"forked": "复刻成功", "forked": "",
"starsCount": "星", "starsCount": "",
"forksCount": "复刻", "forksCount": "",
"issuesCount": "打开问题", "issuesCount": "",
"openProjectPage": "打开项目页面", "openProjectPage": "",
"openReleasePage": "打开发布页面", "openReleasePage": "",
"releaseDescription": "发布说明", "releaseDescription": "",
"created": "已创建", "created": "",
"error": { "error": {
"label": "错误", "label": "",
"options": { "options": {
"noMatchingVersion": "没有找到匹配的版本" "noMatchingVersion": ""
} }
} }
}, },
@@ -2306,56 +2294,56 @@
"option": {}, "option": {},
"card": { "card": {
"vpn": { "vpn": {
"countConnected": "{count} 已连接" "countConnected": ""
} }
}, },
"error": { "error": {
"integrationsDisconnected": "没有可用的数据,所有集成已断开", "integrationsDisconnected": "",
"unknownContentOption": "网络控制器摘要小部件的未知内容选项: " "unknownContentOption": ""
}, },
"name": "网络控制器概述", "name": "",
"description": "显示网络控制器摘要(如Unifi Controller)" "description": ""
}, },
"networkControllerStatus": { "networkControllerStatus": {
"card": { "card": {
"variants": { "variants": {
"wired": { "wired": {
"name": "有线" "name": ""
}, },
"wifi": { "wifi": {
"name": "Wi-Fi" "name": ""
} }
}, },
"users": { "users": {
"label": "用户数" "label": ""
}, },
"guests": { "guests": {
"label": "游客" "label": ""
} }
}, },
"option": { "option": {
"content": { "content": {
"option": { "option": {
"wifi": { "wifi": {
"label": "Wi-Fi" "label": ""
}, },
"wired": { "wired": {
"label": "有线" "label": ""
} }
}, },
"label": "小部件内容" "label": ""
} }
}, },
"error": { "error": {
"integrationsDisconnected": "没有可用的数据,所有集成已断开", "integrationsDisconnected": "",
"unknownContentOption": "网络状态小部件的未知内容选项: " "unknownContentOption": ""
}, },
"name": "网络状态", "name": "",
"description": "在网络上显示已连接的设备" "description": ""
}, },
"networkController": { "networkController": {
"error": { "error": {
"internalServerError": "获取网络控制器概述失败" "internalServerError": ""
} }
} }
}, },
@@ -2478,10 +2466,10 @@
}, },
"backgroundImageUrl": { "backgroundImageUrl": {
"label": "背景图片 URL", "label": "背景图片 URL",
"placeholder": "开始输入以搜索本地映像", "placeholder": "",
"group": { "group": {
"your": "你的映像", "your": "你的图像",
"other": "其他映像" "other": ""
} }
}, },
"backgroundImageAttachment": { "backgroundImageAttachment": {
@@ -3123,7 +3111,7 @@
"label": "网络控制器" "label": "网络控制器"
}, },
"dockerContainers": { "dockerContainers": {
"label": "Docker 容器" "label": ""
} }
} }
}, },
@@ -3189,7 +3177,7 @@
"updated": "已更新 {when}", "updated": "已更新 {when}",
"search": "搜索 {count} 容器", "search": "搜索 {count} 容器",
"selected": "{totalCount} 容器的 {selectCount}", "selected": "{totalCount} 容器的 {selectCount}",
"footer": "总计 {count} 容器" "footer": ""
}, },
"field": { "field": {
"name": { "name": {
@@ -3209,10 +3197,10 @@
}, },
"stats": { "stats": {
"cpu": { "cpu": {
"label": "CPU" "label": ""
}, },
"memory": { "memory": {
"label": "内存" "label": ""
} }
}, },
"containerImage": { "containerImage": {
@@ -3223,7 +3211,7 @@
} }
}, },
"action": { "action": {
"title": "操作", "title": "",
"start": { "start": {
"label": "开始", "label": "开始",
"notification": { "notification": {
@@ -3668,7 +3656,7 @@
"certificates": { "certificates": {
"label": "证书", "label": "证书",
"hostnames": { "hostnames": {
"label": "主机名" "label": ""
} }
} }
}, },
@@ -4069,25 +4057,25 @@
"certificate": { "certificate": {
"field": { "field": {
"hostname": { "hostname": {
"label": "主机名" "label": ""
}, },
"subject": { "subject": {
"label": "主题" "label": ""
}, },
"issuer": { "issuer": {
"label": "发行者" "label": ""
}, },
"validFrom": { "validFrom": {
"label": "有效期自" "label": ""
}, },
"validTo": { "validTo": {
"label": "有效期至" "label": ""
}, },
"serialNumber": { "serialNumber": {
"label": "序号" "label": ""
}, },
"fingerprint": { "fingerprint": {
"label": "指纹" "label": ""
} }
}, },
"page": { "page": {
@@ -4098,19 +4086,19 @@
"title": "还没有证书" "title": "还没有证书"
}, },
"invalid": { "invalid": {
"title": "无效证书", "title": "",
"description": "解析证书失败" "description": ""
}, },
"expires": "到期时间 {when}", "expires": "到期时间 {when}",
"toHostnames": "信任的主机名" "toHostnames": ""
}, },
"hostnames": { "hostnames": {
"title": "信任的证书主机名", "title": "",
"description": "某些证书不允许指定域名使用 Homarr 请求他们, 由于这个原因,所有信任的主机名及其证书缩略图都被用来绕过这些限制。", "description": "",
"noResults": { "noResults": {
"title": "还没有主机名" "title": ""
}, },
"toCertificates": "证书" "toCertificates": ""
} }
}, },
"action": { "action": {
@@ -4142,16 +4130,16 @@
} }
}, },
"removeHostname": { "removeHostname": {
"label": "删除受信任的主机名", "label": "",
"confirm": "您确定要删除此受信任的主机名吗?这可能会导致一些集成停止工作。", "confirm": "",
"notification": { "notification": {
"success": { "success": {
"title": "主机名已删除", "title": "",
"message": "主机名已成功删除" "message": ""
}, },
"error": { "error": {
"title": "主机名未删除", "title": "",
"message": "无法删除主机名" "message": ""
} }
} }
} }
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "Repositories", "label": "Repositories",
"addRepository": { "addRRepository": {
"label": "Tilføj repository" "label": "Tilføj repository"
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "Udbyder" "label": "Udbyder"
}, },
@@ -2275,9 +2266,6 @@
"label": "Bekræft" "label": "Bekræft"
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "Eksempel" "label": "Eksempel"
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "Repositories", "label": "Repositories",
"addRepository": { "addRRepository": {
"label": "Repository hinzufügen" "label": "Repository hinzufügen"
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "Anbieter" "label": "Anbieter"
}, },
@@ -2275,9 +2266,6 @@
"label": "Bestätigen" "label": "Bestätigen"
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "Beispiel" "label": "Beispiel"
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "Repositories", "label": "Repositories",
"addRepository": { "addRRepository": {
"label": "Add repository" "label": "Add repository"
}, },
"importRepositories": {
"label": "Import from docker",
"loading": "Loading docker images",
"noImagesFound": "No docker images found",
"listFoundImages": "List of found images",
"listAlreadyImportedImages": "List of already imported images",
"allImagesAlreadyImported": "All images already imported",
"onlyAdminCanImport": "Only administrators can import from docker"
},
"provider": { "provider": {
"label": "Provider" "label": "Provider"
}, },
@@ -2275,9 +2266,6 @@
"label": "Confirm" "label": "Confirm"
} }
}, },
"importForm": {
"title": "Import from Docker"
},
"example": { "example": {
"label": "Example" "label": "Example"
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+2 -14
View File
@@ -747,7 +747,7 @@
"message": "" "message": ""
}, },
"hostnameMismatch": { "hostnameMismatch": {
"title": "Nom d'hôte incohérent", "title": "",
"message": "" "message": ""
}, },
"extract": { "extract": {
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+5 -17
View File
@@ -1758,7 +1758,7 @@
"label": "הצג מידע זיכרון" "label": "הצג מידע זיכרון"
}, },
"showUptime": { "showUptime": {
"label": "הצג זמן פעולה" "label": ""
}, },
"fileSystem": { "fileSystem": {
"label": "הצג מידע על מערכת הקבצים" "label": "הצג מידע על מערכת הקבצים"
@@ -1767,7 +1767,7 @@
"label": "כרטיסיית ברירת מחדל" "label": "כרטיסיית ברירת מחדל"
}, },
"visibleClusterSections": { "visibleClusterSections": {
"label": "מקטעי אשכול גלויים" "label": ""
}, },
"sectionIndicatorRequirement": { "sectionIndicatorRequirement": {
"label": "דרישת מציין מקטע" "label": "דרישת מציין מקטע"
@@ -1961,8 +1961,8 @@
"label": "השתמש במסנן כדי לחשב יחס" "label": "השתמש במסנן כדי לחשב יחס"
}, },
"limitPerIntegration": { "limitPerIntegration": {
"label": "הגבלת פריטים לכל אינטגרציה", "label": "",
"description": "פעולה זו תגביל את מספר הפריטים המוצגים בכל אינטגרציה, לא באופן גלובלי" "description": ""
} }
}, },
"errors": { "errors": {
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "מאגרים", "label": "מאגרים",
"addRepository": { "addRRepository": {
"label": "הוסף מאגר" "label": "הוסף מאגר"
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "ספק" "label": "ספק"
}, },
@@ -2275,9 +2266,6 @@
"label": "אשר" "label": "אשר"
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "דוגמא" "label": "דוגמא"
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
File diff suppressed because it is too large. Load diff
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "Depolar", "label": "Depolar",
"addRepository": { "addRRepository": {
"label": "Depo Ekle" "label": "Depo Ekle"
}, },
"importRepositories": {
"label": "Docker'dan içe aktar",
"loading": "Docker imajları yükleniyor",
"noImagesFound": "Hiç docker imajı bulunamadı",
"listFoundImages": "Bulunan imajların listesi",
"listAlreadyImportedImages": "Daha önce içe aktarılan imajların listesi",
"allImagesAlreadyImported": "Tüm imajlar daha önce içe aktarıldı",
"onlyAdminCanImport": "Sadece yöneticiler docker'dan içe aktarabilir"
},
"provider": { "provider": {
"label": "Sağlayıcı" "label": "Sağlayıcı"
}, },
@@ -2275,9 +2266,6 @@
"label": "Onayla" "label": "Onayla"
} }
}, },
"importForm": {
"title": "Docker'dan içe aktar"
},
"example": { "example": {
"label": "Örnek" "label": "Örnek"
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "", "label": "",
"addRepository": { "addRRepository": {
"label": "" "label": ""
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "" "label": ""
}, },
@@ -2275,9 +2266,6 @@
"label": "" "label": ""
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "" "label": ""
}, },
+1 -13
View File
@@ -2223,18 +2223,9 @@
}, },
"repositories": { "repositories": {
"label": "儲存庫", "label": "儲存庫",
"addRepository": { "addRRepository": {
"label": "新增儲存庫" "label": "新增儲存庫"
}, },
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
},
"provider": { "provider": {
"label": "提供者" "label": "提供者"
}, },
@@ -2275,9 +2266,6 @@
"label": "確認" "label": "確認"
} }
}, },
"importForm": {
"title": ""
},
"example": { "example": {
"label": "範例" "label": "範例"
}, },
+6 -6
View File
@@ -29,12 +29,12 @@
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "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/core": "^8.1.0", "@mantine/core": "^8.0.2",
"@mantine/dates": "^8.1.0", "@mantine/dates": "^8.0.2",
"@mantine/hooks": "^8.1.0", "@mantine/hooks": "^8.0.2",
"@tabler/icons-react": "^3.34.0", "@tabler/icons-react": "^3.33.0",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "15.3.3", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0" "react-dom": "19.1.0"
}, },
@@ -43,7 +43,7 @@
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/css-modules": "^1.0.5", "@types/css-modules": "^1.0.5",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+2 -2
View File
@@ -24,14 +24,14 @@
"dependencies": { "dependencies": {
"@homarr/definitions": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"zod": "^3.25.62", "zod": "^3.25.30",
"zod-form-data": "^2.0.7" "zod-form-data": "^2.0.7"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+23 -23
View File
@@ -48,42 +48,42 @@
"@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/charts": "^8.1.0", "@mantine/charts": "^8.0.2",
"@mantine/core": "^8.1.0", "@mantine/core": "^8.0.2",
"@mantine/hooks": "^8.1.0", "@mantine/hooks": "^8.0.2",
"@tabler/icons-react": "^3.34.0", "@tabler/icons-react": "^3.33.0",
"@tiptap/extension-color": "2.14.0", "@tiptap/extension-color": "2.12.0",
"@tiptap/extension-highlight": "2.14.0", "@tiptap/extension-highlight": "2.12.0",
"@tiptap/extension-image": "2.14.0", "@tiptap/extension-image": "2.12.0",
"@tiptap/extension-link": "^2.14.0", "@tiptap/extension-link": "^2.12.0",
"@tiptap/extension-table": "2.14.0", "@tiptap/extension-table": "2.12.0",
"@tiptap/extension-table-cell": "2.14.0", "@tiptap/extension-table-cell": "2.12.0",
"@tiptap/extension-table-header": "2.14.0", "@tiptap/extension-table-header": "2.12.0",
"@tiptap/extension-table-row": "2.14.0", "@tiptap/extension-table-row": "2.12.0",
"@tiptap/extension-task-item": "2.14.0", "@tiptap/extension-task-item": "2.12.0",
"@tiptap/extension-task-list": "2.14.0", "@tiptap/extension-task-list": "2.12.0",
"@tiptap/extension-text-align": "2.14.0", "@tiptap/extension-text-align": "2.12.0",
"@tiptap/extension-text-style": "2.14.0", "@tiptap/extension-text-style": "2.12.0",
"@tiptap/extension-underline": "2.14.0", "@tiptap/extension-underline": "2.12.0",
"@tiptap/react": "^2.14.0", "@tiptap/react": "^2.12.0",
"@tiptap/starter-kit": "^2.14.0", "@tiptap/starter-kit": "^2.12.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "15.3.3", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"recharts": "^2.15.3", "recharts": "^2.15.3",
"video.js": "^8.23.3", "video.js": "^8.22.0",
"zod": "^3.25.62" "zod": "^3.25.30"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/video.js": "^7.3.58", "@types/video.js": "^7.3.58",
"eslint": "^9.28.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
@@ -1,46 +1,19 @@
"use client"; "use client";
import React, { useCallback, useEffect, useMemo, useState } from "react"; import React, { useCallback, useEffect, useMemo, useState } from "react";
import { import { ActionIcon, Button, Divider, Fieldset, Group, Select, Stack, Text, TextInput } from "@mantine/core";
Accordion,
ActionIcon,
Button,
Checkbox,
Code,
Divider,
Fieldset,
Group,
Image,
Loader,
Select,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from "@mantine/core";
import type { CheckboxProps } from "@mantine/core";
import type { FormErrors } from "@mantine/form"; import type { FormErrors } from "@mantine/form";
import { useDebouncedValue } from "@mantine/hooks"; import { useDebouncedValue } from "@mantine/hooks";
import { import { IconEdit, IconTrash, IconTriangleFilled } from "@tabler/icons-react";
IconBrandDocker,
IconEdit,
IconPlus,
IconSquare,
IconSquareCheck,
IconTrash,
IconTriangleFilled,
} from "@tabler/icons-react";
import { escapeForRegEx } from "@tiptap/react"; import { escapeForRegEx } from "@tiptap/react";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { findBestIconMatch, IconPicker } from "@homarr/forms-collection"; import { findBestIconMatch, IconPicker } from "@homarr/forms-collection";
import { createModal, useModalAction } from "@homarr/modals"; import { createModal, useModalAction } from "@homarr/modals";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { MaskedImage } from "@homarr/ui"; import { MaskedOrNormalImage } from "@homarr/ui";
import { isProviderKey, Providers } from "../releases/releases-providers"; import { Providers } from "../releases/releases-providers";
import type { ReleasesRepository, ReleasesVersionFilter } from "../releases/releases-repository"; import type { ReleasesRepository, ReleasesVersionFilter } from "../releases/releases-repository";
import type { CommonWidgetInputProps } from "./common"; import type { CommonWidgetInputProps } from "./common";
import { useWidgetInputTranslation } from "./common"; import { useWidgetInputTranslation } from "./common";
@@ -59,14 +32,11 @@ export const WidgetMultiReleasesRepositoriesInput = ({
const tRepository = useScopedI18n("widget.releases.option.repositories"); const tRepository = useScopedI18n("widget.releases.option.repositories");
const form = useFormContext(); const form = useFormContext();
const repositories = form.values.options[property] as ReleasesRepository[]; const repositories = form.values.options[property] as ReleasesRepository[];
const { openModal: openEditModal } = useModalAction(RepositoryEditModal); const { openModal } = useModalAction(ReleaseEditModal);
const { openModal: openImportModal } = useModalAction(RepositoryImportModal);
const versionFilterPrecisionOptions = useMemo( const versionFilterPrecisionOptions = useMemo(
() => [tRepository("versionFilter.precision.options.none"), "#", "#.#", "#.#.#", "#.#.#.#", "#.#.#.#.#"], () => [tRepository("versionFilter.precision.options.none"), "#", "#.#", "#.#.#", "#.#.#.#", "#.#.#.#.#"],
[tRepository], [tRepository],
); );
const { data: session } = useSession();
const isAdmin = session?.user.permissions.includes("admin") ?? false;
const onRepositorySave = useCallback( const onRepositorySave = useCallback(
(repository: ReleasesRepository, index: number): FormValidation => { (repository: ReleasesRepository, index: number): FormValidation => {
@@ -92,8 +62,8 @@ export const WidgetMultiReleasesRepositoriesInput = ({
[form, property], [form, property],
); );
const addNewRepository = () => { const addNewItem = () => {
const repository: ReleasesRepository = { const item = {
providerKey: "DockerHub", providerKey: "DockerHub",
identifier: "", identifier: "",
}; };
@@ -104,16 +74,16 @@ export const WidgetMultiReleasesRepositoriesInput = ({
...previous, ...previous,
options: { options: {
...previous.options, ...previous.options,
[property]: [...previousValues, repository], [property]: [...previousValues, item],
}, },
}; };
}); });
const index = repositories.length; const index = repositories.length;
openEditModal({ openModal({
fieldPath: `options.${property}.${index}`, fieldPath: `options.${property}.${index}`,
repository, repository: item,
onRepositorySave: (saved) => onRepositorySave(saved, index), onRepositorySave: (saved) => onRepositorySave(saved, index),
onRepositoryCancel: () => onRepositoryRemove(index), onRepositoryCancel: () => onRepositoryRemove(index),
versionFilterPrecisionOptions, versionFilterPrecisionOptions,
@@ -136,56 +106,24 @@ export const WidgetMultiReleasesRepositoriesInput = ({
return ( return (
<Fieldset legend={t("label")}> <Fieldset legend={t("label")}>
<Stack gap="5"> <Stack gap="5">
<Group grow> <Button onClick={addNewItem}>{tRepository("addRRepository.label")}</Button>
<Button leftSection={<IconPlus />} onClick={addNewRepository}>
{tRepository("addRepository.label")}
</Button>
<Tooltip label={tRepository("importRepositories.onlyAdminCanImport")} disabled={isAdmin} withArrow>
<Button
disabled={!isAdmin}
leftSection={<IconBrandDocker stroke={1.25} />}
onClick={() =>
openImportModal({
repositories,
versionFilterPrecisionOptions,
onConfirm: (selectedRepositories) => {
if (!selectedRepositories.length) return;
form.setValues((previous) => {
const previousValues = previous.options?.[property] as ReleasesRepository[];
return {
...previous,
options: {
...previous.options,
[property]: [...previousValues, ...selectedRepositories],
},
};
});
},
isAdmin,
})
}
>
{tRepository("importRepositories.label")}
</Button>
</Tooltip>
</Group>
<Divider my="sm" /> <Divider my="sm" />
{repositories.map((repository, index) => { {repositories.map((repository, index) => {
return ( return (
<Stack key={`${repository.providerKey}.${repository.identifier}`} gap={5}> <Stack key={`${repository.providerKey}.${repository.identifier}`} gap={5}>
<Group align="center" gap="xs"> <Group align="center" gap="xs">
<Image <MaskedOrNormalImage
src={repository.iconUrl ?? Providers[repository.providerKey].iconUrl} hasColor={false}
imageUrl={repository.iconUrl ?? Providers[repository.providerKey]?.iconUrl}
style={{ style={{
height: "1.2em", height: "1em",
width: "1.2em", width: "1em",
}} }}
/> />
<Text c="dimmed" fw={100} size="xs"> <Text c="dimmed" fw={100} size="xs">
{Providers[repository.providerKey].name} {Providers[repository.providerKey]?.name}
</Text> </Text>
<Group justify="space-between" align="center" style={{ flex: 1 }} gap={5}> <Group justify="space-between" align="center" style={{ flex: 1 }} gap={5}>
@@ -197,7 +135,7 @@ export const WidgetMultiReleasesRepositoriesInput = ({
<Button <Button
onClick={() => onClick={() =>
openEditModal({ openModal({
fieldPath: `options.${property}.${index}`, fieldPath: `options.${property}.${index}`,
repository, repository,
onRepositorySave: (saved) => onRepositorySave(saved, index), onRepositorySave: (saved) => onRepositorySave(saved, index),
@@ -247,7 +185,7 @@ const formatIdentifierName = (identifier: string) => {
return unformattedName?.replace(/[-_]/g, " ").replace(/(?:^\w|[A-Z]|\b\w)/g, (char) => char.toUpperCase()) ?? ""; return unformattedName?.replace(/[-_]/g, " ").replace(/(?:^\w|[A-Z]|\b\w)/g, (char) => char.toUpperCase()) ?? "";
}; };
interface RepositoryEditProps { interface ReleaseEditProps {
fieldPath: string; fieldPath: string;
repository: ReleasesRepository; repository: ReleasesRepository;
onRepositorySave: (repository: ReleasesRepository) => FormValidation; onRepositorySave: (repository: ReleasesRepository) => FormValidation;
@@ -255,7 +193,7 @@ interface RepositoryEditProps {
versionFilterPrecisionOptions: string[]; versionFilterPrecisionOptions: string[];
} }
const RepositoryEditModal = createModal<RepositoryEditProps>(({ innerProps, actions }) => { const ReleaseEditModal = createModal<ReleaseEditProps>(({ innerProps, actions }) => {
const tRepository = useScopedI18n("widget.releases.option.repositories"); const tRepository = useScopedI18n("widget.releases.option.repositories");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [tempRepository, setTempRepository] = useState(() => ({ ...innerProps.repository })); const [tempRepository, setTempRepository] = useState(() => ({ ...innerProps.repository }));
@@ -324,7 +262,7 @@ const RepositoryEditModal = createModal<RepositoryEditProps>(({ innerProps, acti
value={tempRepository.providerKey} value={tempRepository.providerKey}
error={formErrors[`${innerProps.fieldPath}.providerKey`]} error={formErrors[`${innerProps.fieldPath}.providerKey`]}
onChange={(value) => { onChange={(value) => {
if (value && isProviderKey(value)) { if (value && Providers[value]) {
handleChange({ providerKey: value }); handleChange({ providerKey: value });
} }
}} }}
@@ -448,7 +386,7 @@ const RepositoryEditModal = createModal<RepositoryEditProps>(({ innerProps, acti
{tRepository("editForm.cancel.label")} {tRepository("editForm.cancel.label")}
</Button> </Button>
<Button data-autofocus onClick={handleConfirm} loading={loading}> <Button data-autofocus onClick={handleConfirm} color="red.9" loading={loading}>
{tRepository("editForm.confirm.label")} {tRepository("editForm.confirm.label")}
</Button> </Button>
</Group> </Group>
@@ -460,247 +398,3 @@ const RepositoryEditModal = createModal<RepositoryEditProps>(({ innerProps, acti
}, },
size: "xl", size: "xl",
}); });
interface ReleasesRepositoryImport extends ReleasesRepository {
alreadyImported: boolean;
}
interface ContainerImageSelectorProps {
containerImage: ReleasesRepositoryImport;
versionFilterPrecisionOptions: string[];
onImageSelectionChanged?: (isSelected: boolean) => void;
}
const ContainerImageSelector = ({
containerImage,
versionFilterPrecisionOptions,
onImageSelectionChanged,
}: ContainerImageSelectorProps) => {
const tRepository = useScopedI18n("widget.releases.option.repositories");
const checkBoxProps: CheckboxProps = !onImageSelectionChanged
? {
disabled: true,
checked: true,
}
: {
onChange: (event) => onImageSelectionChanged(event.currentTarget.checked),
};
return (
<Group
key={`${Providers[containerImage.providerKey].name}/${containerImage.identifier}`}
gap="xl"
justify="space-between"
>
<Group gap="md">
<Checkbox
label={
<Group>
<Image
src={containerImage.iconUrl}
style={{
height: "1.2em",
width: "1.2em",
}}
/>
<Text>{containerImage.identifier}</Text>
</Group>
}
{...checkBoxProps}
/>
{containerImage.versionFilter && (
<Group gap={5}>
<Text c="dimmed" size="xs">
{tRepository("versionFilter.label")}:
</Text>
<Code>{containerImage.versionFilter.prefix && containerImage.versionFilter.prefix}</Code>
<Code color="var(--mantine-primary-color-light)" fw={700}>
{versionFilterPrecisionOptions[containerImage.versionFilter.precision]}
</Code>
<Code>{containerImage.versionFilter.suffix && containerImage.versionFilter.suffix}</Code>
</Group>
)}
</Group>
<Group>
<MaskedImage
color="dimmed"
imageUrl={Providers[containerImage.providerKey].iconUrl}
style={{
height: "1em",
width: "1em",
}}
/>
<Text ff="monospace" c="dimmed" size="sm">
{Providers[containerImage.providerKey].name}
</Text>
</Group>
</Group>
);
};
interface RepositoryImportProps {
repositories: ReleasesRepository[];
versionFilterPrecisionOptions: string[];
onConfirm: (selectedRepositories: ReleasesRepositoryImport[]) => void;
isAdmin: boolean;
}
const RepositoryImportModal = createModal<RepositoryImportProps>(({ innerProps, actions }) => {
const tRepository = useScopedI18n("widget.releases.option.repositories");
const [loading, setLoading] = useState(false);
const [selectedImages, setSelectedImages] = useState([] as ReleasesRepositoryImport[]);
const docker = clientApi.docker.getContainers.useQuery(undefined, {
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
enabled: innerProps.isAdmin,
});
const containersImages: ReleasesRepositoryImport[] = useMemo(
() =>
docker.data?.containers.reduce<ReleasesRepositoryImport[]>((acc, containerImage) => {
const providerKey = containerImage.image.startsWith("ghcr.io/") ? "Github" : "DockerHub";
const [identifier, version] = containerImage.image.replace(/^(ghcr\.io\/|docker\.io\/)/, "").split(":");
if (!identifier) return acc;
if (acc.some((item) => item.providerKey === providerKey && item.identifier === identifier)) return acc;
acc.push({
providerKey,
identifier,
iconUrl: containerImage.iconUrl ?? undefined,
name: formatIdentifierName(identifier),
versionFilter: version ? parseImageVersionToVersionFilter(version) : undefined,
alreadyImported: innerProps.repositories.some(
(item) => item.providerKey === providerKey && item.identifier === identifier,
),
});
return acc;
}, []) ?? [],
[docker.data, innerProps.repositories],
);
const handleConfirm = useCallback(() => {
setLoading(true);
innerProps.onConfirm(selectedImages);
setLoading(false);
actions.closeModal();
}, [innerProps, selectedImages, actions]);
const allImagesImported = useMemo(
() => containersImages.every((containerImage) => containerImage.alreadyImported),
[containersImages],
);
const anyImagesImported = useMemo(
() => containersImages.some((containerImage) => containerImage.alreadyImported),
[containersImages],
);
return (
<Stack>
{docker.isPending ? (
<Stack justify="center" align="center">
<Loader size="xl" />
<Title order={3}>{tRepository("importRepositories.loading")}</Title>
</Stack>
) : containersImages.length === 0 ? (
<Stack justify="center" align="center">
<IconBrandDocker stroke={1} size={128} />
<Title order={3}>{tRepository("importRepositories.noImagesFound")}</Title>
</Stack>
) : (
<Stack>
<Accordion defaultValue={!allImagesImported ? "foundImages" : anyImagesImported ? "alreadyImported" : ""}>
<Accordion.Item value="foundImages">
<Accordion.Control disabled={allImagesImported} icon={<IconSquare stroke={1.25} />}>
<Group>
{tRepository("importRepositories.listFoundImages")}
{allImagesImported && (
<Text c="dimmed" size="sm">
{tRepository("importRepositories.allImagesAlreadyImported")}
</Text>
)}
</Group>
</Accordion.Control>
<Accordion.Panel>
{!allImagesImported &&
containersImages
.filter((containerImage) => !containerImage.alreadyImported)
.map((containerImage) => {
return (
<ContainerImageSelector
key={`${containerImage.providerKey}/${containerImage.identifier}`}
containerImage={containerImage}
versionFilterPrecisionOptions={innerProps.versionFilterPrecisionOptions}
onImageSelectionChanged={(isSelected) =>
isSelected
? setSelectedImages([...selectedImages, containerImage])
: setSelectedImages(selectedImages.filter((img) => img !== containerImage))
}
/>
);
})}
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value="alreadyImported">
<Accordion.Control disabled={!anyImagesImported} icon={<IconSquareCheck stroke={1.25} />}>
{tRepository("importRepositories.listAlreadyImportedImages")}
</Accordion.Control>
<Accordion.Panel>
{anyImagesImported &&
containersImages
.filter((containerImage) => containerImage.alreadyImported)
.map((containerImage) => {
return (
<ContainerImageSelector
key={`${containerImage.providerKey}/${containerImage.identifier}`}
containerImage={containerImage}
versionFilterPrecisionOptions={innerProps.versionFilterPrecisionOptions}
/>
);
})}
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</Stack>
)}
<Group justify="flex-end">
<Button variant="default" onClick={actions.closeModal} color="gray.5">
{tRepository("editForm.cancel.label")}
</Button>
<Button onClick={handleConfirm} loading={loading} disabled={selectedImages.length === 0}>
{tRepository("editForm.confirm.label")}
</Button>
</Group>
</Stack>
);
}).withOptions({
defaultTitle(t) {
return t("widget.releases.option.repositories.importForm.title");
},
size: "xl",
});
const parseImageVersionToVersionFilter = (imageVersion: string): ReleasesVersionFilter | undefined => {
const version = /(?<=\D|^)\d+(?:\.\d+)*(?![\d.])/.exec(imageVersion)?.[0];
if (!version) return undefined;
const [prefix, suffix] = imageVersion.split(version);
return {
prefix,
precision: version.split(".").length,
suffix,
};
};
@@ -38,40 +38,34 @@ export const ResourceTable = ({ type, data, isTiny }: ResourceTableProps) => {
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{data {data.map((item) => {
.sort((itemA, itemB) => { return (
const nodeResult = itemA.node.localeCompare(itemB.node); <ResourcePopover key={item.name} item={item}>
if (nodeResult !== 0) return nodeResult; <Popover.Target>
return itemA.name.localeCompare(itemB.name); <TableTr fz={isTiny ? "8px" : "xs"}>
}) <td>
.map((item) => { <Group wrap="nowrap" gap={isTiny ? 8 : "xs"}>
return ( <Indicator size={isTiny ? 4 : 8} children={null} color={item.isRunning ? "green" : "yellow"} />
<ResourcePopover key={item.id} item={item}> <Text lineClamp={1} fz={isTiny ? "8px" : "xs"}>
<Popover.Target> {item.name}
<TableTr fz={isTiny ? "8px" : "xs"}> </Text>
<td> </Group>
<Group wrap="nowrap" gap={isTiny ? 8 : "xs"}> </td>
<Indicator size={isTiny ? 4 : 8} children={null} color={item.isRunning ? "green" : "yellow"} /> {item.type === "storage" ? (
<Text lineClamp={1} fz={isTiny ? "8px" : "xs"}> <td style={{ WebkitLineClamp: "1" }}>{item.node}</td>
{item.name} ) : (
</Text> <>
</Group> <td style={{ whiteSpace: "nowrap" }}>{(item.cpu.utilization * 100).toFixed(1)}%</td>
</td> <td style={{ whiteSpace: "nowrap" }}>
{item.type === "storage" ? ( {(item.memory.total ? (item.memory.used / item.memory.total) * 100 : 0).toFixed(1)}%
<td style={{ WebkitLineClamp: "1" }}>{item.node}</td> </td>
) : ( </>
<> )}
<td style={{ whiteSpace: "nowrap" }}>{(item.cpu.utilization * 100).toFixed(1)}%</td> </TableTr>
<td style={{ whiteSpace: "nowrap" }}> </Popover.Target>
{(item.memory.total ? (item.memory.used / item.memory.total) * 100 : 0).toFixed(1)}% </ResourcePopover>
</td> );
</> })}
)}
</TableTr>
</Popover.Target>
</ResourcePopover>
);
})}
</TableTbody> </TableTbody>
</Table> </Table>
); );
+3 -3
View File
@@ -156,7 +156,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
> >
<MaskedOrNormalImage <MaskedOrNormalImage
className="releases-repository-header-icon" className="releases-repository-header-icon"
imageUrl={repository.iconUrl ?? Providers[repository.providerKey].iconUrl} imageUrl={repository.iconUrl ?? Providers[repository.providerKey]?.iconUrl}
hasColor={hasIconColor} hasColor={hasIconColor}
style={{ style={{
width: "1em", width: "1em",
@@ -474,7 +474,7 @@ const ExpandedDisplay = ({ repository, hasIconColor }: ExtendedDisplayProps) =>
<Group className="releases-repository-expanded-header-provider-wrapper" gap={5} align="center"> <Group className="releases-repository-expanded-header-provider-wrapper" gap={5} align="center">
<MaskedOrNormalImage <MaskedOrNormalImage
className="releases-repository-expanded-header-provider-icon" className="releases-repository-expanded-header-provider-icon"
imageUrl={Providers[repository.providerKey].iconUrl} imageUrl={Providers[repository.providerKey]?.iconUrl}
hasColor={hasIconColor} hasColor={hasIconColor}
style={{ style={{
width: "1em", width: "1em",
@@ -482,7 +482,7 @@ const ExpandedDisplay = ({ repository, hasIconColor }: ExtendedDisplayProps) =>
}} }}
/> />
<Text className="releases-repository-expanded-header-provider-name" size="xs" c="iconColor" ff="monospace"> <Text className="releases-repository-expanded-header-provider-name" size="xs" c="iconColor" ff="monospace">
{Providers[repository.providerKey].name} {Providers[repository.providerKey]?.name}
</Text> </Text>
</Group> </Group>
</Group> </Group>
@@ -3,7 +3,16 @@ export interface ReleasesProvider {
iconUrl: string; iconUrl: string;
} }
export const Providers = { interface ProvidersProps {
[key: string]: ReleasesProvider;
DockerHub: ReleasesProvider;
Github: ReleasesProvider;
Gitlab: ReleasesProvider;
Npm: ReleasesProvider;
Codeberg: ReleasesProvider;
}
export const Providers: ProvidersProps = {
DockerHub: { DockerHub: {
name: "Docker Hub", name: "Docker Hub",
iconUrl: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/docker.svg", iconUrl: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/docker.svg",
@@ -24,10 +33,4 @@ export const Providers = {
name: "Codeberg", name: "Codeberg",
iconUrl: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/codeberg.svg", iconUrl: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/codeberg.svg",
}, },
} as const satisfies Record<string, ReleasesProvider>;
export type ProviderKey = keyof typeof Providers;
export const isProviderKey = (key: string): key is ProviderKey => {
return key in Providers;
}; };
Loaded 100 of 106 files, more files were not shown because too many files have changed in this diff. Show more