Compare commits

..
Author SHA1 Message Date
Meier Lukas 7251938294 feat: add board preview 2024-05-04 22:27:04 +02:00
Meier Lukas b888fad1fc fix: modal transition not working (#433) 2024-05-04 20:58:44 +02:00
Meier Lukas 5abd438a49 fix: tsx not working (#434)
* fix: tsx not working

* fix: format issues
2024-05-04 20:58:30 +02:00
Meier Lukas b1e065f1da feat: board access group permissions (#422)
* fix: cache is not exportet from react

* fix: format issue

* wip: add usage of group permissions

* feat: show inherited groups and add manage group

* refactor: improve board access management

* chore: address pull request feedback

* fix: type issues

* fix: migrations

* test: add unit tests for board permissions, permissions and board router

* test: add unit tests for board router and get current user permissions method

* fix: format issues

* fix: deepsource issue
2024-05-04 18:34:41 +02:00
Manuel ca49a01352 config: add vscode launch profiles (#432) 2024-05-04 18:18:56 +02:00
homarr-renovate[bot] 2139f81a1d chore(deps): update dependency tsx to ^4.9.1 (#428)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-04 17:18:56 +02:00
homarr-renovate[bot] fb43fc6046 fix(deps): update tanstack-query monorepo to ^5.34.1 (#427)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-04 17:18:35 +02:00
homarr-renovate[bot] 3047f3bf2b chore(deps): update vitest monorepo to ^1.6.0 (#426)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-03 17:31:16 +02:00
homarr-renovate[bot] 4d313be37e chore(deps): update dependency tsx to ^4.9.0 (#425)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-03 12:05:21 +02:00
homarr-renovate[bot] 11c20ce711 fix(deps): update dependency zod to ^3.23.6 (#424)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-03 06:50:55 +02:00
homarr-renovate[bot] 90b9d56729 fix(deps): update mantine monorepo to ^7.9.0 (#423)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-02 16:37:01 +02:00
homarr-renovate[bot] e8805dfb5a chore(deps): update dependency @types/node to ^20.12.8 (#415)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-01 21:57:56 +02:00
Meier Lukas 7c98b65401 fix: server data is loaded on settings page #363 (#414) 2024-05-01 21:57:24 +02:00
Manuel 84801d3174 refactor: move integration pages to management pages (#419) 2024-05-01 21:57:18 +02:00
Meier Lukas fba767bde3 fix: cache is not exportet from react (#417)
* fix: cache is not exportet from react

* fix: format issue

* fix: type issue
2024-05-01 21:17:28 +02:00
Manuel 48c2473ab3 fix: docker development path in package.json (#416) 2024-05-01 21:10:31 +02:00
Meier Lukas ad1796dc02 refactor: add dev directory to organize dev files #398 (#408)
* refactor: add dev directory to organize dev files #398

* chore: address pull request feedback
2024-05-01 19:58:58 +02:00
homarr-renovate[bot] 14d941e57a fix(deps): update dependency drizzle-orm to ^0.30.10 (#413)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-01 19:55:13 +02:00
Meier Lukas 0278e3a854 fix: add rtl compliance for symbols #366 (#406) 2024-05-01 12:05:09 +02:00
homarr-renovate[bot] efaf721b5a chore(deps): update dependency tsx to ^4.8.2 (#412)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-01 12:04:49 +02:00
homarr-renovate[bot] e556bf3e66 fix(deps): update dependency sass to ^1.76.0 (#410)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-01 07:12:55 +02:00
homarr-renovate[bot] a138e83f16 chore(deps): update dependency tsx to ^4.8.1 (#411)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-01 07:12:16 +02:00
homarr-renovate[bot] 7bc7bd6bf9 fix(deps): update dependency @trpc/client to v11.0.0-rc.364 (#409)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-01 07:06:45 +02:00
homarr-renovate[bot] 0020680ca0 fix(deps): update dependency @trpc/client to v11.0.0-rc.362 (#407)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-30 22:13:19 +02:00
homarr-renovate[bot] 876f21a26d fix(deps): update tiptap monorepo to v2.3.1 (#403)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-30 21:56:27 +02:00
homarr-renovate[bot] 46bb3ee0c9 fix(deps): update dependency @tabler/icons-react to ^3.3.0 (#405)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-30 21:55:51 +02:00
homarr-renovate[bot] 6dff7c48e2 fix(deps): update dependency @trpc/client to v11.0.0-rc.361 (#404)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-30 21:33:40 +02:00
Meier Lukas 7ab9dc2501 feat: implement board access control (#349)
* feat: implement board access control

* fix: deepsource issues

* wip: address pull request feedback

* chore: address pull request feedback

* fix: format issue

* test: improve tests

* fix: type and lint issue

* chore: address pull request feedback

* refactor: rename board procedures
2024-04-30 21:32:55 +02:00
homarr-renovate[bot] 56388eb8ef fix(deps): update dependency zod to ^3.23.5 (#399)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-30 21:16:57 +02:00
homarr-renovate[bot] c988f3d7f8 fix(deps): update tanstack-query monorepo to ^5.32.1 (#402)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-30 21:15:52 +02:00
homarr-renovate[bot] e5d024c62f chore(deps): update vitest monorepo to ^1.5.3 (#401)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-30 21:14:56 +02:00
homarr-renovate[bot] fc1360d6bb fix(deps): update dependency video.js to ^8.12.0 (#400)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-30 21:14:24 +02:00
homarr-renovate[bot] 584483aa9c fix(deps): update typescript-eslint monorepo to ^7.8.0 (#395)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-30 21:12:43 +02:00
homarr-renovate[bot] 47accae90d fix(deps): update dependency @trpc/client to v11.0.0-rc.359 (#394)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-29 21:55:54 +02:00
homarr-renovate[bot] 03c2993611 fix(deps): update dependency dayjs to ^1.11.11 (#393)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-29 21:51:30 +02:00
homarr-renovate[bot] 55822ee7a5 fix(deps): update dependency ws to ^8.17.0 (#392)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-29 21:50:52 +02:00
Meier Lukas 7a9e9fb203 ci: add build workflow (#389) 2024-04-29 21:49:57 +02:00
Meier Lukas 5f247977fa fix: sharp is not installed (#397)
Closes #320
2024-04-29 21:46:58 +02:00
Meier Lukas 036925bf78 feat: add user groups (#376)
* feat: add user groups

* wip: add unit tests

* wip: add more tests and normalized name for creation and update

* test: add unit tests for group router

* fix: type issues, missing mysql schema, rename column creator_id to owner_id

* fix: lint and format issues

* fix: deepsource issues

* fix: forgot to add log message

* fix: build not working

* chore: address pull request feedback

* feat: add mysql migration and fix merge conflicts

* fix: format issue and test issue
2024-04-29 21:46:30 +02:00
Meier Lukas 621f6c81ae feat: add user invite management (#338)
* feat: add invite management page

* refactor: improve existing translations

* test: add test for invite router

* feat: update mysql schema to match sqlite schema

* fix: format issues

* fix: deepsource issues

* fix: lint issues

* chore: address pull request feedback
2024-04-29 12:09:34 +02:00
Manuel 16e42d654d feat: read all packages on about page (#391) 2024-04-27 21:51:35 +02:00
homarr-renovate[bot] 45ffbadeec fix(deps): update dependency better-sqlite3 to ^9.6.0 (#386)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-27 18:28:53 +02:00
homarr-renovate[bot] 73a72dc675 fix(deps): update dependency @t3-oss/env-nextjs to ^0.10.1 (#375)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-27 18:25:13 +02:00
Manuel 42a5a1a563 feat: add about page (#388) 2024-04-27 18:24:27 +02:00
homarr-renovate[bot] 0cdf0b73bf fix(deps): update tanstack-query monorepo to ^5.32.0 (#374)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-27 15:35:21 +02:00
homarr-renovate[bot] bfdc8d584b fix(deps): update dependency next-auth to v5.0.0-beta.17 (#383)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-27 15:34:44 +02:00
homarr-renovate[bot]andMeier Lukas 6dfdc8971d chore(deps): update dependency @types/better-sqlite3 to v7.6.10 (#371)
* chore(deps): update dependency @types/better-sqlite3 to v7.6.10

* chore(deps): update dependency @types/better-sqlite3 to v7.6.10

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-27 15:33:51 +02:00
homarr-renovate[bot] f5002c3265 fix(deps): update dependency drizzle-orm to ^0.30.9 (#370)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-27 15:26:13 +02:00
homarr-renovate[bot] cc93824fe0 chore(deps): update turbo monorepo to ^1.13.3 (#385)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-27 14:36:32 +02:00
homarr-renovate[bot]andMeier Lukas 392a185640 fix(deps): update dependency zod to ^3.23.4 (#373)
* fix(deps): update dependency zod to ^3.23.4

* fix: type issue

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-27 14:35:07 +02:00
homarr-renovate[bot] 92b417a37e fix(deps): update mantine monorepo to ^7.8.1 (#377)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-27 14:25:20 +02:00
homarr-renovate[bot] 3b5bd809a4 chore(deps): update pnpm to v9.0.6 (#372)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-27 14:21:46 +02:00
homarr-renovate[bot] 51ea67fc88 fix(deps): update dependency mysql2 to v3.9.7 (#369)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-27 14:19:53 +02:00
homarr-renovate[bot] d88b6def75 fix(deps): update dependency @trpc/client to v11.0.0-rc.354 (#368)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-27 14:18:45 +02:00
homarr-renovate[bot] ea528ea076 fix(deps): update dependency @auth/drizzle-adapter to v1 (#359)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-27 14:13:42 +02:00
homarr-renovate[bot] 847adffe7b fix(deps): update tiptap monorepo to v2.3.0 (#317)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-27 14:06:05 +02:00
homarr-renovate[bot] f204f34850 chore(deps): update vitest monorepo to ^1.5.2 (#380)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-27 12:57:39 +02:00
homarr-renovate[bot] b8780d24bc chore(deps): update dependency tsx to ^4.7.3 (#382)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-27 12:52:58 +02:00
homarr-renovate[bot] a9e593e004 chore(deps): update react monorepo (#384)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-27 12:49:17 +02:00
homarr-renovate[bot] 75e4e8176e fix(deps): update nextjs monorepo to ^14.2.3 (#381)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-27 12:31:46 +02:00
homarr-renovate[bot] 84175e6a7e fix(deps): update dependency postcss-preset-mantine to ^1.15.0 (#387)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-27 12:24:55 +02:00
Meier Lukas b78d32b81c fix: nextjs is slow dev server (#364)
* fix: nextjs slow compile time

* fix: change optimized package imports and transpile packages

* fix: format issue
2024-04-25 22:06:15 +02:00
homarr-renovate[bot] 04a313186f fix(deps): update dependency drizzle-kit to ^0.20.17 (#367)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-24 20:31:19 +02:00
homarr-renovate[bot] 0342a177bb fix(deps): update typescript-eslint monorepo to ^7.7.1 (#354)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-24 06:31:49 +02:00
Meier Lukas e47187512c Merge pull request #379 from homarr-labs/main
chore: add codeowners (#378)
2024-04-23 19:43:42 +02:00
Meier Lukas 974b4cf115 chore: add codeowners (#378) 2024-04-23 19:27:04 +02:00
Meier Lukas c0594f2419 docs: add notice about unstable and no support 2024-04-23 19:20:30 +02:00
Meier Lukas 4970c3a1b2 fix: defaultTitle invalid attribute on div (#350) 2024-04-20 12:52:02 +02:00
homarr-renovate[bot] 9615568ff3 chore(deps): update dependency @types/react to ^18.2.79 (#355)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-20 12:51:19 +02:00
homarr-renovate[bot] f58d0ddd94 fix(deps): update dependency drizzle-kit to ^0.20.16 (#362)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-20 12:50:47 +02:00
homarr-renovate[bot] 8c0ce672d0 chore(deps): update dependency @types/eslint to ^8.56.10 (#360)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-20 12:50:23 +02:00
homarr-renovate[bot] f6c0f3ab78 fix(deps): update dependency @auth/core to ^0.30.0 (#358)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-20 12:49:58 +02:00
homarr-renovate[bot] e7689beee8 chore(deps): update dependency @types/video.js to ^7.3.58 (#353)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-20 12:48:24 +02:00
homarr-renovate[bot] 20f44d3494 fix(deps): update dependency ioredis to v5.4.1 (#357)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-20 12:48:04 +02:00
homarr-renovate[bot] 0ba6455982 fix(deps): update dependency @trpc/client to v11.0.0-rc.345 (#351)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-20 12:28:28 +02:00
homarr-renovate[bot] 7f9f29bba5 chore(deps): update pnpm to v9 (#356)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-20 11:25:46 +02:00
homarr-renovate[bot] cb2b934c83 fix(deps): update dependency zod to ^3.22.5 (#361)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-20 11:17:57 +02:00
homarr-renovate[bot] b465b95ca2 fix(deps): update nextjs monorepo to ^14.2.2 (#345)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-20 11:12:03 +02:00
homarr-renovate[bot] a6d65fda6b fix(deps): update dependency mysql2 to v3.9.6 (#311)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-20 11:04:00 +02:00
homarr-renovate[bot] 3e3299a358 fix(deps): update mantine monorepo to ^7.8.0 (#343)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-13 16:04:56 +02:00
homarr-renovate[bot] 859fdf4c44 fix(deps): update tanstack-query monorepo to ^5.29.2 (#340)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-13 16:03:53 +02:00
homarr-renovate[bot] 0cc5225e32 fix(deps): update dependency sass to ^1.75.0 (#347)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-13 14:27:29 +02:00
homarr-renovate[bot] 01f471ccce chore(deps): update dependency @types/react to ^18.2.78 (#346)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-13 14:27:02 +02:00
homarr-renovate[bot] c40533e758 fix(deps): update dependency @tabler/icons-react to ^3.2.0 (#342)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-13 14:26:19 +02:00
homarr-renovate[bot] 0edce9fd0b chore(deps): update dependency @types/eslint to ^8.56.9 (#341)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-13 14:25:28 +02:00
homarr-renovate[bot] 49e45e8122 fix(deps): update dependency postcss-preset-mantine to ^1.14.4 (#336)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-13 14:23:51 +02:00
homarr-renovate[bot]andMeier Lukas f50550b3e1 fix(deps): update dependency drizzle-orm to ^0.30.8 (#339)
* fix(deps): update dependency drizzle-orm to ^0.30.8

* fix: format issue

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-13 14:23:04 +02:00
homarr-renovate[bot] 48a4b8ba7e fix(deps): update dependency better-sqlite3 to ^9.5.0 (#335)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-13 14:12:59 +02:00
homarr-renovate[bot] ed5e5d1edc fix(deps): update dependency @trpc/client to v11.0.0-rc.334 (#334)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-13 14:12:27 +02:00
homarr-renovate[bot] 2ccb7d1765 fix(deps): update typescript-eslint monorepo to ^7.6.0 (#329)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-13 14:11:32 +02:00
Meier Lukas 9ed298d641 refactor: improve board manage page (#323)
* refactor: improve board manage page

* chore: address pull request feedback
2024-04-13 12:42:03 +02:00
Meier Lukas 6b1879cbb1 feat: add notebook widget (#294)
* feat: add nestjs replacement, remove nestjs

* feat: add notebook widget

* fix: format issue

* fix: add missing tiptap packages

* refactor: improve structure of table options

* fix: downgrade to tiptap 2.2.5 as not yet supported by mantine/tiptap

* fix: format issue

* fix: deepsource issues

* fix: typecheck issues

* refactor: move default notebook content to seperate file

* fix: format issue
2024-04-13 12:32:55 +02:00
Meier Lukas 8f4897186c feat: add iframe widget (#291)
* feat: add nestjs replacement, remove nestjs

* feat: add iframe widget

* fix: format issue

* fix: format issue

* fix: format issue

* fix: lockfile frozen
2024-04-13 11:58:41 +02:00
Meier Lukas 82e9887f36 feat: add video widget (#287)
* feat: add nestjs replacement, remove nestjs

* feat: add video widget

* feat: add notice about youtube not supported with video.js

* fix: format issue

* fix: format issue
2024-04-13 11:44:16 +02:00
Meier Lukas 80d2d485b8 feat: add weather widget (#286)
* feat: add nestjs replacement, remove nestjs

* feat: add weather widget

* fix: lock issue

* fix: format issue

* fix: deepsource issues

* fix: change timezone to auto
2024-04-13 11:34:55 +02:00
homarr-renovate[bot]andMeier Lukas 7fb0decd5b fix(deps): update dependency @auth/drizzle-adapter to ^0.9.0 (#328)
* fix(deps): update dependency @auth/drizzle-adapter to ^0.9.0

* fix(deps): update dependency @auth/drizzle-adapter to ^0.9.0

* Merge branch 'dev' into renovate/auth-drizzle-adapter-0.x

* fix: format issues

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-11 21:24:50 +02:00
homarr-renovate[bot] 719d1cd845 fix(deps): update dependency @auth/core to ^0.29.0 (#327)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-11 21:15:18 +02:00
homarr-renovate[bot] 27c858dc81 fix(deps): update dependency jotai to ^2.8.0 (#326)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-11 21:14:04 +02:00
homarr-renovate[bot]andMeier Lukas 57c6c7385a chore(deps): update dependency node to v20.12.2 (#331)
* chore(deps): update dependency node to v20.12.2

* fix: format issues

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-11 21:09:19 +02:00
homarr-renovate[bot] e0bb40a47e chore(deps): update vitest monorepo to ^1.5.0 (#344)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-11 20:58:08 +02:00
homarr-renovate[bot] 6a2ed0a8cf chore(deps): update react monorepo (#330)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-11 20:56:49 +02:00
homarr-renovate[bot] cb65d465aa chore(deps): update dependency typescript to ^5.4.5 (#337)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-11 20:56:02 +02:00
Meier Lukas 8988880439 feat: add app actions (#322)
* feat: add app actions

* fix: broken lockfile

* fix: app should only load when one is selected and server data not fail when no app found

* fix: add missing dependency

* fix: format issue
2024-04-09 21:51:00 +02:00
Meier Lukas e6e6e4e7d6 fix: xterm-addon-fit deprecated (#321) 2024-04-09 21:50:35 +02:00
Meier Lukas 669c6c8955 config: migrate from nestjs to tasks and websocket server in docker (#316)
* feat: make tasks script run in docker

* feat: make websocket server work in docker

* fix: format issue

* fix: broken lockfile

* fix: non matching typescript versions
2024-04-07 11:32:29 +02:00
Meier Lukas 35ca732744 feat: add web search engines (#290) 2024-04-06 17:47:12 +02:00
homarr-renovate[bot]andMeier Lukas 75224e0533 fix(deps): update tiptap monorepo to ^2.2.5 (#315)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-06 16:34:35 +02:00
homarr-renovate[bot]andMeier Lukas a5b8fdb397 fix(deps): update xterm monorepo (#312)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-06 16:34:06 +02:00
homarr-renovate[bot] fffefbb4c8 fix(deps): update tanstack-query monorepo to ^5.29.0 (#313)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-06 16:30:04 +02:00
homarr-renovate[bot] a6729a2b18 chore(deps): update dependency typescript to ^5.4.4 (#310)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-06 16:28:11 +02:00
homarr-renovate[bot] 48f45d7377 fix(deps): update dependency node-cron to ^3.0.3 (#309)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-06 16:25:17 +02:00
homarr-renovate[bot] 50c162444c fix(deps): update dependency @trpc/client to v11.0.0-rc.332 (#308)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-06 16:21:19 +02:00
homarr-renovate[bot] 6cea1cbd85 chore(deps): update dependency tsx to ^4.7.2 (#307)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-06 16:20:34 +02:00
homarr-renovate[bot] e68a2e5813 fix(deps): update dependency sass to ^1.74.1 (#306)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-06 16:19:57 +02:00
homarr-renovate[bot] 7db5adf110 chore(deps): update dependency @types/react-dom to ^18.2.24 (#305)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-06 16:19:30 +02:00
homarr-renovate[bot]andMeier Lukas 362257cf2b fix(deps): update dependency drizzle-orm to ^0.30.7 (#303)
* fix(deps): update dependency drizzle-orm to ^0.30.7

* fix: duplicate occurence of dependency

* fix: format issue

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-06 16:18:28 +02:00
homarr-renovate[bot]andMeier Lukas 7bff8e5148 fix(deps): update dependency better-sqlite3 to ^9.4.5 (#302)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-06 15:58:53 +02:00
homarr-renovate[bot]andMeier Lukas fbea026122 chore(deps): update dependency node to v20.12.1 (#301)
* chore(deps): update dependency node to v20.12.1

* fix: format issue

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-06 15:54:41 +02:00
Meier Lukas 1fc2df19ec fix: revert mysql2 to version 3.9.2 as with 3.9.3 nextjs build fails 2024-04-04 21:59:23 +02:00
Meier Lukas 1936596c04 feat: add nestjs replacement, remove nestjs (#285)
* feat: add nestjs replacement, remove nestjs

* fix: format issues

* fix: dependency issues

* fix: dependency issues

* fix: format issue

* fix: wrong channel used for logging channel
2024-04-04 18:31:40 +02:00
ManuelandMeier Lukas c82915c6dc feat: add real time logger page (#276)
* feat: add real time logger

* feat: add subscription for logging

* feat: use timestamp and level in xterm, migrate to new xterm package

* feat: improve design on log page

* fit: remove xterm fit addon

* fix: dispose terminal correctly

* style: format code

* refactor: add jsdoc for redis-transport

* fix: redis connection not possible sometimes

* feat: make terminal full size

* fix: deepsource issues

* fix: lint issue

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-04 18:07:23 +02:00
homarr-renovate[bot] 2fb0535260 chore(deps): update dependency @types/react to ^18.2.74 (#300)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-02 21:03:45 +02:00
homarr-renovate[bot] a4f316f10f fix(deps): update tanstack-query monorepo to ^5.28.14 (#299)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-02 21:03:30 +02:00
homarr-renovate[bot] 54a7d1f4ae fix(deps): update typescript-eslint monorepo to ^7.5.0 (#298)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-02 21:02:57 +02:00
homarr-renovate[bot] 538c79528d chore(deps): update pnpm to v8.15.6 (#297)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-02 21:01:36 +02:00
homarr-renovate[bot]andMeier Lukas 3a1b4c3738 chore(deps): update turbo monorepo to ^1.13.2 (#296)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-02 20:58:27 +02:00
homarr-renovate[bot]andMeier Lukas fdf060a3a2 chore(deps): update dependency @types/eslint to ^8.56.7 (#295)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-02 20:54:14 +02:00
homarr-renovate[bot]andMeier Lukas fbc69f6078 fix(deps): update dependency @auth/drizzle-adapter to ^0.8.2 (#293)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-02 20:48:30 +02:00
homarr-renovate[bot]andMeier Lukas ba16ba1382 fix(deps): update dependency @auth/core to ^0.28.2 (#292)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-02 20:43:56 +02:00
homarr-renovate[bot] d8d25d5037 fix(deps): update dependency @trpc/client to v11.0.0-rc.330 (#288)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-02 20:38:52 +02:00
homarr-renovate[bot] d06528f311 fix(deps): update dependency jotai to ^2.7.2 (#284)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-02 06:47:43 +02:00
homarr-renovate[bot] 6ef58beead fix(deps): update dependency drizzle-orm to ^0.30.6 (#282)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-02 06:45:57 +02:00
homarr-renovate[bot]andMeier Lukas e74926d382 fix(deps): update tanstack-query monorepo (#280)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-02 06:45:17 +02:00
homarr-renovate[bot]andMeier Lukas 085bd94704 chore(deps): update dependency node to v20.12.0 (#278)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-02 06:37:04 +02:00
homarr-renovate[bot] 415c632495 chore(deps): update react monorepo (#275)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-02 06:31:25 +02:00
homarr-renovate[bot] e1b62ecd53 fix(deps): update dependency mantine-react-table to v2.0.0-beta.1 (#277)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-04-02 06:30:45 +02:00
homarr-renovate[bot]andMeier Lukas b0bba1418b fix(deps): update dependency mysql2 to ^3.9.3 (#273)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-01 18:23:42 +02:00
homarr-renovate[bot]andMeier Lukas c9e15805a9 fix(deps): update mantine monorepo to ^7.7.1 (#272)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-01 18:16:19 +02:00
homarr-renovate[bot]andMeier Lukas dd7f04e395 fix(deps): update dependency @typescript-eslint/eslint-plugin to ^7.4.0 (#271)
* fix(deps): update dependency @typescript-eslint/eslint-plugin to ^7.4.0

* fix: lint issue

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-04-01 18:10:14 +02:00
Manuel c00d856e1a refactor: improve user table design (#279) 2024-03-26 22:55:43 +01:00
ManuelandMeier Lukas 1825e56349 feat: add redis (#242)
* feat: add refis

* feat: add redis package

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>

* feat: add example docker compose, add redis connection in package

* fix: usage of client after subscribe

* feat: add logger for redis

* refactor: format files

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-25 21:09:40 +01:00
homarr-renovate[bot] c4a4452839 fix(deps): update dependency @typescript-eslint/parser to ^7.4.0 (#270)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-25 19:38:15 +01:00
homarr-renovate[bot] 36073dbc5e fix(deps): update dependency vite to ^5.2.6 (#262)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-25 19:24:02 +01:00
homarr-renovate[bot] 724b348106 fix(deps): update dependency winston to v3.13.0 (#263)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-25 19:23:43 +01:00
homarr-renovate[bot] 4783aaea20 fix(deps): update tanstack-query monorepo to ^5.28.8 (#265)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-25 19:23:05 +01:00
homarr-renovate[bot] 98a8eaac09 fix(deps): update dependency @auth/drizzle-adapter to ^0.8.1 (#267)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-25 19:22:18 +01:00
homarr-renovate[bot] ba27871765 fix(deps): update dependency next-auth to v5.0.0-beta.16 (#268)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-25 19:12:21 +01:00
homarr-renovate[bot] c72f4bb80d fix(deps): update dependency @auth/core to ^0.28.1 (#266)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-25 19:10:42 +01:00
homarr-renovate[bot] 0f59b10c64 chore(deps): update dependency @types/react to ^18.2.70 (#264)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-25 19:07:32 +01:00
Meier Lukas 058a8c4776 feat: add actual user for trpc wss-dev-server (#261)
* feat: add actual user for trpc wss-dev-server #233

* chore: address pull request feedback

* fix: deepsource issue
2024-03-25 18:57:59 +01:00
homarr-renovate[bot]andMeier Lukas 87808c1349 fix(deps): update dependency sharp to ^0.33.3 (#260)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-24 12:20:54 +01:00
homarr-renovate[bot] 8d40f54d7d fix(deps): update dependency vite to ^5.2.5 (#259)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-24 10:49:10 +01:00
homarr-renovate[bot] 78b8cf6b29 chore(deps): update dependency @types/react to ^18.2.69 (#258)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-22 22:37:27 +01:00
homarr-renovate[bot] e4e3463d04 fix(deps): update tanstack-query monorepo to ^5.28.6 (#256)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-22 22:37:11 +01:00
homarr-renovate[bot] d867ab162a chore(deps): update dependency typescript to ^5.4.3 (#254)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-22 22:36:58 +01:00
homarr-renovate[bot] 1dfc6a9f0b chore(deps): update turbo monorepo to ^1.13.0 (#257)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-22 21:00:19 +01:00
homarr-renovate[bot] 7214df3e44 fix(deps): update dependency vite to ^5.2.3 (#253)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-22 20:59:18 +01:00
homarr-renovate[bot]andMeier Lukas 28f630f58b fix(deps): update nextjs monorepo to ^14.1.4 (#252)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-22 20:58:45 +01:00
homarr-renovate[bot] 1f359cd87c chore(deps): update dependency @types/eslint to ^8.56.6 (#251)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-22 20:51:30 +01:00
homarr-renovate[bot] cdc4870925 fix(deps): update dependency drizzle-orm to ^0.30.4 (#250)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-22 20:51:09 +01:00
homarr-renovate[bot] 36e509a474 fix(deps): update typescript-eslint monorepo to ^7.3.1 (#247)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-22 20:50:52 +01:00
homarr-renovate[bot]andMeier Lukas aeed1d7583 chore(deps): update dependency @types/react to ^18.2.67 (#246)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-22 20:50:25 +01:00
homarr-renovate[bot] 0ffb3f18b9 fix(deps): update nest monorepo to ^10.3.5 (#245)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-22 20:41:26 +01:00
homarr-renovate[bot] ab55a1357d fix(deps): update dependency @trpc/client to v11.0.0-next-beta.318 (#234)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-22 20:40:36 +01:00
Meier Lukas 92a8db718c chore: restrict usage of variables shorther than 3 characters (#255)
* chore: restrict usage of variables shorther than 3 characters

* chore: change highlighting to warning
2024-03-20 20:58:24 +01:00
Meier Lukas 361700b239 feat: add board access settings (#249)
* wip: add board access settings

* wip: add user access control

* wip: add user access control

* feat: add user access control

* refactor: move away from mantine-modal-manager

* fix: ci issues and failing tests

* fix: lint issue

* fix: format issue

* fix: deepsource issues

* chore: address pull request feedback
2024-03-20 20:30:58 +01:00
homarr-renovate[bot] 4753bc7162 chore(deps): update dependency @types/node to ^20.11.30 (#248)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-19 21:03:42 +01:00
homarr-renovate[bot] 4a52c6e3bc chore(deps): update pnpm to v8.15.5 (#244)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-18 11:58:40 +01:00
homarr-renovate[bot] c5428efd60 fix(deps): update dependency @ianvs/prettier-plugin-sort-imports to ^4.2.1 (#243)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-17 09:43:55 +01:00
Manuel 0440af3566 fix: add sharp (#241) 2024-03-16 19:01:02 +01:00
homarr-renovate[bot] 30278f37d2 fix(deps): update tanstack-query monorepo to ^5.28.4 (#235)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-16 16:47:27 +01:00
homarr-renovate[bot] 333aaf34dc fix(deps): update dependency @tabler/icons-react to ^3.1.0 (#236)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-16 16:47:19 +01:00
homarr-renovate[bot] 9ba6f91570 chore(deps): update node.js to v20 (#237)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-16 16:47:11 +01:00
homarr-renovate[bot] 75ab823019 fix(deps): update dependency jotai to ^2.7.1 (#238)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-16 15:52:54 +01:00
homarr-renovate[bot] 8b009db4fe chore(deps): update vitest monorepo to ^1.4.0 (#239)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-16 15:51:55 +01:00
homarr-renovate[bot] 404f2945dc fix(deps): update dependency eslint-plugin-react to ^7.34.1 (#240)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-16 15:51:47 +01:00
ManuelandMeier Lukas 98c353349a config: add dockerfile (#214)
* config: add dockerfile

* feat: add working dockerfile for nest and next app with sqlite migration

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-16 15:51:34 +01:00
homarr-renovate[bot] a4951037e3 chore(deps): update dependency @swc/core to ^1.4.8 (#223)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-14 18:55:37 +01:00
homarr-renovate[bot] cf57b33d2c chore(deps): update dependency vite-tsconfig-paths to ^4.3.2 (#228)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-14 18:55:27 +01:00
homarr-renovate[bot] 64354e4b43 fix(deps): update dependency @tabler/icons-react to ^3.0.3 (#229)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-14 18:55:13 +01:00
homarr-renovate[bot] 4bab91f8de fix(deps): update dependency sass to ^1.72.0 (#230)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-14 18:53:27 +01:00
homarr-renovate[bot] 603b8a01af chore(deps): update dependency @types/react to ^18.2.66 (#231)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-14 18:53:13 +01:00
homarr-renovate[bot] 90465175f9 fix(deps): update dependency drizzle-orm to ^0.30.2 (#232)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-14 18:52:59 +01:00
Manuel 4f375cbe6d feat: add trpc websocket (#205) 2024-03-14 18:43:47 +01:00
homarr-renovate[bot] 9ae99ad06c chore(deps): update dependency @types/react-dom to ^18.2.22 (#224)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-13 19:01:00 +01:00
homarr-renovate[bot] 9a203e8fac fix(deps): update tanstack-query monorepo to ^5.28.0 (#225)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-13 19:00:50 +01:00
homarr-renovate[bot] 72011147d1 chore(deps): update dependency @types/node to ^20.11.27 (#226)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-13 19:00:41 +01:00
homarr-renovate[bot] 179ceaebf0 fix(deps): update dependency @ianvs/prettier-plugin-sort-imports to ^4.2.0 (#227)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-13 19:00:31 +01:00
homarr-renovate[bot]andMeier Lukas fc29db0151 fix(deps): update mantine monorepo to ^7.6.2 (#221)
* fix(deps): update mantine monorepo to ^7.6.2

* fix: lint issue as use-element-size ref is typed correctly again

* fix: remove unnecessary type convertion

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-13 08:29:05 +01:00
homarr-renovate[bot] b63b2c8e90 fix(deps): update dependency mantine-modal-manager to ^7.6.2 (#222)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-13 08:15:02 +01:00
homarr-renovate[bot]andMeier Lukas 2ee5027a3c chore(deps): update turbo monorepo to ^1.12.5 (#202)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-12 22:01:49 +01:00
homarr-renovate[bot]andMeier Lukas 084adeb166 chore(deps): update dependency @types/node to ^20.11.26 (#219)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-12 21:57:12 +01:00
homarr-renovate[bot] ea79953353 fix(deps): update typescript-eslint monorepo to ^7.2.0 (#218)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-12 21:49:55 +01:00
homarr-renovate[bot] a3c9afddab chore(deps): update dependency vite to ^5.1.6 (#217)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-12 21:49:08 +01:00
homarr-renovate[bot] efbc4cf8fe fix(deps): update tanstack-query monorepo (#216)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-12 21:48:21 +01:00
homarr-renovate[bot]andMeier Lukas 1c222ed89c fix(deps): update dependency @tabler/icons-react to v3 (#215)
* fix(deps): update dependency @tabler/icons-react to v3

* fix: type issues caused by removal of TablerIconProps

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-12 21:46:37 +01:00
Meier Lukas 24ec13c2ab feat: add mysql support (#212)
* feat: add mysql support

* fix: lockfile broken

* fix: ci issues

* fix: ci issues

* fix: ci issues
2024-03-12 21:23:45 +01:00
Meier Lukas c4ff968cbc feat: add app widget (#206)
* refactor: move server api to api package

* feat: add app widget

* refactor: add element size for widget components on board

* feat: add resize listener for widget width

* feat: add widget app input

* refactor: add better responsibe layout, add missing translations

* fix: ci issues

* fix: deepsource issues

* chore: address pull request feedback
2024-03-12 21:23:25 +01:00
homarr-renovate[bot] 7d5b999ab8 chore(deps): update dependency @types/react to ^18.2.65 (#220)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-12 20:09:45 +01:00
Manuel 5000c4f008 config: fix pr template 2024-03-10 12:42:14 +01:00
homarr-renovate[bot] 851e3701c2 chore(deps): update dependency dotenv-cli to ^7.4.1 (#213)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-10 11:32:21 +01:00
TagaishiandMeier Lukas edcba9ceb6 feat: Clock widget and dayjs locale standard (#79)
* feat: Clock widget and dayjs locale standard

Co-authored-by: Meier Lukas
- Widget options modifications
<meierschlumpf@gmail.com>

* perf: add improved time state for clock widget

* fix: final fixes

* refactor: unify selectOptions

* chore: fix CI & remove serverdata from clock widget

* chore: Change custom title to be under a toggle

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-09 19:25:48 +01:00
Tagaishi dceec34929 Merge branch 'dev' of https://github.com/homarr-labs/homarr into dev 2024-03-09 18:14:16 +01:00
Tagaishi 0ee8c5ad91 fix: create board submit button inverted 2024-03-09 18:14:01 +01:00
homarr-renovate[bot] be9434bf8c chore(deps): update dependency dotenv-cli to ^7.4.0 (#209)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-09 18:13:43 +01:00
homarr-renovate[bot] e34161f33e fix(deps): update dependency drizzle-orm to ^0.30.1 (#210)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-09 18:13:30 +01:00
homarr-renovate[bot] 19243b8681 chore(deps): update dependency @swc/core to ^1.4.6 (#211)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-09 18:12:16 +01:00
homarr-renovate[bot] ef7bd4cffb chore(deps): update dependency @types/node to ^20.11.25 (#200)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-07 07:10:45 +01:00
homarr-renovate[bot] 56d5e709bc fix(deps): update nextjs monorepo to ^14.1.3 (#208)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-07 07:10:35 +01:00
homarr-renovate[bot] a1342ea809 chore(deps): update dependency @types/react-dom to ^18.2.21 (#207)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-07 07:09:53 +01:00
homarr-renovate[bot] 4f41765f10 fix(deps): update dependency drizzle-orm to ^0.29.5 (#203)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-06 21:21:44 +01:00
homarr-renovate[bot] 1728d16177 chore(deps): update dependency @types/react to ^18.2.64 (#201)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-06 21:21:22 +01:00
homarr-renovate[bot] 46955830d9 chore(deps): update dependency typescript to ^5.4.2 (#204)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-06 21:21:09 +01:00
Manuel beb7defd32 feat: add change password form (#199) 2024-03-06 21:20:41 +01:00
homarr-renovate[bot] c0401702f0 chore(deps): update dependency @types/react-dom to ^18.2.20 (#197)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-06 18:23:31 +01:00
homarr-renovate[bot] 82e8a02d8e chore(deps): update dependency @swc/core to ^1.4.5 (#198)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-06 18:23:20 +01:00
homarr-renovate[bot] 345843b01c chore(deps): update dependency @swc/core to ^1.4.4 (#195)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-05 21:11:52 +01:00
Manuel 41b99f191c feat: add edit user page (#173) 2024-03-05 21:10:19 +01:00
homarr-renovate[bot]andMeier Lukas 8c9adb31f3 fix(deps): update tanstack-query monorepo to ^5.25.0 (#196)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-05 20:40:20 +01:00
homarr-renovate[bot] e2bc88d9b6 chore(deps): update dependency @types/react to ^18.2.63 (#194)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-05 20:30:30 +01:00
homarr-renovate[bot] 8df514dec8 fix(deps): update nextjs monorepo to ^14.1.2 (#193)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-05 20:30:02 +01:00
Meier Lukas 8d5984c58a feat: Add apps crud (#174)
* wip: add apps crud

* wip: add edit for apps

* feat: add apps crud

* fix: color of icon for no app results wrong

* ci: fix lint issues

* test: add unit tests for app crud

* ci: fix format issue

* fix: missing rename in edit form

* fix: missing callback deepsource issues
2024-03-04 22:13:40 +01:00
homarr-renovate[bot]andMeier Lukas 70f34efd53 fix(deps): update dependency next-auth to v5.0.0-beta.15 (#167)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-04 21:32:40 +01:00
homarr-renovate[bot]andMeier Lukas 50fd4c17ad fix(deps): update dependency @auth/drizzle-adapter to ^0.8.0 (#170)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-04 21:28:30 +01:00
homarr-renovate[bot] aa19139cf7 fix(deps): update dependency @auth/core to ^0.28.0 (#169)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-04 21:24:13 +01:00
homarr-renovate[bot] 27a63dbf7a fix(deps): update typescript-eslint monorepo to ^7.1.1 (#192)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-04 21:22:48 +01:00
homarr-renovate[bot] b454ed38eb chore(deps): update dependency vite to ^5.1.5 (#191)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-04 21:22:35 +01:00
homarr-renovate[bot] a9abc5bfe2 chore(deps): update dependency @types/react to ^18.2.62 (#190)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-04 21:21:55 +01:00
homarr-renovate[bot]andMeier Lukas acf9d1803f fix(deps): update dependency mantine-modal-manager to ^7.6.1 (#171)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-04 21:20:22 +01:00
homarr-renovate[bot]andMeier Lukas 8c0534f54c fix(deps): update mantine monorepo to ^7.6.1 (#157)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-04 21:15:43 +01:00
homarr-renovate[bot]andMeier Lukas 4cd897d521 fix(deps): update dependency eslint-plugin-react to ^7.34.0 (#189)
* fix(deps): update dependency eslint-plugin-react to ^7.34.0

* fix: lockfile broken

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-04 21:11:01 +01:00
homarr-renovate[bot]andMeier Lukas fde6d6601d fix(deps): update dependency winston to v3.12.0 (#188)
* fix(deps): update dependency winston to v3.12.0

* fix: lockfile broken

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-04 21:09:29 +01:00
homarr-renovate[bot]andMeier Lukas f98046d9e2 fix(deps): update tanstack-query monorepo to ^5.24.8 (#187)
* fix(deps): update tanstack-query monorepo to ^5.24.8

* fix: lockfile broken

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-04 21:07:02 +01:00
homarr-renovate[bot]andMeier Lukas f4b93d1eae fix(deps): update dependency rimraf to v5 (#186)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-04 21:06:46 +01:00
homarr-renovate[bot]andMeier Lukas 0da3253d21 chore(deps): update dependency supertest to ^6.3.4 (#183)
* chore(deps): update dependency supertest to ^6.3.4

* ci: fix format issue

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-04 21:01:31 +01:00
homarr-renovate[bot]andMeier Lukas e1a98f7fc0 fix(deps): update dependency rxjs to ^7.8.1 (#181)
* fix(deps): update dependency rxjs to ^7.8.1

* fix: lockfile broken

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-04 21:01:22 +01:00
homarr-renovate[bot]andMeier Lukas 9ee96a1462 fix(deps): update dependency reflect-metadata to ^0.2.1 (#182)
* fix(deps): update dependency reflect-metadata to ^0.2.1

* fix: lockfile broken

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-04 21:01:10 +01:00
homarr-renovate[bot]andMeier Lukas ada16317e3 chore(deps): update dependency vite to v5 (#185)
* chore(deps): update dependency vite to v5

* fix: lockfile broken

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-04 20:53:54 +01:00
homarr-renovate[bot]andMeier Lukas 27a039d27a chore(deps): update dependency tsconfig-paths to v4 (#184)
* chore(deps): update dependency tsconfig-paths to v4

* ci: fix format issue

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-04 20:47:43 +01:00
homarr-renovate[bot]andMeier Lukas dc2e13dcb5 chore(deps): update dependency @types/express to ^4.17.21 (#176)
* chore(deps): update dependency @types/express to ^4.17.21

* ci: fix format issue

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-04 20:38:11 +01:00
homarr-renovate[bot]andMeier Lukas 6ef69475e2 chore(deps): update dependency ts-node to ^10.9.2 (#178)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-04 20:37:56 +01:00
homarr-renovate[bot]andMeier Lukas 9b8255aac7 chore(deps): update dependency @types/supertest to ^2.0.16 (#177)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-04 20:37:37 +01:00
homarr-renovate[bot]andMeier Lukas fd5fe529e8 chore(deps): update dependency @swc/core to ^1.4.2 (#175)
* chore(deps): update dependency @swc/core to ^1.4.2

* ci: fix typecheck issue

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-04 20:32:23 +01:00
Manuel 9440d04261 feat: add nestjs app (#172) 2024-03-03 21:07:27 +01:00
homarr-renovate[bot] 7f2ab7f192 fix(deps): update tanstack-query monorepo to ^5.24.6 (#168)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-03 16:49:22 +01:00
Meier Lukas bb02163e25 feat: board settings (#137)
* refactor: improve user feedback for general board settings section

* wip: add board settings for background and colors, move danger zone to own file, refactor code

* feat: add shade selector

* feat: add slider for opacity

* fix: issue with invalid hex values for color preview

* refactor: add shared mutation hook for saving partial board settings with invalidate query

* fix: add cleanup for not applied changes to logo and page title

* feat: add layout settings

* feat: add empty custom css section to board settings

* refactor: improve layout of board logo on mobile

* feat: add theme provider for board colors

* refactor: add auto contrast for better contrast of buttons with low primary shade

* feat: add background for boards

* feat: add opacity for boards

* feat: add rename board

* feat: add visibility and delete of board settings

* fix: issue that wrong data is updated with update board method

* refactor: improve danger zone button placement for mobile

* fix: board not revalidated when already in boards layout

* refactor: improve board color preview

* refactor: change save button color to teal, add placeholders for general board settings

* chore: update initial migration

* refactor: remove unnecessary div

* chore: address pull request feedback

* fix: ci issues

* fix: deepsource issues

* chore: address pull request feedback

* fix: formatting issue

* chore: address pull request feedback
2024-03-03 16:01:32 +01:00
ManuelandMeier Lukas 2a83df3485 feat: use password input (#163)
* feat: use password input

* chore: address pull request feedback

* fix: typo in function name

* fix: deepsource issues

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-02 17:46:03 +01:00
homarr-renovate[bot]andMeier Lukas 990be660c5 chore(deps): update dependency @types/react to ^18.2.61 (#165)
* chore(deps): update dependency @types/react to ^18.2.61

* fix: formatting issue

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-02 16:41:21 +01:00
homarr-renovate[bot]andMeier Lukas 258ad94bd2 chore(deps): update dependency @types/eslint to ^8.56.5 (#164)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-02 15:58:22 +01:00
homarr-renovate[bot]andMeier Lukas af203e19e7 fix(deps): update dependency jotai to ^2.7.0 (#162)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-02 15:56:01 +01:00
homarr-renovate[bot]andMeier Lukas 14cf39b996 chore(deps): update dependency @types/node to ^20.11.24 (#161)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-02 15:51:40 +01:00
homarr-renovate[bot]andMeier Lukas b407f2a9ac fix(deps): update nextjs monorepo to ^14.1.1 (#147)
* fix(deps): update nextjs monorepo to ^14.1.1

* fix: lint issue

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-03-02 15:37:40 +01:00
homarr-renovate[bot] baa541afa9 fix(deps): update dependency better-sqlite3 to ^9.4.3 (#118)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-03-02 15:28:02 +01:00
homarr-renovate[bot] fc8d9071aa fix(deps): update dependency jotai to ^2.6.5 (#106)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-27 21:54:13 +01:00
Manuel c6c8778397 feat: add password input field (#160) 2024-02-27 21:53:53 +01:00
homarr-renovate[bot] 212063a51e fix(deps): update tiptap monorepo to ^2.2.4 (#153)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-27 21:19:29 +01:00
homarr-renovate[bot] e74a41165d chore(deps): update dependency eslint to ^8.57.0 (#154)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-27 21:19:06 +01:00
homarr-renovate[bot] 47f44128f5 chore(deps): update dependency @types/node to ^20.11.20 (#151)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-27 21:18:35 +01:00
homarr-renovate[bot] f211ffbf78 fix(deps): update dependency mantine-react-table to v2.0.0-beta.0 (#149)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-27 20:53:06 +01:00
homarr-renovate[bot] 4cf505f0c5 fix(deps): update dependency drizzle-orm to ^0.29.4 (#150)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-27 20:43:40 +01:00
homarr-renovate[bot] c4ffa59ede chore(deps): update pnpm to v8.15.4 (#155)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-27 20:43:16 +01:00
homarr-renovate[bot] 3022e7f0af fix(deps): update tanstack-query monorepo to ^5.24.1 (#156)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-27 20:43:09 +01:00
homarr-renovate[bot] e9b02519a4 chore(deps): update dependency @types/react to ^18.2.60 (#158)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-27 20:42:23 +01:00
homarr-renovate[bot] 8e5379225a fix(deps): update typescript-eslint monorepo to ^7.1.0 (#159)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-27 20:42:06 +01:00
Meier Lukas b2cee8f693 fix: next auth host not trusted (#144) 2024-02-23 17:15:24 +01:00
homarr-renovate[bot] cfb3daef95 chore(deps): update dependency @types/react to ^18.2.58 (#152)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-23 17:05:43 +01:00
homarr-renovate[bot] f060f50caf chore(deps): update dependency @types/eslint to ^8.56.3 (#111)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-23 17:05:31 +01:00
homarr-renovate[bot] abc4a9afbc fix(deps): update dependency sass to ^1.71.1 (#148)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-21 21:24:43 +01:00
Manuel 60dec6e3be feat: add 404 page (#145) 2024-02-21 21:24:33 +01:00
homarr-renovate[bot]andMeier Lukas 5d68300600 fix(deps): update tanstack-query monorepo (#141)
* fix(deps): update tanstack-query monorepo

* fix: lockfile broken

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-20 21:43:21 +01:00
homarr-renovate[bot]andMeier Lukas 4d011364bf fix(deps): update dependency next to ^14.1.1-canary.63 (#146)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-20 21:33:40 +01:00
Meier Lukas 3d50f8165f feat: add winston logger (#143)
* feat: add winston logger

* refactor: improve usage of winston logger overide, remove start.js file and logger.js file used for override

* fix: formatting

* fix: formatting
2024-02-20 21:28:27 +01:00
Manuel b4749e7091 feat: add user management (#134) 2024-02-20 21:18:47 +01:00
homarr-renovate[bot]andMeier Lukas fde634d834 fix(deps): update dependency next to ^14.1.1-canary.62 (#140)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-20 20:15:25 +01:00
homarr-renovate[bot] 859eeed329 fix(deps): update typescript-eslint monorepo to ^7.0.2 (#122)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-20 20:09:20 +01:00
homarr-renovate[bot] 69b9247550 chore(deps): update dependency @types/react to ^18.2.57 (#139)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-20 20:08:59 +01:00
homarr-renovate[bot] 72feb06610 chore(deps): update vitest monorepo to ^1.3.1 (#142)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-20 20:08:35 +01:00
homarr-renovate[bot] 64194c51ed fix(deps): update dependency next-auth to v5.0.0-beta.13 (#108)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-20 20:08:11 +01:00
Manuel a4fc6f6444 refactor: client side navigation (#135) 2024-02-19 18:04:01 +01:00
homarr-renovate[bot] 6a02afbfe2 fix(deps): update dependency next to ^14.1.1-canary.61 (#138)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-19 06:31:42 +01:00
homarr-renovate[bot]andMeier Lukas e150577c7e fix(deps): update tanstack-query monorepo to ^5.21.7 (#133)
* fix(deps): update tanstack-query monorepo to ^5.21.7

* fix: formatting issue

* fix: formatting issue

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-18 22:13:19 +01:00
homarr-renovate[bot]andMeier Lukas 4565f38bef fix(deps): update dependency mantine-modal-manager to ^7.5.3 (#132)
* fix(deps): update dependency mantine-modal-manager to ^7.5.3

* fix: formatting issue

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-18 22:03:38 +01:00
Manuel 7d648600ee config. replace logo (#136) 2024-02-18 21:53:51 +01:00
Manuel e3e38e7f44 feat: add create board modal (#131) 2024-02-18 15:44:49 +01:00
Meier Lukas f1aa422614 feat: add support for multiple integration kind options (#127)
* feat: add support for multiple integration kind options

* fix: deepsource issue JS-0417 missing use callback
2024-02-18 14:24:07 +01:00
Meier Lukas 1e414af57c fix: issue with category removal and ready state (#129) 2024-02-18 09:10:16 +01:00
homarr-renovate[bot] 4f911c4f56 fix(deps): update dependency next to ^14.1.1-canary.59 (#121)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-18 08:50:26 +01:00
homarr-renovate[bot] dbf02c6264 fix(deps): update dependency @tanstack/react-query-devtools to ^5.21.5 (#130)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-18 08:48:40 +01:00
homarr-renovate[bot] a9a3d98248 fix(deps): update tanstack-query monorepo to ^5.21.4 (#128)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-17 22:07:12 +01:00
Manuel 3d3c1b50a3 Merge branch 'dev' of https://github.com/homarr-labs/homarr into dev 2024-02-17 16:22:14 +01:00
Manuel 547f75d19c fix: crash with no user in session 2024-02-17 16:22:05 +01:00
Meier Lukas d5025da789 Add search spotlight with registration hook (#82)
* wip: add spotlight

* feat: add spotlight with registration hook and group chips

* chore: address pull request feedback

* docs: add documentation for usage of spotlight actions

* fix: deepsource issue JS-0415

* feat: add support for dependencies of spotlight actions

* fix: lockfile broken

* feat: add hover effect for spotlight action

* docs: Add documentation about dependency array

* refactor: remove test spotlight actions, disallow all as group for actions

* fix: type issues

* chore: address pull request feedback
2024-02-17 14:11:23 +01:00
homarr-renovate[bot]andMeier Lukas 3577bd6ac3 chore(deps): update dependency @types/react to ^18.2.56 (#104)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-17 12:58:01 +01:00
homarr-renovate[bot]andMeier Lukas 71521c0768 fix(deps)!: update tanstack-query monorepo (#126)
* fix(deps): update tanstack-query monorepo to ^5.21.2

* fix(deps): update tanstack-query monorepo

* fix: type issue with transformer

* fix: issues with next-auth, updated to next canary

* chore: fix type issue in trpc route

* chore: fix formatting

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-17 12:52:25 +01:00
Manuel 3bdd117659 ci: remove automatic-release.yml 2024-02-16 23:05:30 +01:00
homarr-renovate[bot] 1d3fa16484 chore(deps): update dependency node to v20.11.1 (#92)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-16 22:57:08 +01:00
homarr-renovate[bot] 3e44644ef6 fix(deps): update tiptap monorepo to ^2.2.3 (#95)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-16 22:55:32 +01:00
homarr-renovate[bot] 9a60588a03 fix(deps): update dependency sass to ^1.71.0 (#96)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-16 22:53:43 +01:00
homarr-renovate[bot] ba0c3c72ed chore(deps): update vitest monorepo to ^1.3.0 (#98)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-16 22:53:09 +01:00
homarr-renovate[bot] 4ea1d14966 fix(deps): update dependency @tanstack/react-query-devtools to ^5.21.0 (#101)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-16 22:50:43 +01:00
homarr-renovate[bot] 35edfc74c4 fix(deps): update mantine monorepo to ^7.5.3 (#97)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-16 22:49:45 +01:00
homarr-renovate[bot] 5d69ff74f3 chore(deps): update pnpm to v8.15.3 (#94)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-16 22:43:16 +01:00
Manuel 5997afdd7b chore: merge update branch 2024-02-16 22:26:28 +01:00
Meier Lukas 144e1512f7 ci: update node version to 20 2024-02-16 20:26:48 +01:00
homarr-renovate[bot] f91338446d fix(deps): update typescript-eslint monorepo to v7 (#87)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-15 20:40:50 +01:00
homarr-renovate[bot]andMeier Lukas 6c5e4752ea fix(deps): update dependency @auth/core to ^0.27.0 (#89)
* fix(deps): update dependency @auth/core to ^0.27.0

* fix: lockfile broken

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-15 20:39:55 +01:00
homarr-renovate[bot]andMeier Lukas 11393525d9 fix(deps): update dependency @auth/drizzle-adapter to ^0.7.0 (#90)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-15 20:28:21 +01:00
homarr-renovate[bot] 336dc38b2d fix(deps): update dependency next-auth to v5.0.0-beta.11 (#91)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-15 20:23:57 +01:00
homarr-renovate[bot] b77e652822 fix(deps): update tanstack-query monorepo to ^5.20.5 (#86)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-15 20:22:00 +01:00
homarr-renovate[bot] 966425c119 chore(deps): update pnpm to v8.15.2 (#88)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-14 22:55:18 +01:00
homarr-renovate[bot] c5fb8075ce chore(deps): update turbo monorepo to ^1.12.4 (#93)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-14 22:54:32 +01:00
Meier Lukas 25693b10b1 Fix issue where gridstack items can have a width of zero (#85) 2024-02-11 16:18:15 +01:00
homarr-renovate[bot] 34f06e817d fix(deps): update dependency better-sqlite3 to ^9.4.1 (#83)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-11 09:37:37 +01:00
Manuel 08fa338abb feat: add user avatar menu (#80) 2024-02-10 23:14:02 +01:00
Meier Lukas 16442bc379 feat: add support for should hide option i widget edit modal (#78) 2024-02-10 19:11:40 +01:00
Meier Lukas f070a0cb0a test: add initial unit tests (#56)
* chore: add initial db migration

* test: add unit tests for packages auth, common, widgets

* fix: deep source issues

* fix: format issues

* wip: add unit tests for api routers

* fix: deep source issues

* test:  add missing unit tests for integration router

* wip: board tests

* test: add unit tests for board router

* fix: remove unnecessary null assertions

* fix: deepsource issues

* fix: formatting

* fix: pnpm lock

* fix: lint and typecheck issues

* chore: address pull request feedback

* fix: non-null assertions

* fix: lockfile broken
2024-02-10 19:00:08 +01:00
Manuel 13aae82790 docs: add pull request template 2024-02-10 18:22:23 +01:00
Manuel 34875dfd15 feat: add winston logger (#77) 2024-02-10 18:07:24 +01:00
homarr-renovate[bot] 00f4a288a4 fix(deps): update tanstack-query monorepo to ^5.20.1 (#75)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-10 13:45:18 +01:00
homarr-renovate[bot] 60d5ebbc11 fix(deps): update dependency next-international to ^1.2.4 (#76)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-10 11:27:31 +01:00
Manuel 3e732fc613 config: add permissions to automatic release workflow 2024-02-09 23:05:50 +01:00
homarr-renovate[bot] f704bad36e fix(deps): update dependency mantine-modal-manager to ^7.5.2 (#72)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-09 22:20:41 +01:00
Manuel 5ef79edc1a feat: add boards management page (#55) 2024-02-09 22:20:28 +01:00
homarr-renovate[bot] 81e61b4d6b fix(deps): update mantine monorepo to ^7.5.2 (#71)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-09 22:05:39 +01:00
homarr-renovate[bot] 7f4ad88b0f chore(deps): update dependency @types/node to ^20.11.17 (#70)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-09 21:23:24 +01:00
deepsource-autofix[bot]andMeier Lukas a4f6a7c16a refactor: replace short hand type conversions with function calls (#65)
* refactor: replace short hand type conversions with function calls

Prefer using explicit casts by calling `Number`, `Boolean`, or `String` over using operators like `+`, `!!` or `"" +`. This is considered best practice as it improves readability.

* fix: formatting

---------

Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-08 20:06:57 +01:00
homarr-renovate[bot]andMeier Lukas 11d1568714 fix(deps): update dependency @t3-oss/env-nextjs to ^0.9.2 (#69)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-08 20:05:09 +01:00
homarr-renovate[bot]andMeier Lukas 15f08011a6 fix(deps): update dependency prettier to ^3.2.5 (#53)
* fix(deps): update dependency prettier to ^3.2.5

* fix: formatting

* fix: formatting

* fix: formatting

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-08 19:58:56 +01:00
Meier Lukas 975f9123dd feat: add widget server loader (#16)
* wip: Add gridstack board

* wip: Centralize board pages, Add board settings page

* fix: remove cyclic dependency and rename widget-sort to kind

* improve: Add header actions as parallel route

* feat: add item select modal, add category edit modal,

* feat: add edit item modal

* feat: add remove item modal

* wip: add category actions

* feat: add saving of board, wip: add app widget

* Merge branch 'main' into add-board

* chore: update turbo dependencies

* chore: update mantine dependencies

* chore: fix typescript errors, lint and format

* feat: add confirm modal to category removal, move items of removed category to above wrapper

* feat: remove app widget to continue in another branch

* feat: add loading spinner until board is initialized

* fix: issue with cellheight of gridstack items

* feat: add translations for board

* fix: issue with translation for settings page

* feat: add widget server loader

* fix: typing issue

* chore: address pull request feedback

* fix: formatting
2024-02-08 07:00:00 +01:00
homarr-renovate[bot] cc52c2ba78 fix(deps): update dependency next-auth to v5.0.0-beta.9 (#68)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-08 06:48:19 +01:00
Tagaishi 55ec60041a fix: Remove version override (#67)
* 🐛 remove version override

* fix: configuration userid error
2024-02-07 21:24:50 +01:00
deepsource-autofix[bot] 91812531b4 refactor: remove true from boolean attribute (#66)
When using a boolean attribute in JSX, you can set the attribute value to true or omit the value. This helps to keep consistency in code.

Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
2024-02-07 21:02:22 +01:00
homarr-renovate[bot] 6797cca259 fix(deps): update dependency @auth/drizzle-adapter to ^0.6.3 (#52)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-07 20:54:51 +01:00
homarr-renovate[bot] 5cbd02b142 fix(deps): update dependency @auth/core to ^0.26.3 (#54)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-07 20:45:29 +01:00
homarr-renovate[bot] 941307b954 chore(deps): update turbo monorepo to ^1.12.3 (#62)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-07 20:45:17 +01:00
homarr-renovate[bot] 593bc9ca1c fix(deps): update tiptap monorepo to ^2.2.2 (#63)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-07 20:45:09 +01:00
homarr-renovate[bot] 8994ac2501 chore(deps): update dependency @types/react-dom to ^18.2.19 (#64)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-07 20:44:59 +01:00
homarr-renovate[bot] 544a6d9926 fix(deps): update dependency next-auth to v5.0.0-beta.8 (#60)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-06 21:52:11 +01:00
homarr-renovate[bot] ddc6f1fa16 chore(deps): update dependency @types/react to ^18.2.55 (#61)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-06 21:51:51 +01:00
homarr-renovate[bot] e683d61568 fix(deps): update typescript-eslint monorepo to ^6.21.0 (#58)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-05 20:32:29 +01:00
homarr-renovate[bot] 5d3ca5b190 chore(deps): update dependency @types/react to ^18.2.54 (#59)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-05 20:32:16 +01:00
homarr-renovate[bot] 19588d77c8 chore(deps): update dependency @types/react to ^18.2.53 (#57)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-05 07:12:16 +01:00
Manuel c89d13557d config: add telemetry opt out to env 2024-02-04 11:26:05 +01:00
Meier Lukas ae12f0c9df test: add vitest for unit tests (#50)
* feat: add vitest for unit tests

* chore: address pull request feedback

* test: add unit test code quality workflow
2024-02-04 09:47:23 +01:00
ca04bf692d chore(deps): update dependency node to v20 (#42)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-04 09:21:53 +01:00
homarr-renovate[bot] 7cd56217eb chore(deps): update anothrnick/github-tag-action action to v1.67.0 (#43)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-04 09:21:05 +01:00
4815230f8b fix(deps): update typescript-eslint monorepo to ^6.20.0 (#41)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-04 09:15:59 +01:00
homarr-renovate[bot] 59f024184b chore(deps): update actions/setup-node action to v4 (#44)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-04 09:15:51 +01:00
homarr-renovate[bot] 2b5ebda727 chore(deps): update docker/build-push-action action to v5 (#45)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-04 09:14:57 +01:00
homarr-renovate[bot] a1e15c65d2 chore(deps): update docker/metadata-action action to v5 (#46)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-04 09:14:04 +01:00
1e01312fcd fix(deps): update tiptap monorepo to ^2.2.1 (#40)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-04 09:12:40 +01:00
homarr-renovate[bot] 58803bcece chore(deps): update docker/setup-buildx-action action to v3 (#47)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-04 09:11:28 +01:00
717e919450 fix(deps): update tanstack-query monorepo to ^5.18.1 (#39)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-04 09:08:36 +01:00
homarr-renovate[bot] 33c918603c chore(deps): update docker/setup-qemu-action action to v3 (#48)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-04 09:07:27 +01:00
dadfc0c5d7 fix(deps): update nextjs monorepo to ^14.1.0 (#38)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-04 09:05:59 +01:00
homarr-renovate[bot] ab7c884fbf chore(deps): update peter-evans/create-pull-request action to v6 (#49)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-02-04 09:05:27 +01:00
9aaec5495d fix(deps): update dependency prettier to ^3.2.4 (#37)
* fix(deps): update dependency prettier to ^3.2.4

* fix: formatting issues

---------

Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-04 09:03:22 +01:00
ManuelandRenovate Bot d73e7a3c3f fix(deps): update dependency postcss-preset-mantine to ^1.13.0 (#36)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2024-02-04 08:53:33 +01:00
ManuelandRenovate Bot b8fe04144f fix(deps): update dependency next-international to ^1.2.3 (#35)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2024-02-04 08:53:16 +01:00
ManuelandRenovate Bot 837e3900fa fix(deps): update dependency eslint-config-turbo to ^1.12.2 (#34)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2024-02-04 08:47:20 +01:00
ManuelandRenovate Bot 00db901833 fix(deps): update dependency better-sqlite3 to ^9.4.0 (#33)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2024-02-04 08:46:39 +01:00
ManuelandRenovate Bot 6a892e02c8 fix(deps): update dependency @tabler/icons-react to ^2.47.0 (#32)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2024-02-04 08:46:26 +01:00
f8a3aae116 fix(deps): update dependency @t3-oss/env-nextjs to ^0.8.0 (#31)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-04 08:46:06 +01:00
191d773aaf fix(deps): update dependency @auth/drizzle-adapter to ^0.5.0 (#30)
* fix(deps): update dependency @auth/drizzle-adapter to ^0.5.0

* fix: remove duplication of auth dependencies

* fix: formatting issue

---------

Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-04 08:41:56 +01:00
ManuelandRenovate Bot 8bcbc45943 fix(deps): update dependency @auth/core to ^0.25.0 (#29)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2024-02-04 08:31:02 +01:00
ManuelandRenovate Bot d9d169c0ce chore(deps): update pnpm to v8.15.1 (#28)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2024-02-04 08:29:40 +01:00
ManuelandRenovate Bot a7d5c36aa7 fix(deps): update dependency jotai to ^2.6.4 (#24)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2024-02-04 08:29:21 +01:00
ManuelandRenovate Bot b3e2032b54 chore(deps): update dependency @types/cookies to v0.9.0 (#25)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2024-02-04 08:28:18 +01:00
ManuelandRenovate Bot 7da312134c chore(deps): update dependency @types/eslint to ^8.56.2 (#26)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2024-02-04 08:28:05 +01:00
ManuelandRenovate Bot 8a4fc849a2 chore(deps): update dependency node to v18.19.0 (#27)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2024-02-04 08:27:51 +01:00
ManuelandRenovate Bot f3bb990c4d fix(deps): update dependency eslint-plugin-import to ^2.29.1 (#23)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2024-02-03 23:13:50 +01:00
ManuelandRenovate Bot 8a9656a01e chore(deps): update dependency drizzle-kit to ^0.20.14 (#22)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2024-02-03 23:13:43 +01:00
deepsource-io[bot] 2e555fbb35 ci: update .deepsource.toml 2024-02-03 22:06:11 +00:00
deepsource-io[bot] e6a5c83d9c ci: add .deepsource.toml 2024-02-03 22:01:50 +00:00
Meier Lukas 2943819f74 chore: update gridstack to homarr/gridstack package (#20)
* chore: migrate gridstack to homarr gridstack

* fix: formatting issue
2024-02-03 22:59:34 +01:00
d3c11f49dc chore(deps): update dependency @types/better-sqlite3 to v7.6.9 (#17)
* chore(deps): update dependency @types/better-sqlite3 to v7.6.9

* fix: lint issue

---------

Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-02-03 22:59:15 +01:00
Manuel 74e1ad2f00 config: push webhook on image build 2024-02-03 22:57:49 +01:00
Manuel a3156bcb18 config: disable turborepo telemetry 2024-02-03 22:50:01 +01:00
Manuel ea7cb27cfa config: add pnpm to deployment workflow 2024-02-03 22:46:21 +01:00
Manuel 154fc050cd config: add docker image workflow 2024-02-03 22:35:30 +01:00
Meier Lukas 9d520874f4 feat: add board (#15)
* wip: Add gridstack board
* wip: Centralize board pages, Add board settings page
* fix: remove cyclic dependency and rename widget-sort to kind
* improve: Add header actions as parallel route
* feat: add item select modal, add category edit modal,
* feat: add edit item modal
* feat: add remove item modal
* wip: add category actions
* feat: add saving of board, wip: add app widget
* Merge branch 'main' into add-board
* chore: update turbo dependencies
* chore: update mantine dependencies
* chore: fix typescript errors, lint and format
* feat: add confirm modal to category removal, move items of removed category to above wrapper
* feat: remove app widget to continue in another branch
* feat: add loading spinner until board is initialized
* fix: issue with cellheight of gridstack items
* feat: add translations for board
* fix: issue with translation for settings page
* chore: address pull request feedback
2024-02-03 22:26:12 +01:00
Manuel cfd1c14034 Merge remote-tracking branch 'origin/dev' into dev 2024-02-03 22:10:45 +01:00
Manuel f0fa0eb321 config: disable dependency dashboard 2024-02-03 22:10:21 +01:00
ManuelandRenovate Bot e9c6583d21 chore(deps): update dependency @types/react to ^18.2.52 (#18)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2024-02-03 22:09:11 +01:00
Manuel 3e2bc9df0e config: remove renovate workflow 2024-02-03 22:06:40 +01:00
Manuel c736f17f14 config: disable auto discover 2024-02-03 21:59:49 +01:00
Manuel 39b4e761de config: migrate renovate config 2024-02-03 21:56:58 +01:00
Manuel 89311c44a0 config: remove docker properties renovate 2024-02-03 21:53:15 +01:00
Manuel b678401020 config: add base branch to renovate 2024-02-03 21:50:55 +01:00
Manuel f12192e6f2 config: add renovate CI 2024-02-03 21:50:08 +01:00
Manuel 3724071fcb docs: update funding 2024-02-03 21:39:25 +01:00
Manuel b671f68b13 config: add deployment workflow (#8) 2024-02-03 21:30:45 +01:00
Meier Lukas 1740450648 feat: Add widget integration option (#14)
* wip: add widget integrations

* feat: Add integration option to widgets

* feat: Add translation for widget integration select

* fix: formatting issue

* chore: address pull request feedback
2024-02-03 10:24:39 +01:00
Meier Lukas 3a0f280984 chore: update dependencies (#13)
* chore: update dependencies

* chore: update nextauth dependency to beta 5

* chore: fix formatting
2024-01-23 21:49:47 +01:00
Meier Lukas 81d35656ba refactor: move integration pages to main directory for main layout 2024-01-03 06:04:42 +01:00
Meier Lukas 367beb6759 feat: add crud for integrations (#11)
* wip: add crud for services and integrations

* feat: remove services

* feat: move integration definitions to homarr/definitions, add temporary test connection solution without actual request

* feat: add integration count badge

* feat: add translation for integrations

* feat: add notifications and translate them

* feat: add notice to integration forms about test connection

* chore: fix ci check issues

* feat: add confirm modals for integration deletion and secret card cancellation, change ordering for list page, add name property to integrations

* refactor: move revalidate path action

* chore: fix ci check issues

* chore: install missing dependencies

* chore: fix ci check issues

* chore: address pull request feedback
2024-01-02 17:12:26 +01:00
Manuel 2809e01b03 feat: add management pages (#12) 2024-01-02 16:49:02 +01:00
Meier Lukas 782897527f feat: add widget preview pages (#9)
* feat: add widget definition system

* fix: wrong typecheck command in turbo generator

* chore: fix formatting

* feat: add widget preview page

* chore: fix formatting and type errors

* chore: fix from widget edit modal and remove some never casts

* chore: address pull request feedback
2024-01-02 15:36:59 +01:00
Meier Lukas fa19966fcc chore: restructure packages of the project (#7)
* chore: restructure validation package

* chore: move zod only to validation package

* chore: rename packages from alparr to homarr

* chore: move mantine core, dates and icons library to ui package, move most other mantine packages to seperate packages for further customization and centralization

* chore: fix formatting

* fix: wrong typecheck command in turbo generator

* chore: fix formatting

* chore: address pull request feedback

* chore: fix ci check issues
2024-01-02 14:18:37 +01:00
Meier Lukas a082f70470 feat: add next-international translations (#2)
* feat: add next-international translations
* chore: fix formatting
* chore: address pull request feedback
2023-12-19 23:09:41 +01:00
Meier Lukas 3cedb7fba5 feat: add credentials authentication (#1) 2023-12-10 17:12:20 +01:00
Meier Lukas 41e54d940b refactor: migrate to sqlite as database 2023-12-09 00:32:45 +01:00
Meier Lukas f6094c6ae2 chore: add ui package and custom theme colors 2023-12-08 23:47:55 +01:00
Meier Lukas 9ea96899b1 chore: rename local packages from acme to alparr 2023-12-08 23:13:46 +01:00
Meier Lukas d115779c85 chore: migrate from tailwind to mantine 2023-12-08 23:11:37 +01:00
Meier Lukas a2cedf73c0 Initial commit 2023-12-08 22:35:15 +01:00
449 changed files with 40942 additions and 0 deletions

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
version = 1
[[analyzers]]
name = "javascript"
[analyzers.meta]
plugins = ["react"]
environment = ["nodejs"]
[[transformers]]
name = "prettier"
+8
View File
@@ -0,0 +1,8 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git
dev
+33
View File
@@ -0,0 +1,33 @@
# Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo.
# Keep this file up-to-date when you add new variables to \`.env\`.
# This file will be committed to version control, so make sure not to have any secrets in it.
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
# This is how you can use the sqlite driver:
DB_DRIVER='better-sqlite3'
DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
# Those are the two ways to use the mysql2 driver:
# 1. Using the URL format:
# DB_DRIVER='mysql2'
# DB_URL='mysql://user:password@host:port/database'
# 2. Using the connection options format:
# DB_DRIVER='mysql2'
# DB_HOST='localhost'
# DB_PORT='3306'
# DB_USER='username'
# DB_PASSWORD='password'
# DB_NAME='name-of-database'
# @see https://next-auth.js.org/configuration/options#nextauth_url
AUTH_URL='http://localhost:3000'
# You can generate the secret via 'openssl rand -base64 32' on Unix
# @see https://next-auth.js.org/configuration/options#secret
AUTH_SECRET='supersecret'
TURBO_TELEMETRY_DISABLED=1
# Configure logging to use winston logger
NODE_OPTIONS='-r @homarr/log/override'
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
open_collective: homarr
+37
View File
@@ -0,0 +1,37 @@
name: 🐞 Bug Report
description: Create a bug report to help us improve
title: "bug: "
labels: ["🐞❔ unconfirmed bug"]
body:
- type: textarea
attributes:
label: Provide environment information
description: |
Run this command in your project root and paste the results in a code block:
```bash
npx envinfo --system --binaries
```
validations:
required: true
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of the bug, as well as what you expected to happen when encountering it.
validations:
required: true
- type: input
attributes:
label: Link to reproduction
description: Please provide a link to a reproduction of the bug. Issues without a reproduction repo may be ignored.
validations:
required: true
- type: textarea
attributes:
label: To reproduce
description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc.
validations:
required: true
- type: textarea
attributes:
label: Additional information
description: Add any other information related to the bug here, screenshots if applicable.
@@ -0,0 +1,28 @@
# This template is heavily inspired by the Next.js's template:
# See here: https://github.com/vercel/next.js/blob/canary/.github/ISSUE_TEMPLATE/3.feature_request.yml
name: 🛠 Feature Request
description: Create a feature request for the core packages
title: "feat: "
labels: ["✨ enhancement"]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to file a feature request. Please fill out this form as completely as possible.
- type: textarea
attributes:
label: Describe the feature you'd like to request
description: Please describe the feature as clear and concise as possible. Remember to add context as to why you believe this feature is needed.
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like to see
description: Please describe the solution you would like to see. Adding example usage is a good way to provide context.
validations:
required: true
- type: textarea
attributes:
label: Additional information
description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here.
+13
View File
@@ -0,0 +1,13 @@
<br/>
<div align="center">
<img src="https://homarr.dev/img/logo.png" height="80" alt="" />
<h3>Homarr</h3>
</div>
**Thank you for your contribution. Please ensure that your pull request meets the following pull request:**
- [ ] Builds without warnings or errors (``pnpm buid``, autofix with ``pnpm format:fix``)
- [ ] Pull request targets ``dev`` branch
- [ ] 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)
+15
View File
@@ -0,0 +1,15 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"packageRules": [
{
"matchPackagePatterns": ["^@homarr/"],
"enabled": false
}
],
"updateInternalDeps": true,
"rangeStrategy": "bump",
"automerge": false,
"baseBranches": ["dev"],
"dependencyDashboard": false
}
+31
View File
@@ -0,0 +1,31 @@
name: Build apps and migration script
on:
pull_request:
branches: ["*"]
push:
branches: ["main"]
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
env:
FORCE_COLOR: 3
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup
uses: ./tooling/github/setup
- name: Copy env
shell: bash
run: cp .env.example .env
- name: Build
run: pnpm build
+68
View File
@@ -0,0 +1,68 @@
name: Code quality analysis
on:
pull_request:
branches: ["*"]
push:
branches: ["main"]
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
# You can leverage Vercel Remote Caching with Turbo to speed up your builds
# @link https://turborepo.org/docs/core-concepts/remote-caching#remote-caching-on-vercel-builds
env:
FORCE_COLOR: 3
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup
uses: ./tooling/github/setup
- name: Copy env
shell: bash
run: cp .env.example .env
- name: Lint
run: pnpm lint && pnpm lint:ws
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup
uses: ./tooling/github/setup
- name: Format
run: pnpm format
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup
uses: ./tooling/github/setup
- name: Typecheck
run: turbo typecheck
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup
uses: ./tooling/github/setup
- name: Test
run: pnpm test
+89
View File
@@ -0,0 +1,89 @@
name: Docker image
on:
pull_request:
types:
- closed
branches:
- main
workflow_dispatch: {}
permissions:
contents: write
packages: write
env:
TURBO_TELEMETRY_DISABLED: 1
concurrency: production
jobs:
deploy:
name: Deploy docker image
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20]
steps:
- name: Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: "Deployment of an image has been triggered"
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Build artifacts
run: pnpm build
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Bump version and push tag
id: githubTagAction
uses: anothrNick/github-tag-action@1.67.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WITH_V: false
DRY_RUN: true
- name: Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: "Image has been tagged as ${{ steps.githubTagAction.outputs.new_tag }}"
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
type=raw,value=${{ steps.githubTagAction.outputs.new_tag }}
- name: Build and push
id: buildPushAction
uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm64,linux/riscv64,linux/arm/v7,linux/arm/v6
context: .
push: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
network: host
- name: Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: "Image built with ID ${{ steps.buildPushAction.outputs.imageid }}"
+54
View File
@@ -0,0 +1,54 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnp
.pnp.js
.idea/
# testing
coverage
# next.js
.next/
out/
next-env.d.ts
# nest.js
apps/nestjs/dist
# nitro
.nitro/
.output/
# production
build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
# turbo
.turbo
# database
db.sqlite
# logs
*.log
+5
View File
@@ -0,0 +1,5 @@
# Expo doesn't play nice with pnpm by default.
# The symbolic links of pnpm break the rules of Expo monorepos.
# @link https://docs.expo.dev/guides/monorepos/#common-issues
node-linker=hoisted
strict-peer-dependencies=false
+1
View File
@@ -0,0 +1 @@
20.12.2
+7
View File
@@ -0,0 +1,7 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"yoavbls.pretty-ts-errors"
]
}
+87
View File
@@ -0,0 +1,87 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "dev",
"type": "node-terminal",
"request": "launch",
"command": "pnpm dev",
"cwd": "${workspaceFolder}",
"skipFiles": ["<node_internals>/**"]
},
{
"name": "docker dev",
"type": "node-terminal",
"request": "launch",
"command": "pnpm docker:dev",
"cwd": "${workspaceFolder}",
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "lint fix",
"type": "node-terminal",
"request": "launch",
"command": "pnpm lint:fix",
"cwd": "${workspaceFolder}",
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "format fix",
"type": "node-terminal",
"request": "launch",
"command": "pnpm format:fix",
"cwd": "${workspaceFolder}",
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "db push",
"type": "node-terminal",
"request": "launch",
"command": "pnpm db:push",
"cwd": "${workspaceFolder}",
"skipFiles": ["<node_internals>/**"]
},
{
"name": "db studio",
"type": "node-terminal",
"request": "launch",
"command": "pnpm db:studio",
"cwd": "${workspaceFolder}",
"skipFiles": ["<node_internals>/**"]
},
{
"name": "db migration run",
"type": "node-terminal",
"request": "launch",
"command": "pnpm db:migration:run",
"cwd": "${workspaceFolder}",
"skipFiles": ["<node_internals>/**"]
},
{
"name": "test",
"type": "node-terminal",
"request": "launch",
"command": "pnpm test",
"cwd": "${workspaceFolder}",
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "test ui",
"type": "node-terminal",
"request": "launch",
"command": "pnpm test:ui",
"cwd": "${workspaceFolder}",
"skipFiles": [
"<node_internals>/**"
]
}
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"eslint.workingDirectories": [
{
"mode": "auto"
}
],
"typescript.tsdk": "node_modules\\typescript\\lib",
"js/ts.implicitProjectConfig.experimentalDecorators": true,
"cSpell.words": [
"superjson",
"homarr",
"trpc"
]
}
+1
View File
@@ -0,0 +1 @@
* @homarr-labs/maintainers
+77
View File
@@ -0,0 +1,77 @@
FROM node:20.12.2-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
RUN apk update
# Set working directory
WORKDIR /app
COPY . .
RUN npm i -g turbo
RUN turbo prune @homarr/nextjs --docker --out-dir ./next-out
RUN turbo prune @homarr/tasks --docker --out-dir ./tasks-out
RUN turbo prune @homarr/websocket --docker --out-dir ./websocket-out
RUN turbo prune @homarr/db --docker --out-dir ./migration-out
# Add lockfile and package.json's of isolated subworkspace
FROM base AS installer
RUN apk add --no-cache libc6-compat curl bash
RUN apk update
WORKDIR /app
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/tasks-out/json/ .
COPY --from=builder /app/tasks-out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN corepack enable pnpm && pnpm install
COPY --from=builder /app/websocket-out/json/ .
COPY --from=builder /app/websocket-out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN corepack enable pnpm && pnpm install
COPY --from=builder /app/migration-out/json/ .
COPY --from=builder /app/migration-out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN corepack enable pnpm && pnpm install
COPY --from=builder /app/next-out/json/ .
COPY --from=builder /app/next-out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN corepack enable pnpm && pnpm install sharp -w
# Build the project
COPY --from=builder /app/tasks-out/full/ .
COPY --from=builder /app/websocket-out/full/ .
COPY --from=builder /app/next-out/full/ .
COPY --from=builder /app/migration-out/full/ .
ARG SKIP_ENV_VALIDATION=true
RUN corepack enable pnpm && pnpm turbo run build
FROM base AS runner
WORKDIR /app
RUN apk add --no-cache redis
# Don't run production as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
COPY --from=installer /app/apps/nextjs/next.config.mjs .
COPY --from=installer /app/apps/nextjs/package.json .
COPY --from=installer --chown=nextjs:nodejs /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
COPY --from=installer --chown=nextjs:nodejs /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
COPY --from=installer --chown=nextjs:nodejs /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
COPY --from=installer --chown=nextjs:nodejs /app/packages/db/migrations ./db/migrations
COPY --from=installer --chown=nextjs:nodejs /app/packages/db/migrate.cjs ./db/migrate.cjs
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=installer --chown=nextjs:nodejs /app/apps/nextjs/.next/standalone ./
COPY --from=installer --chown=nextjs:nodejs /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
COPY --from=installer --chown=nextjs:nodejs /app/apps/nextjs/public ./apps/nextjs/public
COPY --chown=nextjs:nodejs scripts/run.sh ./run.sh
ENV DB_URL='/app/db/db.sqlite'
CMD ["sh", "run.sh"]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Julius Marminge
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+5
View File
@@ -0,0 +1,5 @@
# THIS PROJECT IS STILL UNSTABLE AND WE DO NOT PROVIDE ANY SUPPORT FOR ISSUES THAT OCCURE.
## PLEASE DO NOT OPEN ANY ISSUES OR DISCUSSIONS
### EVERYTHING IS SUBJECT TO CHANGE
Please use [this](https://github.com/ajnart/homarr) version of Homarr when you want to use it
+28
View File
@@ -0,0 +1,28 @@
# Create T3 App
This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`.
## What's next? How do I make an app with this?
We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary.
If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help.
- [Next.js](https://nextjs.org)
- [NextAuth.js](https://next-auth.js.org)
- [Drizzle](https://orm.drizzle.team)
- [Tailwind CSS](https://tailwindcss.com)
- [tRPC](https://trpc.io)
## Learn More
To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources:
- [Documentation](https://create.t3.gg/)
- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials
You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome!
## How do I deploy this?
Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel) and [Docker](https://create.t3.gg/en/deployment/docker) for more information.
+31
View File
@@ -0,0 +1,31 @@
// Importing env files here to validate on build
import "./src/env.mjs";
import "@homarr/auth/env.mjs";
/** @type {import("next").NextConfig} */
const config = {
output: "standalone",
reactStrictMode: true,
/** We already do linting and typechecking as separate tasks in CI */
eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true },
experimental: {
optimizePackageImports: [
"@mantine/core",
"@mantine/hooks",
"@tabler/icons-react",
],
},
transpilePackages: [
"@homarr/ui",
"@homarr/notifications",
"@homarr/modals",
"@homarr/spotlight",
"@homarr/widgets",
],
images: {
domains: ["cdn.jsdelivr.net"],
},
};
export default config;
+83
View File
@@ -0,0 +1,83 @@
{
"name": "@homarr/nextjs",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "pnpm with-env next build",
"clean": "git clean -xdf .next .turbo node_modules",
"dev": "pnpm with-env next dev",
"lint": "dotenv -v SKIP_ENV_VALIDATION=1 next lint",
"format": "prettier --check . --ignore-path ../../.gitignore",
"start": "pnpm with-env next start",
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --"
},
"dependencies": {
"@homarr/api": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/form": "workspace:^0.1.0",
"@homarr/gridstack": "^1.0.0",
"@homarr/log": "workspace:^",
"@homarr/modals": "workspace:^0.1.0",
"@homarr/notifications": "workspace:^0.1.0",
"@homarr/spotlight": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "^7.9.0",
"@mantine/hooks": "^7.9.0",
"@mantine/modals": "^7.9.0",
"@mantine/tiptap": "^7.9.0",
"@t3-oss/env-nextjs": "^0.10.1",
"@tanstack/react-query": "^5.34.1",
"@tanstack/react-query-devtools": "^5.34.1",
"@tanstack/react-query-next-experimental": "5.34.1",
"@trpc/client": "11.0.0-rc.364",
"@trpc/next": "next",
"@trpc/react-query": "next",
"@trpc/server": "next",
"@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^5.5.0",
"chroma-js": "^2.4.2",
"dayjs": "^1.11.11",
"dotenv": "^16.4.5",
"glob": "^10.3.12",
"jotai": "^2.8.0",
"next": "^14.2.3",
"postcss-preset-mantine": "^1.15.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"sass": "^1.76.0",
"superjson": "2.2.1",
"use-deep-compare-effect": "^1.8.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "2.4.4",
"@types/node": "^20.12.8",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"concurrently": "^8.2.2",
"eslint": "^8.57.0",
"prettier": "^3.2.5",
"tsx": "4.9.0",
"typescript": "^5.4.5"
},
"eslintConfig": {
"root": true,
"extends": [
"@homarr/eslint-config/base",
"@homarr/eslint-config/nextjs",
"@homarr/eslint-config/react"
]
},
"prettier": "@homarr/prettier-config"
}
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
plugins: {
"postcss-preset-mantine": {},
"postcss-simple-vars": {
variables: {
"mantine-breakpoint-xs": "36em",
"mantine-breakpoint-sm": "48em",
"mantine-breakpoint-md": "62em",
"mantine-breakpoint-lg": "75em",
"mantine-breakpoint-xl": "88em",
},
},
},
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -0,0 +1,14 @@
import type { PropsWithChildren } from "react";
import { AppShellMain } from "@mantine/core";
import { MainHeader } from "~/components/layout/header";
import { ClientShell } from "~/components/layout/shell";
export default function MainLayout({ children }: PropsWithChildren) {
return (
<ClientShell hasNavigation={false}>
<MainHeader />
<AppShellMain>{children}</AppShellMain>
</ClientShell>
);
}
@@ -0,0 +1,9 @@
import { Stack, Title } from "@mantine/core";
export default function HomePage() {
return (
<Stack>
<Title>Home</Title>
</Stack>
);
}
@@ -0,0 +1,8 @@
"use client";
import type { PropsWithChildren } from "react";
import { Provider } from "jotai";
export const JotaiProvider = ({ children }: PropsWithChildren) => {
return <Provider>{children}</Provider>;
};
@@ -0,0 +1,15 @@
import type { PropsWithChildren } from "react";
import { defaultLocale } from "@homarr/translation";
import { I18nProviderClient } from "@homarr/translation/client";
export const NextInternationalProvider = ({
children,
locale,
}: PropsWithChildren<{ locale: string }>) => {
return (
<I18nProviderClient locale={locale} fallback={defaultLocale}>
{children}
</I18nProviderClient>
);
};
@@ -0,0 +1,17 @@
"use client";
import type { PropsWithChildren } from "react";
import type { Session } from "@homarr/auth";
import { SessionProvider } from "@homarr/auth/client";
interface AuthProviderProps {
session: Session | null;
}
export const AuthProvider = ({
children,
session,
}: PropsWithChildren<AuthProviderProps>) => {
return <SessionProvider session={session}>{children}</SessionProvider>;
};
@@ -0,0 +1,85 @@
"use client";
import type { PropsWithChildren } from "react";
import { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
import {
createWSClient,
loggerLink,
unstable_httpBatchStreamLink,
wsLink,
} from "@trpc/client";
import superjson from "superjson";
import type { AppRouter } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
const wsClient = createWSClient({
url: "ws://localhost:3001",
});
export function TRPCReactProvider(props: PropsWithChildren) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 1000,
},
},
}),
);
const [trpcClient] = useState(() => {
return clientApi.createClient({
links: [
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === "development" ||
(opts.direction === "down" && opts.result instanceof Error),
}),
(args) => {
return ({ op, next }) => {
console.log("op", op.type, op.input, op.path, op.id);
if (op.type === "subscription") {
const link = wsLink<AppRouter>({
client: wsClient,
transformer: superjson,
});
return link(args)({ op, next });
}
return unstable_httpBatchStreamLink({
transformer: superjson,
url: `${getBaseUrl()}/api/trpc`,
headers() {
const headers = new Headers();
headers.set("x-trpc-source", "nextjs-react");
return headers;
},
})(args)({ op, next });
};
},
],
});
});
return (
<clientApi.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<ReactQueryStreamedHydration transformer={superjson}>
{props.children}
</ReactQueryStreamedHydration>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</clientApi.Provider>
);
}
function getBaseUrl() {
if (typeof window !== "undefined") return window.location.origin;
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return `http://localhost:${process.env.PORT ?? 3000}`;
}
@@ -0,0 +1,82 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import {
Alert,
Button,
PasswordInput,
rem,
Stack,
TextInput,
} from "@mantine/core";
import { IconAlertTriangle } from "@tabler/icons-react";
import { signIn } from "@homarr/auth/client";
import { useForm, zodResolver } from "@homarr/form";
import { useScopedI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
export const LoginForm = () => {
const t = useScopedI18n("user");
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string>();
const form = useForm<FormType>({
validate: zodResolver(validation.user.signIn),
initialValues: {
name: "",
password: "",
},
});
const handleSubmit = async (values: FormType) => {
setIsLoading(true);
setError(undefined);
await signIn("credentials", {
...values,
redirect: false,
callbackUrl: "/",
})
.then((response) => {
if (!response?.ok || response.error) {
throw response?.error;
}
void router.push("/");
})
.catch((error: Error | string) => {
setIsLoading(false);
setError(error.toString());
});
};
return (
<Stack gap="xl">
<form onSubmit={form.onSubmit((values) => void handleSubmit(values))}>
<Stack gap="lg">
<TextInput
label={t("field.username.label")}
{...form.getInputProps("name")}
/>
<PasswordInput
label={t("field.password.label")}
{...form.getInputProps("password")}
/>
<Button type="submit" fullWidth loading={isLoading}>
{t("action.login")}
</Button>
</Stack>
</form>
{error && (
<Alert icon={<IconAlertTriangle size={rem(16)} />} color="red">
{error}
</Alert>
)}
</Stack>
);
};
type FormType = z.infer<typeof validation.user.signIn>;
@@ -0,0 +1,29 @@
import { Card, Center, Stack, Text, Title } from "@mantine/core";
import { getScopedI18n } from "@homarr/translation/server";
import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo";
import { LoginForm } from "./_login-form";
export default async function Login() {
const t = await getScopedI18n("user.page.login");
return (
<Center>
<Stack align="center" mt="xl">
<HomarrLogoWithTitle size="lg" />
<Stack gap={6} align="center">
<Title order={3} fw={400} ta="center">
{t("title")}
</Title>
<Text size="sm" c="gray.5" ta="center">
{t("subtitle")}
</Text>
</Stack>
<Card bg="dark.8" w={64 * 6} maw="90vw">
<LoginForm />
</Card>
</Stack>
</Center>
);
}
@@ -0,0 +1,9 @@
import { api } from "@homarr/api/server";
import { createBoardContentPage } from "../_creator";
export default createBoardContentPage<{ locale: string }>({
async getInitialBoard() {
return await api.board.getDefaultBoard();
},
});
@@ -0,0 +1,5 @@
import definition from "./_definition";
const { layout } = definition;
export default layout;
@@ -0,0 +1,7 @@
import definition from "./_definition";
const { generateMetadata, page } = definition;
export default page;
export { generateMetadata };
@@ -0,0 +1,9 @@
import { api } from "@homarr/api/server";
import { createBoardContentPage } from "../_creator";
export default createBoardContentPage<{ locale: string; name: string }>({
async getInitialBoard({ name }) {
return await api.board.getBoardByName({ name });
},
});
@@ -0,0 +1,5 @@
import definition from "./_definition";
const { layout } = definition;
export default layout;
@@ -0,0 +1,7 @@
import definition from "./_definition";
const { generateMetadata, page } = definition;
export default page;
export { generateMetadata };
@@ -0,0 +1,87 @@
"use client";
import { useCallback, useRef } from "react";
import { Box, LoadingOverlay, Stack } from "@mantine/core";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { BoardCategorySection } from "~/components/board/sections/category-section";
import { BoardEmptySection } from "~/components/board/sections/empty-section";
import { BoardBackgroundVideo } from "~/components/layout/background";
import { fullHeightWithoutHeaderAndFooter } from "~/constants";
import { useIsBoardReady, useRequiredBoard } from "./_context";
let boardName: string | null = null;
export const updateBoardName = (name: string | null) => {
boardName = name;
};
type UpdateCallback = (
prev: RouterOutputs["board"]["getDefaultBoard"],
) => RouterOutputs["board"]["getDefaultBoard"];
export const useUpdateBoard = () => {
const utils = clientApi.useUtils();
const updateBoard = useCallback(
(updaterWithoutUndefined: UpdateCallback) => {
if (!boardName) {
throw new Error("Board name is not set");
}
utils.board.getBoardByName.setData({ name: boardName }, (previous) =>
previous ? updaterWithoutUndefined(previous) : previous,
);
},
[utils],
);
return {
updateBoard,
};
};
export const ClientBoard = () => {
const board = useRequiredBoard();
const isReady = useIsBoardReady();
const sortedSections = board.sections.sort(
(sectionA, sectionB) => sectionA.position - sectionB.position,
);
const ref = useRef<HTMLDivElement>(null);
return (
<Box h="100%" pos="relative">
<BoardBackgroundVideo />
<LoadingOverlay
visible={!isReady}
transitionProps={{ duration: 500 }}
loaderProps={{ size: "lg" }}
h={fullHeightWithoutHeaderAndFooter}
/>
<Stack
ref={ref}
h="100%"
style={{ visibility: isReady ? "visible" : "hidden" }}
>
{sortedSections.map((section) =>
section.kind === "empty" ? (
<BoardEmptySection
key={section.id}
section={section}
mainRef={ref}
/>
) : (
<BoardCategorySection
key={section.id}
section={section}
mainRef={ref}
/>
),
)}
</Stack>
</Box>
);
};
@@ -0,0 +1,116 @@
"use client";
import type { PropsWithChildren } from "react";
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import { usePathname } from "next/navigation";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { updateBoardName } from "./_client";
const BoardContext = createContext<{
board: RouterOutputs["board"]["getDefaultBoard"];
isReady: boolean;
markAsReady: (id: string) => void;
} | null>(null);
export const BoardProvider = ({
children,
initialBoard,
}: PropsWithChildren<{
initialBoard: RouterOutputs["board"]["getBoardByName"];
}>) => {
const pathname = usePathname();
const utils = clientApi.useUtils();
const [readySections, setReadySections] = useState<string[]>([]);
const { data } = clientApi.board.getBoardByName.useQuery(
{ name: initialBoard.name },
{
initialData: initialBoard,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
},
);
// Update the board name so it can be used within updateBoard method
updateBoardName(initialBoard.name);
// Invalidate the board when the pathname changes
// This allows to refetch the board when it might have changed - e.g. if someone else added an item
useEffect(() => {
return () => {
setReadySections([]);
void utils.board.getBoardByName.invalidate({ name: initialBoard.name });
};
}, [pathname, utils, initialBoard.name]);
useEffect(() => {
setReadySections((previous) =>
previous.filter((id) =>
data.sections.some((section) => section.id === id),
),
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data.sections.length, setReadySections]);
const markAsReady = useCallback((id: string) => {
setReadySections((previous) =>
previous.includes(id) ? previous : [...previous, id],
);
}, []);
return (
<BoardContext.Provider
value={{
board: data,
isReady: data.sections.length === readySections.length,
markAsReady,
}}
>
{children}
</BoardContext.Provider>
);
};
export const useMarkSectionAsReady = () => {
const context = useContext(BoardContext);
if (!context) {
throw new Error("Board is required");
}
return context.markAsReady;
};
export const useIsBoardReady = () => {
const context = useContext(BoardContext);
if (!context) {
throw new Error("Board is required");
}
return context.isReady;
};
export const useRequiredBoard = () => {
const optionalBoard = useOptionalBoard();
if (!optionalBoard) {
throw new Error("Board is required");
}
return optionalBoard;
};
export const useOptionalBoard = () => {
const context = useContext(BoardContext);
return context?.board;
};
@@ -0,0 +1,58 @@
import type { Metadata } from "next";
import { TRPCError } from "@trpc/server";
import { capitalize } from "@homarr/common";
// Placed here because gridstack styles are used for board content
import "~/styles/gridstack.scss";
import { createBoardLayout } from "../_layout-creator";
import type { Board } from "../_types";
import { ClientBoard } from "./_client";
import { BoardContentHeaderActions } from "./_header-actions";
export type Params = Record<string, unknown>;
interface Props<TParams extends Params> {
getInitialBoard: (params: TParams) => Promise<Board>;
}
export const createBoardContentPage = <
TParams extends Record<string, unknown>,
>({
getInitialBoard,
}: Props<TParams>) => {
return {
layout: createBoardLayout({
headerActions: <BoardContentHeaderActions />,
getInitialBoard,
isBoardContentPage: true,
}),
page: () => {
return <ClientBoard />;
},
generateMetadata: async ({
params,
}: {
params: TParams;
}): Promise<Metadata> => {
try {
const board = await getInitialBoard(params);
return {
title: board.metaTitle ?? `${capitalize(board.name)} board | Homarr`,
icons: {
icon: board.faviconImageUrl ? board.faviconImageUrl : undefined,
},
};
} catch (error) {
// Ignore not found errors and return empty metadata
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return {};
}
throw error;
}
},
};
};
@@ -0,0 +1,159 @@
"use client";
import { useCallback } from "react";
import { Group, Menu } from "@mantine/core";
import {
IconBox,
IconBoxAlignTop,
IconChevronDown,
IconPackageImport,
IconPencil,
IconPencilOff,
IconPlus,
IconSettings,
} from "@tabler/icons-react";
import { useAtom, useAtomValue } from "jotai";
import { clientApi } from "@homarr/api/client";
import { useModalAction } from "@homarr/modals";
import {
showErrorNotification,
showSuccessNotification,
} from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { revalidatePathAction } from "~/app/revalidatePathAction";
import { editModeAtom } from "~/components/board/editMode";
import { ItemSelectModal } from "~/components/board/items/item-select-modal";
import { useBoardPermissions } from "~/components/board/permissions/client";
import { useCategoryActions } from "~/components/board/sections/category/category-actions";
import { CategoryEditModal } from "~/components/board/sections/category/category-edit-modal";
import { HeaderButton } from "~/components/layout/header/button";
import { useRequiredBoard } from "./_context";
export const BoardContentHeaderActions = () => {
const isEditMode = useAtomValue(editModeAtom);
const board = useRequiredBoard();
const { hasChangeAccess } = useBoardPermissions(board);
if (!hasChangeAccess) {
return null; // Hide actions for user without access
}
return (
<>
{isEditMode && <AddMenu />}
<EditModeMenu />
<HeaderButton href={`/boards/${board.name}/settings`}>
<IconSettings stroke={1.5} />
</HeaderButton>
</>
);
};
const AddMenu = () => {
const { openModal: openCategoryEditModal } =
useModalAction(CategoryEditModal);
const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal);
const { addCategoryToEnd } = useCategoryActions();
const t = useI18n();
const handleAddCategory = useCallback(
() =>
openCategoryEditModal(
{
category: {
id: "new",
name: "",
},
onSuccess({ name }) {
addCategoryToEnd({ name });
},
submitLabel: t("section.category.create.submit"),
},
{
title: (t) => t("section.category.create.title"),
},
),
[addCategoryToEnd, openCategoryEditModal, t],
);
const handleSelectItem = useCallback(() => {
openItemSelectModal();
}, [openItemSelectModal]);
return (
<Menu position="bottom-end" withArrow>
<Menu.Target>
<HeaderButton w="auto" px={4}>
<Group gap={4} wrap="nowrap">
<IconPlus stroke={1.5} />
<IconChevronDown color="gray" size={16} />
</Group>
</HeaderButton>
</Menu.Target>
<Menu.Dropdown style={{ transform: "translate(-3px, 0)" }}>
<Menu.Item
leftSection={<IconBox size={20} />}
onClick={handleSelectItem}
>
{t("item.action.create")}
</Menu.Item>
<Menu.Item leftSection={<IconPackageImport size={20} />}>
{t("item.action.import")}
</Menu.Item>
<Menu.Divider />
<Menu.Item
leftSection={<IconBoxAlignTop size={20} />}
onClick={handleAddCategory}
>
{t("section.category.action.create")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
);
};
const EditModeMenu = () => {
const [isEditMode, setEditMode] = useAtom(editModeAtom);
const board = useRequiredBoard();
const utils = clientApi.useUtils();
const t = useScopedI18n("board.action.edit");
const { mutate: saveBoard, isPending } =
clientApi.board.saveBoard.useMutation({
onSuccess() {
showSuccessNotification({
title: t("notification.success.title"),
message: t("notification.success.message"),
});
void utils.board.getBoardByName.invalidate({ name: board.name });
void revalidatePathAction(`/boards/${board.name}`);
setEditMode(false);
},
onError() {
showErrorNotification({
title: t("notification.error.title"),
message: t("notification.error.message"),
});
},
});
const toggle = useCallback(() => {
if (isEditMode) return saveBoard(board);
setEditMode(true);
}, [board, isEditMode, saveBoard, setEditMode]);
return (
<HeaderButton onClick={toggle} loading={isPending}>
{isEditMode ? (
<IconPencilOff stroke={1.5} />
) : (
<IconPencil stroke={1.5} />
)}
</HeaderButton>
);
};
@@ -0,0 +1,49 @@
"use client";
import type { PropsWithChildren } from "react";
import type { MantineColorsTuple } from "@mantine/core";
import { createTheme, darken, lighten, MantineProvider } from "@mantine/core";
import { useRequiredBoard } from "./_context";
export const BoardMantineProvider = ({ children }: PropsWithChildren) => {
const board = useRequiredBoard();
const theme = createTheme({
colors: {
primaryColor: generateColors(board.primaryColor),
secondaryColor: generateColors(board.secondaryColor),
},
primaryColor: "primaryColor",
autoContrast: true,
});
return <MantineProvider theme={theme}>{children}</MantineProvider>;
};
export const generateColors = (hex: string) => {
const lightnessForColors = [
-0.25, -0.2, -0.15, -0.1, -0.05, 0, 0.05, 0.1, 0.15, 0.2,
] as const;
const rgbaColors = lightnessForColors.map((lightness) => {
if (lightness < 0) {
return lighten(hex, -lightness);
}
return darken(hex, lightness);
});
return rgbaColors.map((color) => {
return (
"#" +
color
.split("(")[1]!
.replaceAll(" ", "")
.replace(")", "")
.split(",")
.map((color) => parseInt(color, 10))
.slice(0, 3)
.map((color) => color.toString(16).padStart(2, "0"))
.join("")
);
}) as unknown as MantineColorsTuple;
};
@@ -0,0 +1,12 @@
import { api } from "@homarr/api/server";
import { BoardOtherHeaderActions } from "../_header-actions";
import { createBoardLayout } from "../_layout-creator";
export default createBoardLayout<{ locale: string; name: string }>({
headerActions: <BoardOtherHeaderActions />,
async getInitialBoard({ name }) {
return await api.board.getBoardByName({ name });
},
isBoardContentPage: false,
});
@@ -0,0 +1,105 @@
"use client";
import { useState } from "react";
import { Group, Stack, Tabs } from "@mantine/core";
import { IconUser, IconUserDown, IconUsersGroup } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useScopedI18n } from "@homarr/translation/client";
import type { TablerIcon } from "@homarr/ui";
import { CountBadge } from "@homarr/ui";
import type { Board } from "../../_types";
import { GroupsForm } from "./_access/group-access";
import { InheritTable } from "./_access/inherit-access";
import { UsersForm } from "./_access/user-access";
interface Props {
board: Board;
initialPermissions: RouterOutputs["board"]["getBoardPermissions"];
}
export const AccessSettingsContent = ({ board, initialPermissions }: Props) => {
const { data: permissions } = clientApi.board.getBoardPermissions.useQuery(
{
id: board.id,
},
{
initialData: initialPermissions,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
},
);
const [counts, setCounts] = useState({
user: initialPermissions.userPermissions.length + (board.creator ? 1 : 0),
group: initialPermissions.groupPermissions.length,
});
return (
<Stack>
<Tabs color="red" defaultValue="user">
<Tabs.List grow>
<TabItem value="user" count={counts.user} icon={IconUser} />
<TabItem value="group" count={counts.group} icon={IconUsersGroup} />
<TabItem
value="inherited"
count={initialPermissions.inherited.length}
icon={IconUserDown}
/>
</Tabs.List>
<Tabs.Panel value="user">
<UsersForm
board={board}
initialPermissions={permissions}
onCountChange={(callback) =>
setCounts(({ user, ...others }) => ({
user: callback(user),
...others,
}))
}
/>
</Tabs.Panel>
<Tabs.Panel value="group">
<GroupsForm
board={board}
initialPermissions={permissions}
onCountChange={(callback) =>
setCounts(({ group, ...others }) => ({
group: callback(group),
...others,
}))
}
/>
</Tabs.Panel>
<Tabs.Panel value="inherited">
<InheritTable initialPermissions={permissions} />
</Tabs.Panel>
</Tabs>
</Stack>
);
};
interface TabItemProps {
value: "user" | "group" | "inherited";
count: number;
icon: TablerIcon;
}
const TabItem = ({ value, icon: Icon, count }: TabItemProps) => {
const t = useScopedI18n("board.setting.section.access.permission");
return (
<Tabs.Tab value={value} leftSection={<Icon stroke={1.5} size={16} />}>
<Group gap="sm">
{t(`tab.${value}`)}
<CountBadge count={count} />
</Group>
</Tabs.Tab>
);
};
@@ -0,0 +1,139 @@
import { useCallback } from "react";
import type { ReactNode } from "react";
import type { SelectProps } from "@mantine/core";
import {
Button,
Flex,
Group,
Select,
TableTd,
TableTr,
Text,
} from "@mantine/core";
import {
IconCheck,
IconEye,
IconPencil,
IconSettings,
} from "@tabler/icons-react";
import type { BoardPermission } from "@homarr/definitions";
import { boardPermissions } from "@homarr/definitions";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { TablerIcon } from "@homarr/ui";
import type { OnCountChange } from "./form";
import { useFormContext } from "./form";
const icons = {
"board-change": IconPencil,
"board-view": IconEye,
"board-full": IconSettings,
} satisfies Record<BoardPermission | "board-full", TablerIcon>;
interface BoardAccessSelectRowProps {
itemContent: ReactNode;
permission: BoardPermission;
index: number;
onCountChange: OnCountChange;
}
export const BoardAccessSelectRow = ({
itemContent,
permission,
index,
onCountChange,
}: BoardAccessSelectRowProps) => {
const tRoot = useI18n();
const tPermissions = useScopedI18n("board.setting.section.access.permission");
const form = useFormContext();
const Icon = icons[permission];
const handleRemove = useCallback(() => {
form.setFieldValue(
"items",
form.values.items.filter((_, i) => i !== index),
);
onCountChange((prev) => prev - 1);
}, [form, index, onCountChange]);
return (
<TableTr>
<TableTd w={{ sm: 128, lg: 256 }}>{itemContent}</TableTd>
<TableTd>
<Flex
direction={{ base: "column", xs: "row" }}
align={{ base: "end", xs: "center" }}
wrap="nowrap"
>
<Select
allowDeselect={false}
flex="1"
leftSection={<Icon size="1rem" />}
renderOption={RenderOption}
variant="unstyled"
data={boardPermissions.map((permission) => ({
value: permission,
label: tPermissions(`item.${permission}.label`),
}))}
{...form.getInputProps(`items.${index}.permission`)}
/>
<Button size="xs" variant="subtle" onClick={handleRemove}>
{tRoot("common.action.remove")}
</Button>
</Flex>
</TableTd>
</TableTr>
);
};
interface BoardAccessDisplayRowProps {
itemContent: ReactNode;
permission: BoardPermission | "board-full";
}
export const BoardAccessDisplayRow = ({
itemContent,
permission,
}: BoardAccessDisplayRowProps) => {
const tPermissions = useScopedI18n("board.setting.section.access.permission");
const Icon = icons[permission];
return (
<TableTr>
<TableTd w={{ sm: 128, lg: 256 }}>{itemContent}</TableTd>
<TableTd>
<Group gap={0}>
<Flex w={34} h={34} align="center" justify="center">
<Icon
size="1rem"
color="var(--input-section-color, var(--mantine-color-dimmed))"
/>
</Flex>
<Text size="sm">{tPermissions(`item.${permission}.label`)}</Text>
</Group>
</TableTd>
</TableTr>
);
};
const iconProps = {
stroke: 1.5,
color: "currentColor",
opacity: 0.6,
size: "1rem",
};
const RenderOption: SelectProps["renderOption"] = ({ option, checked }) => {
const Icon = icons[option.value as BoardPermission];
return (
<Group flex="1" gap="xs" wrap="nowrap">
<Icon {...iconProps} />
{option.label}
{checked && (
<IconCheck style={{ marginInlineStart: "auto" }} {...iconProps} />
)}
</Group>
);
};
@@ -0,0 +1,14 @@
import type { BoardPermission } from "@homarr/definitions";
import { createFormContext } from "@homarr/form";
export interface BoardAccessFormType {
items: {
itemId: string;
permission: BoardPermission;
}[];
}
export const [FormProvider, useFormContext, useForm] =
createFormContext<BoardAccessFormType>();
export type OnCountChange = (callback: (prev: number) => number) => void;
@@ -0,0 +1,148 @@
import { useCallback, useState } from "react";
import Link from "next/link";
import {
Anchor,
Button,
Group,
Stack,
Table,
TableTbody,
TableTh,
TableThead,
TableTr,
} from "@mantine/core";
import { IconPlus } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useModalAction } from "@homarr/modals";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { BoardAccessSelectRow } from "./board-access-table-rows";
import type { BoardAccessFormType } from "./form";
import { FormProvider, useForm } from "./form";
import { GroupSelectModal } from "./group-select-modal";
import type { FormProps } from "./user-access";
export const GroupsForm = ({
board,
initialPermissions,
onCountChange,
}: FormProps) => {
const { mutate, isPending } =
clientApi.board.saveGroupBoardPermissions.useMutation();
const utils = clientApi.useUtils();
const [groups, setGroups] = useState<Map<string, Group>>(
new Map(
initialPermissions.groupPermissions.map(({ group }) => [group.id, group]),
),
);
const { openModal } = useModalAction(GroupSelectModal);
const t = useI18n();
const tPermissions = useScopedI18n("board.setting.section.access.permission");
const form = useForm({
initialValues: {
items: initialPermissions.groupPermissions.map(
({ group, permission }) => ({
itemId: group.id,
permission,
}),
),
},
});
const handleSubmit = useCallback(
(values: BoardAccessFormType) => {
mutate(
{
id: board.id,
permissions: values.items,
},
{
onSuccess: () => {
void utils.board.getBoardPermissions.invalidate();
},
},
);
},
[board.id, mutate, utils.board.getBoardPermissions],
);
const handleAddUser = useCallback(() => {
openModal({
presentGroupIds: form.values.items.map(({ itemId: id }) => id),
onSelect: (group) => {
setGroups((prev) => new Map(prev).set(group.id, group));
form.setFieldValue("items", [
{
itemId: group.id,
permission: "board-view",
},
...form.values.items,
]);
onCountChange((prev) => prev + 1);
},
});
}, [form, openModal, onCountChange]);
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<FormProvider form={form}>
<Stack pt="sm">
<Table>
<TableThead>
<TableTr>
<TableTh style={{ whiteSpace: "nowrap" }}>
{tPermissions("field.group.label")}
</TableTh>
<TableTh>{tPermissions("field.permission.label")}</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{form.values.items.map((row, index) => (
<BoardAccessSelectRow
key={row.itemId}
itemContent={
<GroupItemContent group={groups.get(row.itemId)!} />
}
permission={row.permission}
index={index}
onCountChange={onCountChange}
/>
))}
</TableTbody>
</Table>
<Group justify="space-between">
<Button
rightSection={<IconPlus size="1rem" />}
variant="light"
onClick={handleAddUser}
>
{t("common.action.add")}
</Button>
<Button type="submit" loading={isPending} color="teal">
{t("common.action.saveChanges")}
</Button>
</Group>
</Stack>
</FormProvider>
</form>
);
};
export const GroupItemContent = ({ group }: { group: Group }) => {
return (
<Anchor
component={Link}
href={`/manage/users/groups/${group.id}`}
size="sm"
style={{ whiteSpace: "nowrap" }}
>
{group.name}
</Anchor>
);
};
type Group =
RouterOutputs["board"]["getBoardPermissions"]["groupPermissions"][0]["group"];
@@ -0,0 +1,72 @@
import { useState } from "react";
import { Button, Group, Loader, Select, Stack } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import { useForm } from "@homarr/form";
import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
interface InnerProps {
presentGroupIds: string[];
onSelect: (props: { id: string; name: string }) => void | Promise<void>;
confirmLabel?: string;
}
interface GroupSelectFormType {
groupId: string;
}
export const GroupSelectModal = createModal<InnerProps>(
({ actions, innerProps }) => {
const t = useI18n();
const { data: groups, isPending } = clientApi.group.selectable.useQuery();
const [loading, setLoading] = useState(false);
const form = useForm<GroupSelectFormType>();
const handleSubmit = async (values: GroupSelectFormType) => {
const currentGroup = groups?.find((group) => group.id === values.groupId);
if (!currentGroup) return;
setLoading(true);
await innerProps.onSelect({
id: currentGroup.id,
name: currentGroup.name,
});
setLoading(false);
actions.closeModal();
};
const confirmLabel = innerProps.confirmLabel ?? t("common.action.add");
return (
<form onSubmit={form.onSubmit((values) => void handleSubmit(values))}>
<Stack>
<Select
{...form.getInputProps("groupId")}
label={t("group.action.select.label")}
clearable
searchable
leftSection={isPending ? <Loader size="xs" /> : undefined}
nothingFoundMessage={t("group.action.select.notFound")}
limit={5}
data={groups
?.filter(
(group) => !innerProps.presentGroupIds.includes(group.id),
)
.map((group) => ({ value: group.id, label: group.name }))}
/>
<Group justify="end">
<Button variant="default" onClick={actions.closeModal}>
{t("common.action.cancel")}
</Button>
<Button type="submit" loading={loading}>
{confirmLabel}
</Button>
</Group>
</Stack>
</form>
);
},
).withOptions({
defaultTitle: (t) =>
t("board.setting.section.access.permission.groupSelect.title"),
});
@@ -0,0 +1,66 @@
import {
Stack,
Table,
TableTbody,
TableTh,
TableThead,
TableTr,
} from "@mantine/core";
import type { RouterOutputs } from "@homarr/api";
import { getPermissionsWithChildren } from "@homarr/definitions";
import type { BoardPermission, GroupPermissionKey } from "@homarr/definitions";
import { useScopedI18n } from "@homarr/translation/client";
import { BoardAccessDisplayRow } from "./board-access-table-rows";
import { GroupItemContent } from "./group-access";
export interface InheritTableProps {
initialPermissions: RouterOutputs["board"]["getBoardPermissions"];
}
const mapPermissions = {
"board-full-access": "board-full",
"board-modify-all": "board-change",
"board-view-all": "board-view",
} satisfies Partial<Record<GroupPermissionKey, BoardPermission | "board-full">>;
export const InheritTable = ({ initialPermissions }: InheritTableProps) => {
const tPermissions = useScopedI18n("board.setting.section.access.permission");
return (
<Stack pt="sm">
<Table>
<TableThead>
<TableTr>
<TableTh>{tPermissions("field.user.label")}</TableTh>
<TableTh>{tPermissions("field.permission.label")}</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{initialPermissions.inherited.map(({ group, permission }) => {
const boardPermission =
permission in mapPermissions
? mapPermissions[permission as keyof typeof mapPermissions]
: getPermissionsWithChildren([permission]).includes(
"board-full-access",
)
? "board-full"
: null;
if (!boardPermission) {
return null;
}
return (
<BoardAccessDisplayRow
key={group.id}
itemContent={<GroupItemContent group={group} />}
permission={boardPermission}
/>
);
})}
</TableTbody>
</Table>
</Stack>
);
};
@@ -0,0 +1,173 @@
import { useCallback, useState } from "react";
import Link from "next/link";
import {
Anchor,
Box,
Button,
Group,
Stack,
Table,
TableTbody,
TableTh,
TableThead,
TableTr,
} from "@mantine/core";
import { IconPlus } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useModalAction } from "@homarr/modals";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { UserAvatar } from "@homarr/ui";
import type { Board } from "../../../_types";
import {
BoardAccessDisplayRow,
BoardAccessSelectRow,
} from "./board-access-table-rows";
import type { BoardAccessFormType, OnCountChange } from "./form";
import { FormProvider, useForm } from "./form";
import { UserSelectModal } from "./user-select-modal";
export interface FormProps {
board: Pick<Board, "id" | "creatorId" | "creator">;
initialPermissions: RouterOutputs["board"]["getBoardPermissions"];
onCountChange: OnCountChange;
}
export const UsersForm = ({
board,
initialPermissions,
onCountChange,
}: FormProps) => {
const { mutate, isPending } =
clientApi.board.saveUserBoardPermissions.useMutation();
const utils = clientApi.useUtils();
const [users, setUsers] = useState<Map<string, User>>(
new Map(
initialPermissions.userPermissions.map(({ user }) => [user.id, user]),
),
);
const { openModal } = useModalAction(UserSelectModal);
const t = useI18n();
const tPermissions = useScopedI18n("board.setting.section.access.permission");
const form = useForm({
initialValues: {
items: initialPermissions.userPermissions.map(({ user, permission }) => ({
itemId: user.id,
permission,
})),
},
});
const handleSubmit = useCallback(
(values: BoardAccessFormType) => {
mutate(
{
id: board.id,
permissions: values.items,
},
{
onSuccess: () => {
void utils.board.getBoardPermissions.invalidate();
},
},
);
},
[board.id, mutate, utils.board.getBoardPermissions],
);
const handleAddUser = useCallback(() => {
const presentUserIds = form.values.items.map(({ itemId: id }) => id);
openModal({
presentUserIds: board.creatorId
? presentUserIds.concat(board.creatorId)
: presentUserIds,
onSelect: (user) => {
setUsers((prev) => new Map(prev).set(user.id, user));
form.setFieldValue("items", [
{
itemId: user.id,
permission: "board-view",
},
...form.values.items,
]);
onCountChange((prev) => prev + 1);
},
});
}, [form, openModal, board.creatorId, onCountChange]);
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<FormProvider form={form}>
<Stack pt="sm">
<Table>
<TableThead>
<TableTr>
<TableTh>{tPermissions("field.user.label")}</TableTh>
<TableTh>{tPermissions("field.permission.label")}</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{board.creator && (
<BoardAccessDisplayRow
itemContent={<UserItemContent user={board.creator} />}
permission="board-full"
/>
)}
{form.values.items.map((row, index) => (
<BoardAccessSelectRow
key={row.itemId}
itemContent={
<UserItemContent user={users.get(row.itemId)!} />
}
permission={row.permission}
index={index}
onCountChange={onCountChange}
/>
))}
</TableTbody>
</Table>
<Group justify="space-between">
<Button
rightSection={<IconPlus size="1rem" />}
variant="light"
onClick={handleAddUser}
>
{t("common.action.add")}
</Button>
<Button type="submit" loading={isPending} color="teal">
{t("common.action.saveChanges")}
</Button>
</Group>
</Stack>
</FormProvider>
</form>
);
};
const UserItemContent = ({ user }: { user: User }) => {
return (
<Group wrap="nowrap">
<Box visibleFrom="xs">
<UserAvatar user={user} size="sm" />
</Box>
<Anchor
component={Link}
href={`/manage/users/${user.id}`}
size="sm"
style={{ whiteSpace: "nowrap" }}
>
{user.name}
</Anchor>
</Group>
);
};
interface User {
id: string;
name: string | null;
image: string | null;
}
@@ -0,0 +1,112 @@
import { useState } from "react";
import type { SelectProps } from "@mantine/core";
import { Button, Group, Loader, Select, Stack } from "@mantine/core";
import { IconCheck } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useForm } from "@homarr/form";
import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
import { UserAvatar } from "@homarr/ui";
interface InnerProps {
presentUserIds: string[];
onSelect: (props: {
id: string;
name: string;
image: string;
}) => void | Promise<void>;
confirmLabel?: string;
}
interface UserSelectFormType {
userId: string;
}
export const UserSelectModal = createModal<InnerProps>(
({ actions, innerProps }) => {
const t = useI18n();
const { data: users, isPending } = clientApi.user.selectable.useQuery();
const [loading, setLoading] = useState(false);
const form = useForm<UserSelectFormType>();
const handleSubmit = async (values: UserSelectFormType) => {
const currentUser = users?.find((user) => user.id === values.userId);
if (!currentUser) return;
setLoading(true);
await innerProps.onSelect({
id: currentUser.id,
name: currentUser.name ?? "",
image: currentUser.image ?? "",
});
setLoading(false);
actions.closeModal();
};
const confirmLabel = innerProps.confirmLabel ?? t("common.action.add");
const currentUser = users?.find((user) => user.id === form.values.userId);
return (
<form onSubmit={form.onSubmit((values) => void handleSubmit(values))}>
<Stack>
<Select
{...form.getInputProps("userId")}
label={t("user.action.select.label")}
searchable
clearable
leftSection={
isPending ? (
<Loader size="xs" />
) : currentUser ? (
<UserAvatar user={currentUser} size="xs" />
) : undefined
}
nothingFoundMessage={t("user.action.select.notFound")}
renderOption={createRenderOption(users ?? [])}
limit={5}
data={users
?.filter((user) => !innerProps.presentUserIds.includes(user.id))
.map((user) => ({ value: user.id, label: user.name ?? "" }))}
/>
<Group justify="end">
<Button variant="default" onClick={actions.closeModal}>
{t("common.action.cancel")}
</Button>
<Button type="submit" loading={loading}>
{confirmLabel}
</Button>
</Group>
</Stack>
</form>
);
},
).withOptions({
defaultTitle: (t) =>
t("board.setting.section.access.permission.userSelect.title"),
});
const iconProps = {
stroke: 1.5,
color: "currentColor",
opacity: 0.6,
size: "1rem",
};
const createRenderOption = (
users: RouterOutputs["user"]["selectable"],
): SelectProps["renderOption"] =>
function InnerRenderRoot({ option, checked }) {
const user = users.find((user) => user.id === option.value);
if (!user) return null;
return (
<Group flex="1" gap="xs">
<UserAvatar user={user} size="xs" />
{option.label}
{checked && (
<IconCheck style={{ marginInlineStart: "auto" }} {...iconProps} />
)}
</Group>
);
};
@@ -0,0 +1,135 @@
"use client";
import { Button, Grid, Group, Stack, TextInput } from "@mantine/core";
import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
} from "@homarr/definitions";
import { useForm } from "@homarr/form";
import type { TranslationObject } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import type { SelectItemWithDescriptionBadge } from "@homarr/ui";
import { SelectWithDescriptionBadge } from "@homarr/ui";
import type { Board } from "../../_types";
import { useSavePartialSettingsMutation } from "./_shared";
interface Props {
board: Board;
}
export const BackgroundSettingsContent = ({ board }: Props) => {
const t = useI18n();
const { mutate: savePartialSettings, isPending } =
useSavePartialSettingsMutation(board);
const form = useForm({
initialValues: {
backgroundImageUrl: board.backgroundImageUrl ?? "",
backgroundImageAttachment: board.backgroundImageAttachment,
backgroundImageRepeat: board.backgroundImageRepeat,
backgroundImageSize: board.backgroundImageSize,
},
});
const backgroundImageAttachmentData = useBackgroundOptionData(
"backgroundImageAttachment",
backgroundImageAttachments,
);
const backgroundImageSizeData = useBackgroundOptionData(
"backgroundImageSize",
backgroundImageSizes,
);
const backgroundImageRepeatData = useBackgroundOptionData(
"backgroundImageRepeat",
backgroundImageRepeats,
);
return (
<form
onSubmit={form.onSubmit((values) => {
savePartialSettings({
id: board.id,
...values,
});
})}
>
<Stack>
<Grid>
<Grid.Col span={12}>
<TextInput
label={t("board.field.backgroundImageUrl.label")}
{...form.getInputProps("backgroundImageUrl")}
/>
</Grid.Col>
<Grid.Col span={12}>
<SelectWithDescriptionBadge
label={t("board.field.backgroundImageAttachment.label")}
data={backgroundImageAttachmentData}
{...form.getInputProps("backgroundImageAttachment")}
/>
</Grid.Col>
<Grid.Col span={12}>
<SelectWithDescriptionBadge
label={t("board.field.backgroundImageSize.label")}
data={backgroundImageSizeData}
{...form.getInputProps("backgroundImageSize")}
/>
</Grid.Col>
<Grid.Col span={12}>
<SelectWithDescriptionBadge
label={t("board.field.backgroundImageRepeat.label")}
data={backgroundImageRepeatData}
{...form.getInputProps("backgroundImageRepeat")}
/>
</Grid.Col>
</Grid>
<Group justify="end">
<Button type="submit" loading={isPending} color="teal">
{t("common.action.saveChanges")}
</Button>
</Group>
</Stack>
</form>
);
};
type BackgroundImageKey =
| "backgroundImageAttachment"
| "backgroundImageSize"
| "backgroundImageRepeat";
type inferOptions<TKey extends BackgroundImageKey> =
TranslationObject["board"]["field"][TKey]["option"];
const useBackgroundOptionData = <
TKey extends BackgroundImageKey,
TOptions extends inferOptions<TKey> = inferOptions<TKey>,
>(
key: TKey,
data: {
values: (keyof TOptions)[];
defaultValue: keyof TOptions;
},
) => {
const t = useI18n();
return data.values.map(
(value) =>
({
label: t(`board.field.${key}.option.${value as string}.label` as never),
description: t(
`board.field.${key}.option.${value as string}.description` as never,
),
value: value as string,
badge:
data.defaultValue === value
? {
color: "blue",
label: t("common.select.badge.recommended"),
}
: undefined,
}) satisfies SelectItemWithDescriptionBadge,
);
};
@@ -0,0 +1,158 @@
"use client";
import {
Anchor,
Button,
Collapse,
ColorInput,
ColorSwatch,
Grid,
Group,
InputWrapper,
isLightColor,
Slider,
Stack,
Text,
useMantineTheme,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { useForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";
import type { Board } from "../../_types";
import { generateColors } from "../../(content)/_theme";
import { useSavePartialSettingsMutation } from "./_shared";
interface Props {
board: Board;
}
const hexRegex = /^#[0-9a-fA-F]{6}$/;
const progressPercentageLabel = (value: number) => `${value}%`;
export const ColorSettingsContent = ({ board }: Props) => {
const form = useForm({
initialValues: {
primaryColor: board.primaryColor,
secondaryColor: board.secondaryColor,
opacity: board.opacity,
},
});
const [showPreview, { toggle }] = useDisclosure(false);
const t = useI18n();
const theme = useMantineTheme();
const { mutate: savePartialSettings, isPending } =
useSavePartialSettingsMutation(board);
return (
<form
onSubmit={form.onSubmit((values) => {
savePartialSettings({
id: board.id,
...values,
});
})}
>
<Stack>
<Grid>
<Grid.Col span={{ sm: 12, md: 6 }}>
<Stack gap="xs">
<ColorInput
label={t("board.field.primaryColor.label")}
format="hex"
swatches={Object.values(theme.colors).map((color) => color[6])}
{...form.getInputProps("primaryColor")}
/>
</Stack>
</Grid.Col>
<Grid.Col span={{ sm: 12, md: 6 }}>
<ColorInput
label={t("board.field.secondaryColor.label")}
format="hex"
swatches={Object.values(theme.colors).map((color) => color[6])}
{...form.getInputProps("secondaryColor")}
/>
</Grid.Col>
<Grid.Col span={12}>
<Anchor onClick={toggle}>
{showPreview
? t("common.preview.hide")
: t("common.preview.show")}
</Anchor>
</Grid.Col>
<Grid.Col span={12}>
<Collapse in={showPreview}>
<Stack>
<ColorsPreview previewColor={form.values.primaryColor} />
<ColorsPreview previewColor={form.values.secondaryColor} />
</Stack>
</Collapse>
</Grid.Col>
<Grid.Col span={{ sm: 12, md: 6 }}>
<InputWrapper label={t("board.field.opacity.label")}>
<Slider
my={6}
min={0}
max={100}
step={5}
label={progressPercentageLabel}
{...form.getInputProps("opacity")}
/>
</InputWrapper>
</Grid.Col>
</Grid>
<Group justify="end">
<Button type="submit" loading={isPending} color="teal">
{t("common.action.saveChanges")}
</Button>
</Group>
</Stack>
</form>
);
};
interface ColorsPreviewProps {
previewColor: string;
}
const ColorsPreview = ({ previewColor }: ColorsPreviewProps) => {
const theme = useMantineTheme();
const colors = hexRegex.test(previewColor)
? generateColors(previewColor)
: generateColors("#000000");
return (
<Group gap={0} wrap="nowrap">
{colors.map((color, index) => (
<ColorSwatch
key={index}
color={color}
w="10%"
pb="10%"
c={isLightColor(color) ? "black" : "white"}
radius={0}
styles={{
colorOverlay: {
borderTopLeftRadius: index === 0 ? theme.radius.md : 0,
borderBottomLeftRadius: index === 0 ? theme.radius.md : 0,
borderTopRightRadius: index === 9 ? theme.radius.md : 0,
borderBottomRightRadius: index === 9 ? theme.radius.md : 0,
},
}}
>
<Stack align="center" gap={4}>
<Text visibleFrom="md" fw={500} size="lg">
{index}
</Text>
<Text visibleFrom="md" fw={500} size="xs" tt="uppercase">
{color}
</Text>
</Stack>
</ColorSwatch>
))}
</Group>
);
};
@@ -0,0 +1,7 @@
"use client";
// TODO: add some sort of store (maybe directory on GitHub)
export const CustomCssSettingsContent = () => {
return null;
};
@@ -0,0 +1,157 @@
"use client";
import { useCallback } from "react";
import { useRouter } from "next/navigation";
import { Button, Divider, Group, Stack, Text } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import { useConfirmModal, useModalAction } from "@homarr/modals";
import { useScopedI18n } from "@homarr/translation/client";
import { BoardRenameModal } from "~/components/board/modals/board-rename-modal";
import { useRequiredBoard } from "../../(content)/_context";
import classes from "./danger.module.css";
export const DangerZoneSettingsContent = () => {
const board = useRequiredBoard();
const t = useScopedI18n("board.setting");
const router = useRouter();
const { openConfirmModal } = useConfirmModal();
const { openModal } = useModalAction(BoardRenameModal);
const { mutate: changeVisibility, isPending: isChangeVisibilityPending } =
clientApi.board.changeBoardVisibility.useMutation();
const { mutate: deleteBoard, isPending: isDeletePending } =
clientApi.board.deleteBoard.useMutation();
const utils = clientApi.useUtils();
const visibility = board.isPublic ? "public" : "private";
const onRenameClick = useCallback(
() =>
openModal({
id: board.id,
previousName: board.name,
onSuccess: (name) => router.push(`/boards/${name}/settings`),
}),
[board.id, board.name, router, openModal],
);
const onVisibilityClick = useCallback(() => {
openConfirmModal({
title: t(
`section.dangerZone.action.visibility.confirm.${visibility}.title`,
),
children: t(
`section.dangerZone.action.visibility.confirm.${visibility}.description`,
),
onConfirm: () => {
changeVisibility(
{
id: board.id,
visibility: visibility === "public" ? "private" : "public",
},
{
onSettled() {
void utils.board.getBoardByName.invalidate({ name: board.name });
void utils.board.getDefaultBoard.invalidate();
},
},
);
},
});
}, [
board.id,
board.name,
changeVisibility,
t,
utils.board.getBoardByName,
utils.board.getDefaultBoard,
visibility,
openConfirmModal,
]);
const onDeleteClick = useCallback(() => {
openConfirmModal({
title: t("section.dangerZone.action.delete.confirm.title"),
children: t("section.dangerZone.action.delete.confirm.description"),
onConfirm: () => {
deleteBoard(
{ id: board.id },
{
onSettled: () => {
router.push("/");
},
},
);
},
});
}, [board.id, deleteBoard, router, t, openConfirmModal]);
return (
<Stack gap="sm">
<Divider />
<DangerZoneRow
label={t("section.dangerZone.action.rename.label")}
description={t("section.dangerZone.action.rename.description")}
buttonText={t("section.dangerZone.action.rename.button")}
onClick={onRenameClick}
/>
<Divider />
<DangerZoneRow
label={t("section.dangerZone.action.visibility.label")}
description={t(
`section.dangerZone.action.visibility.description.${visibility}`,
)}
buttonText={t(
`section.dangerZone.action.visibility.button.${visibility}`,
)}
onClick={onVisibilityClick}
isPending={isChangeVisibilityPending}
/>
<Divider />
<DangerZoneRow
label={t("section.dangerZone.action.delete.label")}
description={t("section.dangerZone.action.delete.description")}
buttonText={t("section.dangerZone.action.delete.button")}
onClick={onDeleteClick}
isPending={isDeletePending}
/>
</Stack>
);
};
interface DangerZoneRowProps {
label: string;
description: string;
buttonText: string;
isPending?: boolean;
onClick: () => void;
}
const DangerZoneRow = ({
label,
description,
buttonText,
onClick,
isPending,
}: DangerZoneRowProps) => {
return (
<Group justify="space-between" px="md" className={classes.dangerZoneGroup}>
<Stack gap={0}>
<Text fw="bold" size="sm">
{label}
</Text>
<Text size="sm">{description}</Text>
</Stack>
<Group justify="end" w={{ base: "100%", xs: "auto" }}>
<Button
variant="subtle"
color="red"
loading={isPending}
onClick={onClick}
>
{buttonText}
</Button>
</Group>
</Group>
);
};
@@ -0,0 +1,197 @@
"use client";
import { useEffect, useRef } from "react";
import {
Button,
Grid,
Group,
Loader,
Stack,
TextInput,
Tooltip,
} from "@mantine/core";
import {
useDebouncedValue,
useDocumentTitle,
useFavicon,
} from "@mantine/hooks";
import { IconAlertTriangle } from "@tabler/icons-react";
import { useForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";
import type { Board } from "../../_types";
import { useUpdateBoard } from "../../(content)/_client";
import { useSavePartialSettingsMutation } from "./_shared";
interface Props {
board: Board;
}
export const GeneralSettingsContent = ({ board }: Props) => {
const t = useI18n();
const ref = useRef({
pageTitle: board.pageTitle,
logoImageUrl: board.logoImageUrl,
});
const { updateBoard } = useUpdateBoard();
const { mutate: savePartialSettings, isPending } =
useSavePartialSettingsMutation(board);
const form = useForm({
initialValues: {
pageTitle: board.pageTitle ?? "",
logoImageUrl: board.logoImageUrl ?? "",
metaTitle: board.metaTitle ?? "",
faviconImageUrl: board.faviconImageUrl ?? "",
},
onValuesChange({ pageTitle }) {
updateBoard((previous) => ({
...previous,
pageTitle,
}));
},
});
const metaTitleStatus = useMetaTitlePreview(form.values.metaTitle);
const faviconStatus = useFaviconPreview(form.values.faviconImageUrl);
const logoStatus = useLogoPreview(form.values.logoImageUrl);
// Cleanup for not applied changes of the page title and logo image URL
useEffect(() => {
return () => {
updateBoard((previous) => ({
...previous,
pageTitle: ref.current.pageTitle,
logoImageUrl: ref.current.logoImageUrl,
}));
};
}, [updateBoard]);
return (
<form
onSubmit={form.onSubmit((values) => {
// Save the current values to the ref so that it does not reset if the form is submitted
ref.current = {
pageTitle: values.pageTitle,
logoImageUrl: values.logoImageUrl,
};
savePartialSettings({
id: board.id,
...values,
});
})}
>
<Stack>
<Grid>
<Grid.Col span={{ xs: 12, md: 6 }}>
<TextInput
label={t("board.field.pageTitle.label")}
placeholder="Homarr"
{...form.getInputProps("pageTitle")}
/>
</Grid.Col>
<Grid.Col span={{ xs: 12, md: 6 }}>
<TextInput
label={t("board.field.metaTitle.label")}
placeholder="Default Board | Homarr"
rightSection={<PendingOrInvalidIndicator {...metaTitleStatus} />}
{...form.getInputProps("metaTitle")}
/>
</Grid.Col>
<Grid.Col span={{ xs: 12, md: 6 }}>
<TextInput
label={t("board.field.logoImageUrl.label")}
placeholder="/logo/logo.png"
rightSection={<PendingOrInvalidIndicator {...logoStatus} />}
{...form.getInputProps("logoImageUrl")}
/>
</Grid.Col>
<Grid.Col span={{ xs: 12, md: 6 }}>
<TextInput
label={t("board.field.faviconImageUrl.label")}
placeholder="/logo/logo.png"
rightSection={<PendingOrInvalidIndicator {...faviconStatus} />}
{...form.getInputProps("faviconImageUrl")}
/>
</Grid.Col>
</Grid>
<Group justify="end">
<Button type="submit" loading={isPending} color="teal">
{t("common.action.saveChanges")}
</Button>
</Group>
</Stack>
</form>
);
};
const PendingOrInvalidIndicator = ({
isPending,
isInvalid,
}: {
isPending: boolean;
isInvalid?: boolean;
}) => {
const t = useI18n();
if (isInvalid) {
return (
<Tooltip
multiline
w={220}
label={t("board.setting.section.general.unrecognizedLink")}
>
<IconAlertTriangle size="1rem" color="red" />
</Tooltip>
);
}
if (isPending) {
return <Loader size="xs" />;
}
return null;
};
const useLogoPreview = (url: string | null) => {
const { updateBoard } = useUpdateBoard();
const [logoDebounced] = useDebouncedValue(url ?? "", 500);
useEffect(() => {
if (!logoDebounced.includes(".") && logoDebounced.length >= 1) return;
updateBoard((previous) => ({
...previous,
logoImageUrl: logoDebounced.length >= 1 ? logoDebounced : null,
}));
}, [logoDebounced, updateBoard]);
return {
isPending: (url ?? "") !== logoDebounced,
isInvalid: logoDebounced.length >= 1 && !logoDebounced.includes("."),
};
};
const useMetaTitlePreview = (title: string | null) => {
const [metaTitleDebounced] = useDebouncedValue(title ?? "", 200);
useDocumentTitle(metaTitleDebounced);
return {
isPending: (title ?? "") !== metaTitleDebounced,
};
};
const validFaviconExtensions = ["ico", "png", "svg", "gif"];
const isValidUrl = (url: string) =>
url.includes("/") &&
validFaviconExtensions.some((extension) => url.endsWith(`.${extension}`));
const useFaviconPreview = (url: string | null) => {
const [faviconDebounced] = useDebouncedValue(url ?? "", 500);
useFavicon(isValidUrl(faviconDebounced) ? faviconDebounced : "");
return {
isPending: (url ?? "") !== faviconDebounced,
isInvalid: faviconDebounced.length >= 1 && !isValidUrl(faviconDebounced),
};
};
@@ -0,0 +1,55 @@
"use client";
import { Button, Grid, Group, Input, Slider, Stack } from "@mantine/core";
import { useForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";
import type { Board } from "../../_types";
import { useSavePartialSettingsMutation } from "./_shared";
interface Props {
board: Board;
}
export const LayoutSettingsContent = ({ board }: Props) => {
const t = useI18n();
const { mutate: savePartialSettings, isPending } =
useSavePartialSettingsMutation(board);
const form = useForm({
initialValues: {
columnCount: board.columnCount,
},
});
return (
<form
onSubmit={form.onSubmit((values) => {
savePartialSettings({
id: board.id,
...values,
});
})}
>
<Stack>
<Grid>
<Grid.Col span={{ sm: 12, md: 6 }}>
<Input.Wrapper label={t("board.field.columnCount.label")}>
<Slider
mt="xs"
min={1}
max={24}
step={1}
{...form.getInputProps("columnCount")}
/>
</Input.Wrapper>
</Grid.Col>
</Grid>
<Group justify="end">
<Button type="submit" loading={isPending} color="teal">
{t("common.action.saveChanges")}
</Button>
</Group>
</Stack>
</form>
);
};
@@ -0,0 +1,13 @@
import { clientApi } from "@homarr/api/client";
import type { Board } from "../../_types";
export const useSavePartialSettingsMutation = (board: Board) => {
const utils = clientApi.useUtils();
return clientApi.board.savePartialBoardSettings.useMutation({
onSettled() {
void utils.board.getBoardByName.invalidate({ name: board.name });
void utils.board.getDefaultBoard.invalidate();
},
});
};
@@ -0,0 +1,5 @@
@media (min-width: 36em) {
.dangerZoneGroup {
--group-wrap: nowrap !important;
}
}
@@ -0,0 +1,172 @@
import type { PropsWithChildren } from "react";
import { notFound } from "next/navigation";
import {
AccordionControl,
AccordionItem,
AccordionPanel,
Container,
Stack,
Text,
Title,
} from "@mantine/core";
import {
IconAlertTriangle,
IconBrush,
IconFileTypeCss,
IconLayout,
IconPhoto,
IconSettings,
IconUser,
} from "@tabler/icons-react";
import { TRPCError } from "@trpc/server";
import { api } from "@homarr/api/server";
import { capitalize } from "@homarr/common";
import type { TranslationObject } from "@homarr/translation";
import { getScopedI18n } from "@homarr/translation/server";
import type { TablerIcon } from "@homarr/ui";
import { getBoardPermissions } from "~/components/board/permissions/server";
import { ActiveTabAccordion } from "../../../../../components/active-tab-accordion";
import { AccessSettingsContent } from "./_access";
import { BackgroundSettingsContent } from "./_background";
import { ColorSettingsContent } from "./_colors";
import { CustomCssSettingsContent } from "./_customCss";
import { DangerZoneSettingsContent } from "./_danger";
import { GeneralSettingsContent } from "./_general";
import { LayoutSettingsContent } from "./_layout";
interface Props {
params: {
name: string;
};
searchParams: {
tab?: keyof TranslationObject["board"]["setting"]["section"];
};
}
const getBoardAndPermissions = async (params: Props["params"]) => {
try {
const board = await api.board.getBoardByName({ name: params.name });
const { hasFullAccess } = await getBoardPermissions(board);
const permissions = hasFullAccess
? await api.board.getBoardPermissions({ id: board.id })
: {
userPermissions: [],
groupPermissions: [],
inherited: [],
};
return { board, permissions };
} catch (error) {
// Ignore not found errors and redirect to 404
// error is already logged in _layout-creator.tsx
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
notFound();
}
throw error;
}
};
export default async function BoardSettingsPage({
params,
searchParams,
}: Props) {
const { board, permissions } = await getBoardAndPermissions(params);
const { hasFullAccess } = await getBoardPermissions(board);
const t = await getScopedI18n("board.setting");
return (
<Container>
<Stack>
<Title>{t("title", { boardName: capitalize(board.name) })}</Title>
<ActiveTabAccordion
variant="separated"
defaultValue={searchParams.tab ?? "general"}
>
<AccordionItemFor value="general" icon={IconSettings}>
<GeneralSettingsContent board={board} />
</AccordionItemFor>
<AccordionItemFor value="layout" icon={IconLayout}>
<LayoutSettingsContent board={board} />
</AccordionItemFor>
<AccordionItemFor value="background" icon={IconPhoto}>
<BackgroundSettingsContent board={board} />
</AccordionItemFor>
<AccordionItemFor value="color" icon={IconBrush}>
<ColorSettingsContent board={board} />
</AccordionItemFor>
<AccordionItemFor value="customCss" icon={IconFileTypeCss}>
<CustomCssSettingsContent />
</AccordionItemFor>
{hasFullAccess && (
<>
<AccordionItemFor value="access" icon={IconUser}>
<AccessSettingsContent
board={board}
initialPermissions={permissions}
/>
</AccordionItemFor>
<AccordionItemFor
value="dangerZone"
icon={IconAlertTriangle}
danger
noPadding
>
<DangerZoneSettingsContent />
</AccordionItemFor>
</>
)}
</ActiveTabAccordion>
</Stack>
</Container>
);
}
type AccordionItemForProps = PropsWithChildren<{
value: keyof TranslationObject["board"]["setting"]["section"];
icon: TablerIcon;
danger?: boolean;
noPadding?: boolean;
}>;
const AccordionItemFor = async ({
value,
children,
icon: Icon,
danger,
noPadding,
}: AccordionItemForProps) => {
const t = await getScopedI18n("board.setting.section");
return (
<AccordionItem
value={value}
styles={
danger
? {
item: {
"--__item-border-color": "rgba(248,81,73,0.4)",
borderWidth: 4,
},
}
: undefined
}
>
<AccordionControl icon={<Icon />}>
<Text fw="bold" size="lg">
{t(`${value}.title`)}
</Text>
</AccordionControl>
<AccordionPanel
styles={
noPadding
? { content: { paddingRight: 0, paddingLeft: 0 } }
: undefined
}
>
{children}
</AccordionPanel>
</AccordionItem>
);
};
@@ -0,0 +1,16 @@
"use client";
import { IconLayoutBoard } from "@tabler/icons-react";
import { HeaderButton } from "~/components/layout/header/button";
import { useRequiredBoard } from "./(content)/_context";
export const BoardOtherHeaderActions = () => {
const board = useRequiredBoard();
return (
<HeaderButton href={`/boards/${board.name}`}>
<IconLayoutBoard stroke={1.5} />
</HeaderButton>
);
};
@@ -0,0 +1,65 @@
import type { PropsWithChildren } from "react";
import { notFound } from "next/navigation";
import { AppShellMain } from "@mantine/core";
import { TRPCError } from "@trpc/server";
import { logger } from "@homarr/log";
import { GlobalItemServerDataRunner } from "@homarr/widgets";
import { MainHeader } from "~/components/layout/header";
import { BoardLogoWithTitle } from "~/components/layout/logo/board-logo";
import { ClientShell } from "~/components/layout/shell";
import type { Board } from "./_types";
import { BoardProvider } from "./(content)/_context";
import type { Params } from "./(content)/_creator";
import { BoardMantineProvider } from "./(content)/_theme";
interface CreateBoardLayoutProps<TParams extends Params> {
headerActions: JSX.Element;
getInitialBoard: (params: TParams) => Promise<Board>;
isBoardContentPage: boolean;
}
export const createBoardLayout = <TParams extends Params>({
headerActions,
getInitialBoard,
isBoardContentPage,
}: CreateBoardLayoutProps<TParams>) => {
const Layout = async ({
params,
children,
}: PropsWithChildren<{
params: TParams;
}>) => {
const initialBoard = await getInitialBoard(params).catch((error) => {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
logger.warn(error);
notFound();
}
throw error;
});
return (
<GlobalItemServerDataRunner
board={initialBoard}
shouldRun={isBoardContentPage}
>
<BoardProvider initialBoard={initialBoard}>
<BoardMantineProvider>
<ClientShell hasNavigation={false}>
<MainHeader
logo={<BoardLogoWithTitle size="md" hideTitleOnMobile />}
actions={headerActions}
hasNavigation={false}
/>
<AppShellMain>{children}</AppShellMain>
</ClientShell>
</BoardMantineProvider>
</BoardProvider>
</GlobalItemServerDataRunner>
);
};
return Layout;
};
@@ -0,0 +1,14 @@
import type { RouterOutputs } from "@homarr/api";
import type { WidgetKind } from "@homarr/definitions";
export type Board = RouterOutputs["board"]["getDefaultBoard"];
export type Section = Board["sections"][number];
export type Item = Section["items"][number];
export type CategorySection = Extract<Section, { kind: "category" }>;
export type EmptySection = Extract<Section, { kind: "empty" }>;
export type ItemOfKind<TKind extends WidgetKind> = Extract<
Item,
{ kind: TKind }
>;
+18
View File
@@ -0,0 +1,18 @@
import React from "react";
type PropsWithChildren = Required<React.PropsWithChildren>;
export const composeWrappers = (
wrappers: React.FunctionComponent<PropsWithChildren>[],
): React.FunctionComponent<PropsWithChildren> => {
return wrappers
.reverse()
.reduce((Acc, Current): React.FunctionComponent<PropsWithChildren> => {
// eslint-disable-next-line react/display-name
return (props) => (
<Current>
<Acc {...props} />
</Current>
);
});
};
@@ -0,0 +1,80 @@
"use client";
import { useRouter } from "next/navigation";
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import { useForm, zodResolver } from "@homarr/form";
import {
showErrorNotification,
showSuccessNotification,
} from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
export const InitUserForm = () => {
const router = useRouter();
const t = useScopedI18n("user");
const { mutateAsync, error, isPending } =
clientApi.user.initUser.useMutation();
const form = useForm<FormType>({
validate: zodResolver(validation.user.init),
validateInputOnBlur: true,
validateInputOnChange: true,
initialValues: {
username: "",
password: "",
confirmPassword: "",
},
});
const handleSubmit = async (values: FormType) => {
await mutateAsync(values, {
onSuccess: () => {
showSuccessNotification({
title: "User created",
message: "You can now log in",
});
router.push("/auth/login");
},
onError: () => {
showErrorNotification({
title: "User creation failed",
message: error?.message ?? "Unknown error",
});
},
});
};
return (
<Stack gap="xl">
<form
onSubmit={form.onSubmit(
(values) => void handleSubmit(values),
(err) => console.log(err),
)}
>
<Stack gap="lg">
<TextInput
label={t("field.username.label")}
{...form.getInputProps("username")}
/>
<PasswordInput
label={t("field.password.label")}
{...form.getInputProps("password")}
/>
<PasswordInput
label={t("field.passwordConfirm.label")}
{...form.getInputProps("confirmPassword")}
/>
<Button type="submit" fullWidth loading={isPending}>
{t("action.create")}
</Button>
</Stack>
</form>
</Stack>
);
};
type FormType = z.infer<typeof validation.user.init>;
@@ -0,0 +1,41 @@
import { notFound } from "next/navigation";
import { Card, Center, Stack, Text, Title } from "@mantine/core";
import { db } from "@homarr/db";
import { getScopedI18n } from "@homarr/translation/server";
import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo";
import { InitUserForm } from "./_init-user-form";
export default async function InitUser() {
const firstUser = await db.query.users.findFirst({
columns: {
id: true,
},
});
if (firstUser) {
notFound();
}
const t = await getScopedI18n("user.page.init");
return (
<Center>
<Stack align="center" mt="xl">
<HomarrLogoWithTitle size="lg" />
<Stack gap={6} align="center">
<Title order={3} fw={400} ta="center">
{t("title")}
</Title>
<Text size="sm" c="gray.5" ta="center">
{t("subtitle")}
</Text>
</Stack>
<Card bg="dark.8" w={64 * 6} maw="90vw">
<InitUserForm />
</Card>
</Stack>
</Center>
);
}
+93
View File
@@ -0,0 +1,93 @@
import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import "@homarr/notifications/styles.css";
import "@homarr/spotlight/styles.css";
import "@mantine/core/styles.css";
import { ColorSchemeScript, createTheme, MantineProvider } from "@mantine/core";
import { auth } from "@homarr/auth/next";
import { ModalProvider } from "@homarr/modals";
import { Notifications } from "@homarr/notifications";
import { JotaiProvider } from "./_client-providers/jotai";
import { NextInternationalProvider } from "./_client-providers/next-international";
import { AuthProvider } from "./_client-providers/session";
import { TRPCReactProvider } from "./_client-providers/trpc";
import { composeWrappers } from "./compose";
const fontSans = Inter({
subsets: ["latin"],
variable: "--font-sans",
});
export const metadata: Metadata = {
metadataBase: new URL("http://localhost:3000"),
title: "Homarr",
description:
"Simplify the management of your server with Homarr - a sleek, modern dashboard that puts all of your apps and services at your fingertips.",
openGraph: {
title: "Homarr Dashboard",
description:
"Simplify the management of your server with Homarr - a sleek, modern dashboard that puts all of your apps and services at your fingertips.",
url: "https://homarr.dev",
siteName: "Homarr Documentation",
},
twitter: {
card: "summary_large_image",
site: "@jullerino",
creator: "@jullerino",
},
};
export const viewport: Viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "white" },
{ media: "(prefers-color-scheme: dark)", color: "black" },
],
};
export default function Layout(props: {
children: React.ReactNode;
params: { locale: string };
}) {
const colorScheme = "dark";
const StackedProvider = composeWrappers([
async (innerProps) => {
const session = await auth();
return <AuthProvider session={session} {...innerProps} />;
},
(innerProps) => <JotaiProvider {...innerProps} />,
(innerProps) => <TRPCReactProvider {...innerProps} />,
(innerProps) => (
<NextInternationalProvider {...innerProps} locale={props.params.locale} />
),
(innerProps) => (
<MantineProvider
{...innerProps}
defaultColorScheme="dark"
theme={createTheme({
primaryColor: "red",
autoContrast: true,
})}
/>
),
(innerProps) => <ModalProvider {...innerProps} />,
]);
return (
<html lang="en" suppressHydrationWarning>
<head>
<ColorSchemeScript defaultColorScheme={colorScheme} />
</head>
<body className={["font-sans", fontSans.variable].join(" ")}>
<StackedProvider>
<Notifications />
{props.children}
</StackedProvider>
</body>
</html>
);
}
+9
View File
@@ -0,0 +1,9 @@
import { Center, Loader } from "@mantine/core";
export default function CommonLoading() {
return (
<Center h="100vh">
<Loader />
</Center>
);
}
@@ -0,0 +1,17 @@
import { Center, Stack, Text, Title } from "@mantine/core";
import { getScopedI18n } from "@homarr/translation/server";
export default async function NotFound() {
const t = await getScopedI18n("management.notFound");
return (
<Center h="100%">
<Stack align="center">
<Title order={1} tt="uppercase">
{t("title")}
</Title>
<Text>{t("text")}</Text>
</Stack>
</Center>
);
}
@@ -0,0 +1,36 @@
.root {
border-radius: var(--mantine-radius-sm);
background-color: light-dark(
var(--mantine-color-gray-0),
var(--mantine-color-dark-6)
);
}
.item {
background-color: light-dark(
var(--mantine-color-gray-0),
var(--mantine-color-dark-6)
);
border: 1px solid transparent;
position: relative;
z-index: 0;
transition: transform 150ms ease;
&[data-active] {
transform: scale(1.015);
z-index: 1;
background-color: var(--mantine-color-body);
border-color: light-dark(
var(--mantine-color-gray-2),
var(--mantine-color-dark-4)
);
box-shadow: var(--mantine-shadow-md);
border-radius: var(--mantine-radius-md);
}
}
.chevron {
&[data-rotate] {
transform: rotate(-90deg);
}
}
@@ -0,0 +1,109 @@
import Image from "next/image";
import {
Accordion,
AccordionControl,
AccordionItem,
AccordionPanel,
Center,
Group,
List,
ListItem,
Stack,
Text,
Title,
} from "@mantine/core";
import { IconLanguage, IconLibrary, IconUsers } from "@tabler/icons-react";
import { setStaticParamsLocale } from "next-international/server";
import { getScopedI18n, getStaticParams } from "@homarr/translation/server";
import { getPackageAttributesAsync } from "~/versions/package-reader";
import logo from "../../../../../public/logo/logo.png";
import classes from "./accordion.module.css";
export async function generateMetadata() {
const t = await getScopedI18n("management");
const metaTitle = `${t("metaTitle")} • Homarr`;
return {
title: metaTitle,
};
}
interface PageProps {
params: {
locale: string;
};
}
export default async function AboutPage({ params: { locale } }: PageProps) {
setStaticParamsLocale(locale);
const t = await getScopedI18n("management.page.about");
const attributes = await getPackageAttributesAsync();
return (
<div>
<Center w="100%">
<Group py="lg">
<Image src={logo} width={100} height={100} alt="" />
<Stack gap={0}>
<Title order={1} tt="uppercase">
Homarr
</Title>
<Title order={2}>
{t("version", { version: attributes.version })}
</Title>
</Stack>
</Group>
</Center>
<Text mb="xl">{t("text")}</Text>
<Accordion classNames={classes}>
<AccordionItem value="contributors">
<AccordionControl icon={<IconUsers size="1rem" />}>
{t("accordion.contributors.title")}
</AccordionControl>
<AccordionPanel></AccordionPanel>
</AccordionItem>
<AccordionItem value="translators">
<AccordionControl icon={<IconLanguage size="1rem" />}>
{t("accordion.translators.title")}
</AccordionControl>
<AccordionPanel></AccordionPanel>
</AccordionItem>
<AccordionItem value="libraries">
<AccordionControl icon={<IconLibrary size="1rem" />}>
<Stack gap={0}>
<Text>{t("accordion.libraries.title")}</Text>
<Text size="sm" c="dimmed">
{t("accordion.libraries.subtitle", {
count: Object.keys(attributes.dependencies).length,
})}
</Text>
</Stack>
</AccordionControl>
<AccordionPanel>
<List>
{Object.entries(attributes.dependencies)
.sort(([key1], [key2]) => key1.localeCompare(key2))
.map(([key, value]) => (
<ListItem key={key}>
{value.includes("workspace:") ? (
<Text>{key}</Text>
) : (
<a href={`https://www.npmjs.com/package/${key}`}>{key}</a>
)}
</ListItem>
))}
</List>
</AccordionPanel>
</AccordionItem>
</Accordion>
</div>
);
}
export function generateStaticParams() {
return getStaticParams();
}
export const dynamic = "force-static";
@@ -0,0 +1,65 @@
"use client";
import { useCallback } from "react";
import { ActionIcon } from "@mantine/core";
import { IconTrash } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useConfirmModal } from "@homarr/modals";
import {
showErrorNotification,
showSuccessNotification,
} from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { revalidatePathAction } from "../../../revalidatePathAction";
interface AppDeleteButtonProps {
app: RouterOutputs["app"]["all"][number];
}
export const AppDeleteButton = ({ app }: AppDeleteButtonProps) => {
const t = useScopedI18n("app.page.delete");
const { openConfirmModal } = useConfirmModal();
const { mutate, isPending } = clientApi.app.delete.useMutation();
const onClick = useCallback(() => {
openConfirmModal({
title: t("title"),
children: t("message", app),
onConfirm: () => {
mutate(
{ id: app.id },
{
onSuccess: () => {
showSuccessNotification({
title: t("notification.success.title"),
message: t("notification.success.message"),
});
void revalidatePathAction("/manage/apps");
},
onError: () => {
showErrorNotification({
title: t("notification.error.title"),
message: t("notification.error.message"),
});
},
},
);
},
});
}, [app, mutate, t, openConfirmModal]);
return (
<ActionIcon
loading={isPending}
variant="subtle"
color="red"
onClick={onClick}
aria-label="Delete app"
>
<IconTrash color="red" size={16} stroke={1.5} />
</ActionIcon>
);
};
@@ -0,0 +1,60 @@
"use client";
import Link from "next/link";
import { Button, Group, Stack, Textarea, TextInput } from "@mantine/core";
import { useForm, zodResolver } from "@homarr/form";
import type { TranslationFunction } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
// TODO: add icon picker
type FormType = z.infer<typeof validation.app.manage>;
interface AppFormProps {
submitButtonTranslation: (t: TranslationFunction) => string;
initialValues?: FormType;
handleSubmit: (values: FormType) => void;
isPending: boolean;
}
export const AppForm = (props: AppFormProps) => {
const { submitButtonTranslation, handleSubmit, initialValues, isPending } =
props;
const t = useI18n();
const form = useForm({
initialValues: initialValues ?? {
name: "",
description: "",
iconUrl: "",
href: "",
},
validate: zodResolver(validation.app.manage),
});
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack>
<TextInput {...form.getInputProps("name")} withAsterisk label="Name" />
<TextInput
{...form.getInputProps("iconUrl")}
withAsterisk
label="Icon URL"
/>
<Textarea {...form.getInputProps("description")} label="Description" />
<TextInput {...form.getInputProps("href")} label="URL" />
<Group justify="end">
<Button variant="default" component={Link} href="/manage/apps">
{t("common.action.backToOverview")}
</Button>
<Button type="submit" loading={isPending}>
{submitButtonTranslation(t)}
</Button>
</Group>
</Stack>
</form>
);
};
@@ -0,0 +1,68 @@
"use client";
import { useCallback } from "react";
import { useRouter } from "next/navigation";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import {
showErrorNotification,
showSuccessNotification,
} from "@homarr/notifications";
import type { TranslationFunction } from "@homarr/translation";
import { useScopedI18n } from "@homarr/translation/client";
import type { validation, z } from "@homarr/validation";
import { revalidatePathAction } from "~/app/revalidatePathAction";
import { AppForm } from "../../_form";
interface AppEditFormProps {
app: RouterOutputs["app"]["byId"];
}
export const AppEditForm = ({ app }: AppEditFormProps) => {
const t = useScopedI18n("app.page.edit.notification");
const router = useRouter();
const { mutate, isPending } = clientApi.app.update.useMutation({
onSuccess: () => {
showSuccessNotification({
title: t("success.title"),
message: t("success.message"),
});
void revalidatePathAction("/manage/apps").then(() => {
router.push("/manage/apps");
});
},
onError: () => {
showErrorNotification({
title: t("error.title"),
message: t("error.message"),
});
},
});
const handleSubmit = useCallback(
(values: z.infer<typeof validation.app.manage>) => {
mutate({
id: app.id,
...values,
});
},
[mutate, app.id],
);
const submitButtonTranslation = useCallback(
(t: TranslationFunction) => t("common.action.save"),
[],
);
return (
<AppForm
submitButtonTranslation={submitButtonTranslation}
initialValues={app}
handleSubmit={handleSubmit}
isPending={isPending}
/>
);
};
@@ -0,0 +1,24 @@
import { Container, Stack, Title } from "@mantine/core";
import { api } from "@homarr/api/server";
import { getI18n } from "@homarr/translation/server";
import { AppEditForm } from "./_app-edit-form";
interface AppEditPageProps {
params: { id: string };
}
export default async function AppEditPage({ params }: AppEditPageProps) {
const app = await api.app.byId({ id: params.id });
const t = await getI18n();
return (
<Container>
<Stack>
<Title>{t("app.page.edit.title")}</Title>
<AppEditForm app={app} />
</Stack>
</Container>
);
}
@@ -0,0 +1,59 @@
"use client";
import { useCallback } from "react";
import { useRouter } from "next/navigation";
import { clientApi } from "@homarr/api/client";
import {
showErrorNotification,
showSuccessNotification,
} from "@homarr/notifications";
import type { TranslationFunction } from "@homarr/translation";
import { useScopedI18n } from "@homarr/translation/client";
import type { validation, z } from "@homarr/validation";
import { revalidatePathAction } from "~/app/revalidatePathAction";
import { AppForm } from "../_form";
export const AppNewForm = () => {
const t = useScopedI18n("app.page.create.notification");
const router = useRouter();
const { mutate, isPending } = clientApi.app.create.useMutation({
onSuccess: () => {
showSuccessNotification({
title: t("success.title"),
message: t("success.message"),
});
void revalidatePathAction("/manage/apps").then(() => {
router.push("/manage/apps");
});
},
onError: () => {
showErrorNotification({
title: t("error.title"),
message: t("error.message"),
});
},
});
const handleSubmit = useCallback(
(values: z.infer<typeof validation.app.manage>) => {
mutate(values);
},
[mutate],
);
const submitButtonTranslation = useCallback(
(t: TranslationFunction) => t("common.action.create"),
[],
);
return (
<AppForm
submitButtonTranslation={submitButtonTranslation}
handleSubmit={handleSubmit}
isPending={isPending}
/>
);
};
@@ -0,0 +1,14 @@
import { Container, Stack, Title } from "@mantine/core";
import { AppNewForm } from "./_app-new-form";
export default function AppNewPage() {
return (
<Container>
<Stack>
<Title>New app</Title>
<AppNewForm />
</Stack>
</Container>
);
}
@@ -0,0 +1,116 @@
import Link from "next/link";
import {
ActionIcon,
ActionIconGroup,
Anchor,
Avatar,
Button,
Card,
Container,
Group,
Stack,
Text,
Title,
} from "@mantine/core";
import { IconApps, IconPencil } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
import { getI18n } from "@homarr/translation/server";
import { AppDeleteButton } from "./_app-delete-button";
export default async function AppsPage() {
const apps = await api.app.all();
return (
<Container>
<Stack>
<Group justify="space-between" align="center">
<Title>Apps</Title>
<Button component={Link} href="/manage/apps/new">
New app
</Button>
</Group>
{apps.length === 0 && <AppNoResults />}
{apps.length > 0 && (
<Stack gap="sm">
{apps.map((app) => (
<AppCard key={app.id} app={app} />
))}
</Stack>
)}
</Stack>
</Container>
);
}
interface AppCardProps {
app: RouterOutputs["app"]["all"][number];
}
const AppCard = ({ app }: AppCardProps) => {
return (
<Card>
<Group justify="space-between">
<Group align="top" justify="start" wrap="nowrap">
<Avatar
size="sm"
src={app.iconUrl}
radius={0}
styles={{
image: {
objectFit: "contain",
},
}}
/>
<Stack gap={0}>
<Text fw={500}>{app.name}</Text>
{app.description && (
<Text size="sm" c="gray.6">
{app.description}
</Text>
)}
{app.href && (
<Anchor href={app.href} size="sm" w="min-content">
{app.href}
</Anchor>
)}
</Stack>
</Group>
<Group>
<ActionIconGroup>
<ActionIcon
component={Link}
href={`/manage/apps/edit/${app.id}`}
variant="subtle"
color="gray"
aria-label="Edit app"
>
<IconPencil size={16} stroke={1.5} />
</ActionIcon>
<AppDeleteButton app={app} />
</ActionIconGroup>
</Group>
</Group>
</Card>
);
};
const AppNoResults = async () => {
const t = await getI18n();
return (
<Card withBorder bg="transparent">
<Stack align="center" gap="sm">
<IconApps size="2rem" />
<Text fw={500} size="lg">
{t("app.page.list.noResults.title")}
</Text>
<Anchor href="/manage/apps/new">
{t("app.page.list.noResults.description")}
</Anchor>
</Stack>
</Card>
);
};
@@ -0,0 +1,92 @@
"use client";
import { useCallback } from "react";
import Link from "next/link";
import { Menu } from "@mantine/core";
import { IconSettings, IconTrash } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useConfirmModal } from "@homarr/modals";
import { useScopedI18n } from "@homarr/translation/client";
import { revalidatePathAction } from "~/app/revalidatePathAction";
import { useBoardPermissions } from "~/components/board/permissions/client";
const iconProps = {
size: 16,
stroke: 1.5,
};
interface BoardCardMenuDropdownProps {
board: Pick<
RouterOutputs["board"]["getAllBoards"][number],
| "id"
| "name"
| "creator"
| "userPermissions"
| "groupPermissions"
| "isPublic"
>;
}
export const BoardCardMenuDropdown = ({
board,
}: BoardCardMenuDropdownProps) => {
const t = useScopedI18n("management.page.board.action");
const tCommon = useScopedI18n("common");
const { hasFullAccess, hasChangeAccess } = useBoardPermissions(board);
const { openConfirmModal } = useConfirmModal();
const { mutateAsync, isPending } = clientApi.board.deleteBoard.useMutation({
onSettled: async () => {
await revalidatePathAction("/manage/boards");
},
});
const handleDeletion = useCallback(() => {
openConfirmModal({
title: t("delete.confirm.title"),
children: t("delete.confirm.description", {
name: board.name,
}),
onConfirm: async () => {
await mutateAsync({
id: board.id,
});
},
});
}, [board.id, board.name, mutateAsync, openConfirmModal, t]);
return (
<Menu.Dropdown>
{hasChangeAccess && (
<Menu.Item
component={Link}
href={`/boards/${board.name}/settings`}
leftSection={<IconSettings {...iconProps} />}
>
{t("settings.label")}
</Menu.Item>
)}
{hasFullAccess && (
<>
<Menu.Divider />
<Menu.Label c="red.7">
{tCommon("menu.section.dangerZone.title")}
</Menu.Label>
<Menu.Item
c="red.7"
leftSection={<IconTrash {...iconProps} />}
onClick={handleDeletion}
disabled={isPending}
>
{t("delete.label")}
</Menu.Item>
</>
)}
</Menu.Dropdown>
);
};
@@ -0,0 +1,83 @@
"use client";
import { Box, Card, Center, Flex, Loader, Text } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import dayjs from "dayjs";
import type { RouterOutputs } from "@homarr/api";
interface BoardPreviewProps {
board: RouterOutputs["board"]["getAllBoards"][number];
}
export const BoardPreview = ({ board }: BoardPreviewProps) => {
const { ref, width } = useElementSize();
return (
<Card
h="100%"
mah={width ? (width / 1920) * 1080 : undefined}
ref={ref}
styles={{
root: {
"--gridstack-column-count": board.columnCount,
"--gridstack-widget-width": width / board.columnCount,
},
}}
>
{width === 0 ? (
<Center h="100%">
<Loader size="sm" />
</Center>
) : (
<>
{board.sections.map((section) => (
<Section
key={section.id}
section={section}
itemWidth={width / board.columnCount}
/>
))}
</>
)}
</Card>
);
};
interface SectionProps {
section: RouterOutputs["board"]["getAllBoards"][number]["sections"][number];
itemWidth: number;
}
const Section = ({ section, itemWidth }: SectionProps) => {
const maxHeight = Math.max(
...section.items.map((item) => item.yOffset + item.height),
);
return (
<Card
withBorder={section.kind === "category"}
h={maxHeight * itemWidth}
key={section.id}
className="grid-stack"
>
{section.items.map((item) => (
<Box
className="grid-stack-item"
gs-h={item.height}
gs-w={item.width}
gs-x={item.xOffset}
gs-y={item.yOffset}
key={item.id}
p={4}
>
<Card withBorder h="100%" p={0}>
<Flex justify="center" align="center" h="100%" w="100%">
<Text fz={itemWidth / 4}>{dayjs().format("HH:mm")}</Text>
</Flex>
</Card>
</Box>
))}
</Card>
);
};
@@ -0,0 +1,48 @@
"use client";
import { useCallback } from "react";
import { Button } from "@mantine/core";
import { IconCategoryPlus } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useModalAction } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
import { revalidatePathAction } from "~/app/revalidatePathAction";
import { AddBoardModal } from "~/components/manage/boards/add-board-modal";
interface CreateBoardButtonProps {
boardNames: string[];
}
export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => {
const t = useI18n();
const { openModal } = useModalAction(AddBoardModal);
const { mutateAsync, isPending } = clientApi.board.createBoard.useMutation({
onSettled: async () => {
await revalidatePathAction("/manage/boards");
},
});
const onClick = useCallback(() => {
openModal({
onSuccess: async (values) => {
await mutateAsync({
name: values.name,
});
},
boardNames,
});
}, [mutateAsync, boardNames, openModal]);
return (
<Button
leftSection={<IconCategoryPlus size="1rem" />}
onClick={onClick}
loading={isPending}
>
{t("management.page.board.action.new.label")}
</Button>
);
};
@@ -0,0 +1,115 @@
import Link from "next/link";
import {
ActionIcon,
Button,
Card,
CardSection,
Grid,
GridCol,
Group,
Menu,
MenuTarget,
Text,
Title,
Tooltip,
} from "@mantine/core";
import { IconDotsVertical, IconLock, IconWorld } from "@tabler/icons-react";
import "~/styles/gridstack.scss";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
import { getScopedI18n } from "@homarr/translation/server";
import { UserAvatar } from "@homarr/ui";
import { getBoardPermissions } from "~/components/board/permissions/server";
import { BoardCardMenuDropdown } from "./_components/board-card-menu-dropdown";
import { BoardPreview } from "./_components/board-preview";
import { CreateBoardButton } from "./_components/create-board-button";
export default async function ManageBoardsPage() {
const t = await getScopedI18n("management.page.board");
const boards = await api.board.getAllBoards();
return (
<>
<Group justify="space-between">
<Title mb="md">{t("title")}</Title>
<CreateBoardButton boardNames={boards.map((board) => board.name)} />
</Group>
<Grid>
{boards.map((board) => (
<GridCol span={{ base: 12, md: 6, xl: 4 }} key={board.id}>
<BoardCard board={board} />
</GridCol>
))}
</Grid>
</>
);
}
interface BoardCardProps {
board: RouterOutputs["board"]["getAllBoards"][number];
}
const BoardCard = async ({ board }: BoardCardProps) => {
const t = await getScopedI18n("management.page.board");
const { hasChangeAccess: isMenuVisible } = await getBoardPermissions(board);
const visibility = board.isPublic ? "public" : "private";
const VisibilityIcon = board.isPublic ? IconWorld : IconLock;
return (
<Card>
<CardSection p="sm" withBorder>
<Group justify="space-between" align="center">
<Group gap="sm">
<Tooltip label={t(`visibility.${visibility}`)}>
<VisibilityIcon size={20} stroke={1.5} />
</Tooltip>
<Text fw="bolder" tt="uppercase">
{board.name}
</Text>
</Group>
{board.creator && (
<Group>
<Group gap="xs">
<UserAvatar user={board.creator} size="sm" />
<Text>{board.creator?.name}</Text>
</Group>
</Group>
)}
</Group>
</CardSection>
<CardSection p="sm" style={{ aspectRatio: 1920 / 1080 }}>
<BoardPreview board={board} />
</CardSection>
<CardSection p="sm">
<Group wrap="nowrap">
<Button
component={Link}
href={`/boards/${board.name}`}
variant="default"
fullWidth
>
{t("action.open.label")}
</Button>
{isMenuVisible && (
<Menu position="bottom-end">
<MenuTarget>
<ActionIcon variant="default" size="lg">
<IconDotsVertical size={16} stroke={1.5} />
</ActionIcon>
</MenuTarget>
<BoardCardMenuDropdown board={board} />
</Menu>
)}
</Group>
</CardSection>
</Card>
);
};
@@ -0,0 +1,19 @@
import { Avatar } from "@mantine/core";
import type { MantineSize } from "@mantine/core";
import { getIconUrl } from "@homarr/definitions";
import type { IntegrationKind } from "@homarr/definitions";
interface IntegrationAvatarProps {
size: MantineSize;
kind: IntegrationKind | null;
}
export const IntegrationAvatar = ({ kind, size }: IntegrationAvatarProps) => {
const url = kind ? getIconUrl(kind) : null;
if (!url) {
return null;
}
return <Avatar size={size} src={url} />;
};
@@ -0,0 +1,70 @@
"use client";
import { useRouter } from "next/navigation";
import { ActionIcon } from "@mantine/core";
import { IconTrash } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useConfirmModal } from "@homarr/modals";
import {
showErrorNotification,
showSuccessNotification,
} from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { revalidatePathAction } from "../../../revalidatePathAction";
interface DeleteIntegrationActionButtonProps {
count: number;
integration: { id: string; name: string };
}
export const DeleteIntegrationActionButton = ({
count,
integration,
}: DeleteIntegrationActionButtonProps) => {
const t = useScopedI18n("integration.page.delete");
const router = useRouter();
const { openConfirmModal } = useConfirmModal();
const { mutateAsync, isPending } = clientApi.integration.delete.useMutation();
return (
<ActionIcon
loading={isPending}
variant="subtle"
color="red"
onClick={() => {
openConfirmModal({
title: t("title"),
children: t("message", integration),
onConfirm: () => {
void mutateAsync(
{ id: integration.id },
{
onSuccess: () => {
showSuccessNotification({
title: t("notification.success.title"),
message: t("notification.success.message"),
});
if (count === 1) {
router.replace("/integrations");
}
void revalidatePathAction("/integrations");
},
onError: () => {
showErrorNotification({
title: t("notification.error.title"),
message: t("notification.error.message"),
});
},
},
);
},
});
}}
aria-label="Delete integration"
>
<IconTrash color="red" size={16} stroke={1.5} />
</ActionIcon>
);
};
@@ -0,0 +1,94 @@
"use client";
import { useState } from "react";
import { useParams } from "next/navigation";
import {
ActionIcon,
Avatar,
Button,
Card,
Collapse,
Group,
Kbd,
Stack,
Text,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { IconEye, IconEyeOff } from "@tabler/icons-react";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import type { RouterOutputs } from "@homarr/api";
import { integrationSecretKindObject } from "@homarr/definitions";
import { useI18n } from "@homarr/translation/client";
import { integrationSecretIcons } from "./_integration-secret-icons";
dayjs.extend(relativeTime);
interface SecretCardProps {
secret: RouterOutputs["integration"]["byId"]["secrets"][number];
children: React.ReactNode;
onCancel: () => Promise<boolean>;
}
export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => {
const params = useParams<{ locale: string }>();
const t = useI18n();
const { isPublic } = integrationSecretKindObject[secret.kind];
const [publicSecretDisplayOpened, { toggle: togglePublicSecretDisplay }] =
useDisclosure(false);
const [editMode, setEditMode] = useState(false);
const DisplayIcon = publicSecretDisplayOpened ? IconEye : IconEyeOff;
const KindIcon = integrationSecretIcons[secret.kind];
return (
<Card>
<Stack>
<Group justify="space-between">
<Group>
<Avatar>
<KindIcon size={16} />
</Avatar>
<Text fw={500}>
{t(`integration.secrets.kind.${secret.kind}.label`)}
</Text>
{publicSecretDisplayOpened ? <Kbd>{secret.value}</Kbd> : null}
</Group>
<Group>
<Text c="gray.6" size="sm">
{t("integration.secrets.lastUpdated", {
date: dayjs().locale(params.locale).to(dayjs(secret.updatedAt)),
})}
</Text>
{isPublic ? (
<ActionIcon
color="gray"
variant="subtle"
onClick={togglePublicSecretDisplay}
>
<DisplayIcon size={16} stroke={1.5} />
</ActionIcon>
) : null}
<Button
variant="default"
onClick={async () => {
if (!editMode) {
setEditMode(true);
return;
}
const shouldCancel = await onCancel();
if (!shouldCancel) return;
setEditMode(false);
}}
>
{editMode ? t("common.action.cancel") : t("common.action.edit")}
</Button>
</Group>
</Group>
<Collapse in={editMode}>{children}</Collapse>
</Stack>
</Card>
);
};
@@ -0,0 +1,10 @@
import { IconKey, IconPassword, IconUser } from "@tabler/icons-react";
import type { IntegrationSecretKind } from "@homarr/definitions";
import type { TablerIcon } from "@homarr/ui";
export const integrationSecretIcons = {
username: IconUser,
apiKey: IconKey,
password: IconPassword,
} satisfies Record<IntegrationSecretKind, TablerIcon>;
@@ -0,0 +1,60 @@
"use client";
import type { ChangeEventHandler, FocusEventHandler } from "react";
import { PasswordInput, TextInput } from "@mantine/core";
import { integrationSecretKindObject } from "@homarr/definitions";
import type { IntegrationSecretKind } from "@homarr/definitions";
import { useI18n } from "@homarr/translation/client";
import { integrationSecretIcons } from "./_integration-secret-icons";
interface IntegrationSecretInputProps {
label?: string;
kind: IntegrationSecretKind;
value?: string;
onChange: ChangeEventHandler<HTMLInputElement>;
onFocus?: FocusEventHandler<HTMLInputElement>;
onBlur?: FocusEventHandler<HTMLInputElement>;
error?: string;
}
export const IntegrationSecretInput = (props: IntegrationSecretInputProps) => {
const { isPublic } = integrationSecretKindObject[props.kind];
if (isPublic) return <PublicSecretInput {...props} />;
return <PrivateSecretInput {...props} />;
};
const PublicSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
const t = useI18n();
const Icon = integrationSecretIcons[kind];
return (
<TextInput
{...props}
label={props.label ?? t(`integration.secrets.kind.${kind}.label`)}
w="100%"
leftSection={<Icon size={20} stroke={1.5} />}
/>
);
};
const PrivateSecretInput = ({
kind,
...props
}: IntegrationSecretInputProps) => {
const t = useI18n();
const Icon = integrationSecretIcons[kind];
return (
<PasswordInput
{...props}
label={props.label ?? t(`integration.secrets.kind.${kind}.label`)}
description={t(`integration.secrets.secureNotice`)}
w="100%"
leftSection={<Icon size={20} stroke={1.5} />}
/>
);
};
@@ -0,0 +1,154 @@
"use client";
import { useRef, useState } from "react";
import { Alert, Anchor, Group, Loader } from "@mantine/core";
import { IconCheck, IconInfoCircle, IconX } from "@tabler/icons-react";
import type { RouterInputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import {
showErrorNotification,
showSuccessNotification,
} from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
interface UseTestConnectionDirtyProps {
defaultDirty: boolean;
initialFormValue: {
url: string;
secrets: { kind: string; value: string | null }[];
};
}
export const useTestConnectionDirty = ({
defaultDirty,
initialFormValue,
}: UseTestConnectionDirtyProps) => {
const [isDirty, setIsDirty] = useState(defaultDirty);
const prevFormValueRef = useRef(initialFormValue);
return {
onValuesChange: (values: typeof initialFormValue) => {
if (isDirty) return;
// If relevant values changed, set dirty
if (
prevFormValueRef.current.url !== values.url ||
!prevFormValueRef.current.secrets
.map((secret) => secret.value)
.every(
(secretValue, index) =>
values.secrets[index]?.value === secretValue,
)
) {
setIsDirty(true);
return;
}
// If relevant values changed back to last tested, set not dirty
setIsDirty(false);
},
isDirty,
removeDirty: () => {
prevFormValueRef.current = initialFormValue;
setIsDirty(false);
},
};
};
interface TestConnectionProps {
isDirty: boolean;
removeDirty: () => void;
integration: RouterInputs["integration"]["testConnection"] & { name: string };
}
export const TestConnection = ({
integration,
removeDirty,
isDirty,
}: TestConnectionProps) => {
const t = useScopedI18n("integration.testConnection");
const { mutateAsync, ...mutation } =
clientApi.integration.testConnection.useMutation();
return (
<Group>
<Anchor
type="button"
component="button"
onClick={async () => {
await mutateAsync(integration, {
onSuccess: () => {
removeDirty();
showSuccessNotification({
title: t("notification.success.title"),
message: t("notification.success.message"),
});
},
onError: (error) => {
if (error.data?.zodError?.fieldErrors.url) {
showErrorNotification({
title: t("notification.invalidUrl.title"),
message: t("notification.invalidUrl.message"),
});
return;
}
if (error.message === "SECRETS_NOT_DEFINED") {
showErrorNotification({
title: t("notification.notAllSecretsProvided.title"),
message: t("notification.notAllSecretsProvided.message"),
});
return;
}
showErrorNotification({
title: t("notification.commonError.title"),
message: t("notification.commonError.message"),
});
},
});
}}
>
{t("action")}
</Anchor>
<TestConnectionIcon isDirty={isDirty} {...mutation} size={20} />
</Group>
);
};
interface TestConnectionIconProps {
isDirty: boolean;
isPending: boolean;
isSuccess: boolean;
isError: boolean;
size: number;
}
const TestConnectionIcon = ({
isDirty,
isPending,
isSuccess,
isError,
size,
}: TestConnectionIconProps) => {
if (isPending) return <Loader color="blue" size={size} />;
if (isDirty) return null;
if (isSuccess) return <IconCheck size={size} stroke={1.5} color="green" />;
if (isError) return <IconX size={size} stroke={1.5} color="red" />;
return null;
};
export const TestConnectionNoticeAlert = () => {
const t = useI18n();
return (
<Alert
variant="light"
color="yellow"
title="Test Connection"
icon={<IconInfoCircle />}
>
{t("integration.testConnection.alertNotice")}
</Alert>
);
};
@@ -0,0 +1,180 @@
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Button, Fieldset, Group, Stack, TextInput } from "@mantine/core";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import {
getAllSecretKindOptions,
getDefaultSecretKinds,
} from "@homarr/definitions";
import { useForm, zodResolver } from "@homarr/form";
import { useConfirmModal } from "@homarr/modals";
import {
showErrorNotification,
showSuccessNotification,
} from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
import { revalidatePathAction } from "~/app/revalidatePathAction";
import { SecretCard } from "../../_integration-secret-card";
import { IntegrationSecretInput } from "../../_integration-secret-inputs";
import {
TestConnection,
TestConnectionNoticeAlert,
useTestConnectionDirty,
} from "../../_integration-test-connection";
interface EditIntegrationForm {
integration: RouterOutputs["integration"]["byId"];
}
export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
const t = useI18n();
const { openConfirmModal } = useConfirmModal();
const secretsKinds =
getAllSecretKindOptions(integration.kind).find((secretKinds) =>
integration.secrets.every((secret) => secretKinds.includes(secret.kind)),
) ?? getDefaultSecretKinds(integration.kind);
const initialFormValues = {
name: integration.name,
url: integration.url,
secrets: secretsKinds.map((kind) => ({
kind,
value:
integration.secrets.find((secret) => secret.kind === kind)?.value ?? "",
})),
};
const { isDirty, onValuesChange, removeDirty } = useTestConnectionDirty({
defaultDirty: true,
initialFormValue: initialFormValues,
});
const router = useRouter();
const form = useForm<FormType>({
initialValues: initialFormValues,
validate: zodResolver(validation.integration.update.omit({ id: true })),
onValuesChange,
});
const { mutateAsync, isPending } = clientApi.integration.update.useMutation();
const secretsMap = new Map(
integration.secrets.map((secret) => [secret.kind, secret]),
);
const handleSubmit = async (values: FormType) => {
if (isDirty) return;
await mutateAsync(
{
id: integration.id,
...values,
secrets: values.secrets.map((secret) => ({
kind: secret.kind,
value: secret.value === "" ? null : secret.value,
})),
},
{
onSuccess: () => {
showSuccessNotification({
title: t("integration.page.edit.notification.success.title"),
message: t("integration.page.edit.notification.success.message"),
});
void revalidatePathAction("/integrations").then(() =>
router.push("/integrations"),
);
},
onError: () => {
showErrorNotification({
title: t("integration.page.edit.notification.error.title"),
message: t("integration.page.edit.notification.error.message"),
});
},
},
);
};
return (
<form onSubmit={form.onSubmit((values) => void handleSubmit(values))}>
<Stack>
<TestConnectionNoticeAlert />
<TextInput
label={t("integration.field.name.label")}
{...form.getInputProps("name")}
/>
<TextInput
label={t("integration.field.url.label")}
{...form.getInputProps("url")}
/>
<Fieldset legend={t("integration.secrets.title")}>
<Stack gap="sm">
{secretsKinds.map((kind, index) => (
<SecretCard
key={kind}
secret={secretsMap.get(kind)!}
onCancel={() =>
new Promise((res) => {
// When nothing changed, just close the secret card
if (
(form.values.secrets[index]?.value ?? "") ===
(secretsMap.get(kind)?.value ?? "")
) {
return res(true);
}
openConfirmModal({
title: t("integration.secrets.reset.title"),
children: t("integration.secrets.reset.message"),
onCancel: () => res(false),
onConfirm: () => {
form.setFieldValue(
`secrets.${index}.value`,
secretsMap.get(kind)!.value ?? "",
);
res(true);
},
});
})
}
>
<IntegrationSecretInput
label={t(`integration.secrets.kind.${kind}.newLabel`)}
key={kind}
kind={kind}
{...form.getInputProps(`secrets.${index}.value`)}
/>
</SecretCard>
))}
</Stack>
</Fieldset>
<Group justify="space-between" align="center">
<TestConnection
isDirty={isDirty}
removeDirty={removeDirty}
integration={{
id: integration.id,
kind: integration.kind,
...form.values,
}}
/>
<Group>
<Button variant="default" component={Link} href="/integrations">
{t("common.action.backToOverview")}
</Button>
<Button type="submit" loading={isPending} disabled={isDirty}>
{t("common.action.save")}
</Button>
</Group>
</Group>
</Stack>
</form>
);
};
type FormType = Omit<z.infer<typeof validation.integration.update>, "id">;
@@ -0,0 +1,33 @@
import { Container, Group, Stack, Title } from "@mantine/core";
import { api } from "@homarr/api/server";
import { getIntegrationName } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { IntegrationAvatar } from "../../_integration-avatar";
import { EditIntegrationForm } from "./_integration-edit-form";
interface EditIntegrationPageProps {
params: { id: string };
}
export default async function EditIntegrationPage({
params,
}: EditIntegrationPageProps) {
const t = await getScopedI18n("integration.page.edit");
const integration = await api.integration.byId({ id: params.id });
return (
<Container>
<Stack>
<Group align="center">
<IntegrationAvatar kind={integration.kind} size="md" />
<Title>
{t("title", { name: getIntegrationName(integration.kind) })}
</Title>
</Group>
<EditIntegrationForm integration={integration} />
</Stack>
</Container>
);
}
@@ -0,0 +1,57 @@
"use client";
import React, { ChangeEvent, useMemo, useState } from "react";
import Link from "next/link";
import { Group, Menu, ScrollArea, Stack, Text, TextInput } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react";
import { getIntegrationName, integrationKinds } from "@homarr/definitions";
import { useI18n } from "@homarr/translation/client";
import { IntegrationAvatar } from "../_integration-avatar";
export const IntegrationCreateDropdownContent = () => {
const t = useI18n();
const [search, setSearch] = useState("");
const filteredKinds = useMemo(() => {
return integrationKinds.filter((kind) =>
kind.includes(search.toLowerCase()),
);
}, [search]);
const handleSearch = React.useCallback(
(event: ChangeEvent<HTMLInputElement>) => setSearch(event.target.value),
[setSearch],
);
return (
<Stack>
<TextInput
leftSection={<IconSearch stroke={1.5} size={20} />}
placeholder={t("integration.page.list.search")}
value={search}
onChange={handleSearch}
/>
{filteredKinds.length > 0 ? (
<ScrollArea.Autosize mah={384}>
{filteredKinds.map((kind) => (
<Menu.Item
component={Link}
href={`/integrations/new?kind=${kind}`}
key={kind}
>
<Group>
<IntegrationAvatar kind={kind} size="sm" />
<Text size="sm">{getIntegrationName(kind)}</Text>
</Group>
</Menu.Item>
))}
</ScrollArea.Autosize>
) : (
<Menu.Item disabled>{t("common.noResults")}</Menu.Item>
)}
</Stack>
);
};
@@ -0,0 +1,192 @@
"use client";
import { useCallback } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
Button,
Fieldset,
Group,
SegmentedControl,
Stack,
TextInput,
} from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import type {
IntegrationKind,
IntegrationSecretKind,
} from "@homarr/definitions";
import { getAllSecretKindOptions } from "@homarr/definitions";
import type { UseFormReturnType } from "@homarr/form";
import { useForm, zodResolver } from "@homarr/form";
import {
showErrorNotification,
showSuccessNotification,
} from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
import { IntegrationSecretInput } from "../_integration-secret-inputs";
import {
TestConnection,
TestConnectionNoticeAlert,
useTestConnectionDirty,
} from "../_integration-test-connection";
import { revalidatePathAction } from "../../../../revalidatePathAction";
interface NewIntegrationFormProps {
searchParams: Partial<z.infer<typeof validation.integration.create>> & {
kind: IntegrationKind;
};
}
export const NewIntegrationForm = ({
searchParams,
}: NewIntegrationFormProps) => {
const t = useI18n();
const secretKinds = getAllSecretKindOptions(searchParams.kind);
const initialFormValues = {
name: searchParams.name ?? "",
url: searchParams.url ?? "",
secrets: secretKinds[0].map((kind) => ({
kind,
value: "",
})),
};
const { isDirty, onValuesChange, removeDirty } = useTestConnectionDirty({
defaultDirty: true,
initialFormValue: initialFormValues,
});
const router = useRouter();
const form = useForm<FormType>({
initialValues: initialFormValues,
validate: zodResolver(validation.integration.create.omit({ kind: true })),
onValuesChange,
});
const { mutateAsync, isPending } = clientApi.integration.create.useMutation();
const handleSubmit = async (values: FormType) => {
if (isDirty) return;
await mutateAsync(
{
kind: searchParams.kind,
...values,
},
{
onSuccess: () => {
showSuccessNotification({
title: t("integration.page.create.notification.success.title"),
message: t("integration.page.create.notification.success.message"),
});
void revalidatePathAction("/integrations").then(() =>
router.push("/integrations"),
);
},
onError: () => {
showErrorNotification({
title: t("integration.page.create.notification.error.title"),
message: t("integration.page.create.notification.error.message"),
});
},
},
);
};
return (
<form onSubmit={form.onSubmit((value) => void handleSubmit(value))}>
<Stack>
<TestConnectionNoticeAlert />
<TextInput
label={t("integration.field.name.label")}
{...form.getInputProps("name")}
/>
<TextInput
label={t("integration.field.url.label")}
{...form.getInputProps("url")}
/>
<Fieldset legend={t("integration.secrets.title")}>
<Stack gap="sm">
{secretKinds.length > 1 && (
<SecretKindsSegmentedControl
secretKinds={secretKinds}
form={form}
/>
)}
{form.values.secrets.map(({ kind }, index) => (
<IntegrationSecretInput
key={kind}
kind={kind}
{...form.getInputProps(`secrets.${index}.value`)}
/>
))}
</Stack>
</Fieldset>
<Group justify="space-between" align="center">
<TestConnection
isDirty={isDirty}
removeDirty={removeDirty}
integration={{
id: null,
kind: searchParams.kind,
...form.values,
}}
/>
<Group>
<Button variant="default" component={Link} href="/integrations">
{t("common.action.backToOverview")}
</Button>
<Button type="submit" loading={isPending} disabled={isDirty}>
{t("common.action.create")}
</Button>
</Group>
</Group>
</Stack>
</form>
);
};
interface SecretKindsSegmentedControlProps {
secretKinds: IntegrationSecretKind[][];
form: UseFormReturnType<FormType, (values: FormType) => FormType>;
}
const SecretKindsSegmentedControl = ({
secretKinds,
form,
}: SecretKindsSegmentedControlProps) => {
const t = useScopedI18n("integration.secrets");
const secretKindGroups = secretKinds.map((kinds) => ({
label: kinds.map((kind) => t(`kind.${kind}.label`)).join(" & "),
value: kinds.join("-"),
}));
const onChange = useCallback(
(value: string) => {
const kinds = value.split("-") as IntegrationSecretKind[];
const secrets = kinds.map((kind) => ({
kind,
value: "",
}));
form.setFieldValue("secrets", secrets);
},
[form],
);
return (
<SegmentedControl
fullWidth
data={secretKindGroups}
onChange={onChange}
></SegmentedControl>
);
};
type FormType = Omit<z.infer<typeof validation.integration.create>, "kind">;
@@ -0,0 +1,44 @@
import { notFound } from "next/navigation";
import { Container, Group, Stack, Title } from "@mantine/core";
import type { IntegrationKind } from "@homarr/definitions";
import { getIntegrationName, integrationKinds } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import type { validation } from "@homarr/validation";
import { z } from "@homarr/validation";
import { IntegrationAvatar } from "../_integration-avatar";
import { NewIntegrationForm } from "./_integration-new-form";
interface NewIntegrationPageProps {
searchParams: Partial<z.infer<typeof validation.integration.create>> & {
kind: IntegrationKind;
};
}
export default async function IntegrationsNewPage({
searchParams,
}: NewIntegrationPageProps) {
const result = z
.enum([integrationKinds[0]!, ...integrationKinds.slice(1)])
.safeParse(searchParams.kind);
if (!result.success) {
notFound();
}
const t = await getScopedI18n("integration.page.create");
const currentKind = result.data;
return (
<Container>
<Stack>
<Group align="center">
<IntegrationAvatar kind={currentKind} size="md" />
<Title>{t("title", { name: getIntegrationName(currentKind) })}</Title>
</Group>
<NewIntegrationForm searchParams={searchParams} />
</Stack>
</Container>
);
}
@@ -0,0 +1,174 @@
import Link from "next/link";
import {
AccordionControl,
AccordionItem,
AccordionPanel,
ActionIcon,
ActionIconGroup,
Anchor,
Button,
Container,
Group,
Menu,
MenuDropdown,
MenuTarget,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from "@mantine/core";
import { IconChevronDown, IconPencil } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
import { objectEntries } from "@homarr/common";
import type { IntegrationKind } from "@homarr/definitions";
import { getIntegrationName } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { CountBadge } from "@homarr/ui";
import { ActiveTabAccordion } from "../../../../components/active-tab-accordion";
import { IntegrationAvatar } from "./_integration-avatar";
import { DeleteIntegrationActionButton } from "./_integration-buttons";
import { IntegrationCreateDropdownContent } from "./new/_integration-new-dropdown";
interface IntegrationsPageProps {
searchParams: {
tab?: IntegrationKind;
};
}
export default async function IntegrationsPage({
searchParams,
}: IntegrationsPageProps) {
const integrations = await api.integration.all();
const t = await getScopedI18n("integration");
return (
<Container>
<Stack>
<Group justify="space-between" align="center">
<Title>{t("page.list.title")}</Title>
<Menu
width={256}
trapFocus
position="bottom-start"
withinPortal
shadow="md"
keepMounted={false}
>
<MenuTarget>
<Button rightSection={<IconChevronDown size={16} stroke={1.5} />}>
{t("action.create")}
</Button>
</MenuTarget>
<MenuDropdown>
<IntegrationCreateDropdownContent />
</MenuDropdown>
</Menu>
</Group>
<IntegrationList
integrations={integrations}
activeTab={searchParams.tab}
/>
</Stack>
</Container>
);
}
interface IntegrationListProps {
integrations: RouterOutputs["integration"]["all"];
activeTab?: IntegrationKind;
}
const IntegrationList = async ({
integrations,
activeTab,
}: IntegrationListProps) => {
const t = await getScopedI18n("integration");
if (integrations.length === 0) {
return <div>{t("page.list.empty")}</div>;
}
const grouppedIntegrations = integrations.reduce(
(acc, integration) => {
if (!acc[integration.kind]) {
acc[integration.kind] = [];
}
acc[integration.kind].push(integration);
return acc;
},
{} as Record<IntegrationKind, RouterOutputs["integration"]["all"]>,
);
return (
<ActiveTabAccordion defaultValue={activeTab} variant="separated">
{objectEntries(grouppedIntegrations).map(([kind, integrations]) => (
<AccordionItem key={kind} value={kind}>
<AccordionControl icon={<IntegrationAvatar size="sm" kind={kind} />}>
<Group>
<Text>{getIntegrationName(kind)}</Text>
<CountBadge count={integrations.length} />
</Group>
</AccordionControl>
<AccordionPanel>
<Table>
<TableThead>
<TableTr>
<TableTh>{t("field.name.label")}</TableTh>
<TableTh>{t("field.url.label")}</TableTh>
<TableTh />
</TableTr>
</TableThead>
<TableTbody>
{integrations.map((integration) => (
<TableTr key={integration.id}>
<TableTd>{integration.name}</TableTd>
<TableTd>
<Anchor
href={integration.url}
target="_blank"
rel="noreferrer"
size="sm"
>
{integration.url}
</Anchor>
</TableTd>
<TableTd>
<Group justify="end">
<ActionIconGroup>
<ActionIcon
component={Link}
href={`/integrations/edit/${integration.id}`}
variant="subtle"
color="gray"
aria-label="Edit integration"
>
<IconPencil size={16} stroke={1.5} />
</ActionIcon>
<DeleteIntegrationActionButton
integration={integration}
count={integrations.length}
/>
</ActionIconGroup>
</Group>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</AccordionPanel>
</AccordionItem>
))}
</ActiveTabAccordion>
);
};
@@ -0,0 +1,134 @@
import type { PropsWithChildren } from "react";
import { AppShellMain } from "@mantine/core";
import {
IconBook2,
IconBox,
IconBrandDiscord,
IconBrandDocker,
IconBrandGithub,
IconGitFork,
IconHome,
IconInfoSmall,
IconLayoutDashboard,
IconLogs,
IconMailForward,
IconPlug,
IconQuestionMark,
IconTool,
IconUser,
IconUsers,
IconUsersGroup,
} from "@tabler/icons-react";
import { getScopedI18n } from "@homarr/translation/server";
import { MainHeader } from "~/components/layout/header";
import type { NavigationLink } from "~/components/layout/navigation";
import { MainNavigation } from "~/components/layout/navigation";
import { ClientShell } from "~/components/layout/shell";
export default async function ManageLayout({ children }: PropsWithChildren) {
const t = await getScopedI18n("management.navbar");
const navigationLinks: NavigationLink[] = [
{
label: t("items.home"),
icon: IconHome,
href: "/manage",
},
{
icon: IconLayoutDashboard,
href: "/manage/boards",
label: t("items.boards"),
},
{
icon: IconBox,
href: "/manage/apps",
label: t("items.apps"),
},
{
icon: IconPlug,
href: "/manage/integrations",
label: t("items.integrations"),
},
{
icon: IconUser,
label: t("items.users.label"),
items: [
{
label: t("items.users.items.manage"),
icon: IconUsers,
href: "/manage/users",
},
{
label: t("items.users.items.invites"),
icon: IconMailForward,
href: "/manage/users/invites",
},
{
label: t("items.users.items.groups"),
icon: IconUsersGroup,
href: "/manage/users/groups",
},
],
},
{
label: t("items.tools.label"),
icon: IconTool,
items: [
{
label: t("items.tools.items.docker"),
icon: IconBrandDocker,
href: "/manage/tools/docker",
},
{
label: t("items.tools.items.logs"),
icon: IconLogs,
href: "/manage/tools/logs",
},
],
},
{
label: t("items.help.label"),
icon: IconQuestionMark,
items: [
{
label: t("items.help.items.documentation"),
icon: IconBook2,
href: "https://homarr.dev/docs/getting-started/prerequisites",
external: true,
},
{
label: t("items.help.items.submitIssue"),
icon: IconBrandGithub,
href: "https://github.com/ajnart/homarr/issues/new/choose",
external: true,
},
{
label: t("items.tools.items.docker"),
icon: IconBrandDiscord,
href: "https://discord.com/invite/aCsmEV5RgA",
external: true,
},
{
label: t("items.help.items.sourceCode"),
icon: IconGitFork,
href: "https://github.com/ajnart/homarr",
external: true,
},
],
},
{
label: t("items.about"),
icon: IconInfoSmall,
href: "/manage/about",
},
];
return (
<ClientShell hasNavigation>
<MainHeader></MainHeader>
<MainNavigation links={navigationLinks}></MainNavigation>
<AppShellMain>{children}</AppShellMain>
</ClientShell>
);
}
@@ -0,0 +1,33 @@
import { Title } from "@mantine/core";
import { getScopedI18n } from "@homarr/translation/server";
import { Test } from "./test";
export async function generateMetadata() {
const t = await getScopedI18n("management");
const metaTitle = `${t("metaTitle")} • Homarr`;
return {
title: metaTitle,
};
}
export default async function ManagementPage() {
const t = await getScopedI18n("management.title");
const dateNow = new Date();
const timeOfDay =
dateNow.getHours() < 10
? "morning"
: dateNow.getHours() < 17
? "afternoon"
: "evening";
return (
<>
<Title>{t(timeOfDay, { username: "admin" })}</Title>
<Test />
</>
);
}
Loaded 100 of 449 files, more files were not shown because too many files have changed in this diff. Show more