Compare commits

..
Author SHA1 Message Date
Meier Lukas 27b04e620e wip: experiment with dropping to a div and removing item 2024-07-07 12:37:24 +02:00
Meier Lukas 95101e34ed feat: add docker actions (#752)
* feat: add docker actions

* chore: remove unnecessary import
2024-07-07 09:58:40 +02:00
Meier Lukas 998615fc11 fix: memory leak caused by many unclosed redis subscriptions (#750)
* fix: memory leak caused by many unclosed redis subscriptions

* chore: address pull request feedback
2024-07-07 09:58:20 +02:00
homarr-renovate[bot] 61cbb74d14 fix(deps): update dependency glob to ^10.4.3 (#751)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-07-06 04:29:19 +00:00
homarr-renovate[bot] 228fc48e33 chore(deps): update dependency @types/node to ^20.14.10 (#748)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-07-05 19:20:33 +00:00
homarr-renovate[bot] a67050e77b fix(deps): update tanstack-query monorepo to ^5.50.1 (#747)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-07-05 04:14:08 +00:00
homarr-renovate[bot] 9375738e05 fix(deps): update dependency react-simple-code-editor to ^0.14.1 (#746)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-07-04 23:24:40 +00:00
homarr-renovate[bot] 75e5a6c42f fix(deps): update dependency @tabler/icons-react to ^3.9.0 (#745)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-07-04 20:10:12 +00:00
Meier Lukas 41dba7b516 fix: controller is already closed trpc subscription observable error (#743) 2024-07-03 20:31:06 +02:00
homarr-renovate[bot] f557fbbae1 chore(deps): update dependency testcontainers to ^10.10.1 (#739)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-07-03 20:28:57 +02:00
homarr-renovate[bot] 1dd509a40f fix(deps): update dependency ws to ^8.18.0 (#740)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-07-03 20:28:47 +02:00
Manuel bb8640b162 feat: add jellyfin integration (#672)
* feat: #655 implement jellyfin media server

* fix: table overflow

* feat: pr feedback

* refactor: format

* refactor: merge existing code

* fix: code smells

* refactor: format commit
2024-07-03 20:06:57 +02:00
Manuel 1cf119c768 feat: log elapsed cron job time (#742) 2024-07-03 19:47:13 +02:00
Meier Lukas 15f3493b5b chore(deps): update pnpm/action-setup to v4 (#741) 2024-07-03 19:37:54 +02:00
homarr-renovate[bot] d32075d410 fix(deps): update dependency react-simple-code-editor to ^0.14.0 (#738)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-07-02 23:48:11 +00:00
homarr-renovate[bot] e12531cccc fix(deps): update dependency @tabler/icons-react to ^3.8.0 (#737)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-07-02 21:23:14 +00:00
homarr-renovate[bot] 6aecd1a713 fix(deps): update dependency better-sqlite3 to ^11.1.2 (#736)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-07-02 17:09:08 +00:00
homarr-renovate[bot] 60c0cfa911 fix(deps): update mantine monorepo to ^7.11.1 (#735)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-07-02 11:21:36 +00:00
Manuel dba97a3bd6 feat: add calendar widget (#663)
* feat: add calendar widget

* feat: add artifacts to gitignore
2024-07-02 12:13:13 +02:00
homarr-renovate[bot] 83ee03b192 chore(deps): update dependency @types/better-sqlite3 to v7.6.11 (#734)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-07-02 04:12:02 +00:00
homarr-renovate[bot] 0ac123b59f chore(deps): update dependency typescript to ^5.5.3 (#733)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-07-01 19:09:01 +00:00
homarr-renovate[bot] 0990e4b552 fix(deps): update dependency typescript-eslint to ^7.15.0 (#732)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-07-01 17:35:31 +00:00
Manuel 08d571ad74 feat: add tasks page (#692) 2024-07-01 18:57:40 +02:00
homarr-renovate[bot] 663eb0bf5b fix(deps): update dependency mysql2 to v3.10.2 (#731)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-07-01 02:46:23 +00:00
homarr-renovate[bot] 8ee0d67a36 fix(deps): update tanstack-query monorepo to ^5.49.2 (#730)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-30 06:48:29 +00:00
Manuel 4e1bbf2ae6 feat: add dynamic breadcrumb (#706)
* feat: add dynamic breadcrumb

* feat: pr feedback
2024-06-29 17:28:22 +02:00
homarr-renovate[bot] be100b610e fix(deps): update dependency drizzle-kit to ^0.22.8 (#729)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-29 12:48:32 +00:00
homarr-renovate[bot] 531cd0de25 fix(deps): update tanstack-query monorepo to ^5.49.0 (#728)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-29 06:40:56 +00:00
Meier Lukas 8fc28844bb fix: typo in automatic release commit message (#726) 2024-06-28 22:03:24 +02:00
homarr-renovate[bot] 4cf10b97c5 chore(deps): update turbo monorepo to ^2.0.6 (#723)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-28 19:33:31 +00:00
homarr-renovate[bot] e057affddf chore(deps): update dependency eslint to ^9.6.0 (#722)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-28 17:48:14 +00:00
homarr-renovate[bot] e46bfd9146 fix(deps): update dependency jotai to ^2.8.4 (#721)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-28 04:28:46 +00:00
homarr-renovate[bot] c8d4461ab8 fix(deps): update dependency better-sqlite3 to ^11.1.1 (#720)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-27 18:11:16 +00:00
homarr-renovate[bot] 52e936f979 fix(deps): update mantine monorepo to ^7.11.0 (#719)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-26 17:08:53 +00:00
homarr-renovate[bot] 7a14c255be fix(deps): update dependency @ianvs/prettier-plugin-sort-imports to ^4.3.0 (#718)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-26 14:09:06 +00:00
Manuel c4b54ed36c fix: open navigation item by default (#716) 2024-06-26 12:19:05 +02:00
homarr-renovate[bot] 988ec6bebb chore(deps): update dependency @types/node to ^20.14.9 (#717)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-25 22:26:38 +00:00
homarr-renovate[bot] 6a3a8465e7 fix(deps): update dependency @tabler/icons-react to ^3.7.0 (#715)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-25 15:48:11 +00:00
homarr-renovate[bot] a76e941378 fix(deps): update tanstack-query monorepo to ^5.48.0 (#714)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-25 14:38:27 +00:00
homarr-renovate[bot] 75748dfbc3 fix(deps): update tanstack-query monorepo to ^5.47.0 (#713)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-25 13:12:11 +00:00
homarr-renovate[bot] 196ac340a1 fix(deps): update dependency typescript-eslint to ^7.14.1 (#709)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-24 22:36:37 +00:00
homarr-renovate[bot] fab69c1c07 chore(deps): update turbo monorepo to ^2.0.5 (#711)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-24 22:28:16 +00:00
homarr-renovate[bot] a2140d0870 chore(deps): update node.js to v20.15.0 (#710)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-24 22:11:17 +00:00
Meier Lukas 6029a5b592 style: improve mobile compatibility of certain manage pages (#678)
* style: improve mobile compatibility of certain manage pages

* style: improve mobile support for more manage pages

* fix: format issues

* chore: address pull request feedback
2024-06-22 21:02:17 +02:00
Meier Lukas f92aeba403 feat: test integration connection (#669)
* feat: add test integration for pi-hole

* refactor: test integration for pi-hole

* fix: multiple secrets of same type could be used for integration creation

* fix: remove integration test connection test and add mock for test-connection function

* fix: add missing onUpdateFn to mysql integration secrets

* fix: format issues

* feat: add home assistant test connection

* fix: deepsource issues

* test: add system integration tests for test connection

* fix: add before all for pulling home assistant image

* test: add unit tests for handleTestConnectionResponseAsync

* test: add unit test for testConnectionAsync

* test: add mroe tests to integration-test-connection

* fix: deepsource issues

* fix: deepsource issue

* chore: address pull request feedback
2024-06-22 21:02:04 +02:00
Meier Lukas 92afd82d22 refactor: add cron job core package (#704)
* refactor: add cron job core package

* docs: add comments to cron validation types

* chore(deps): move node-cron dependencies from tasks app to cron-jobs-core package

* fix: runOnInit is not running on start and rather on job creation

* fix: format issues

* fix: build fails when using top level await in cjs

* chore: update turbo gen package json typescript version to 5.5.2

* fix: format issue

* fix: deepsource issues

* chore: update turbo gen package json eslint version to 9.5.0

* chore: fix frozen lockfile and format
2024-06-22 20:00:20 +02:00
homarr-renovate[bot] ea12da991a chore(deps): update dependency testcontainers to ^10.10.0 (#705)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-22 15:47:58 +00:00
homarr-renovate[bot] af0b986b3e chore(deps): update dependency @types/node to ^20.14.8 (#703)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-22 08:12:17 +00:00
homarr-renovate[bot] 324a643fd1 fix(deps): update dependency @auth/drizzle-adapter to ^1.4.1 (#701)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-21 21:10:42 +00:00
homarr-renovate[bot] 05702bede8 fix(deps): update dependency @auth/core to ^0.34.1 (#702)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-21 20:48:15 +00:00
homarr-renovate[bot] 9087f4bfdf fix(deps): update dependency @auth/core to ^0.34.0 (#700)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-21 20:26:00 +00:00
homarr-renovate[bot] 8d7a7bc9b1 fix(deps): update dependency @tanstack/react-query-next-experimental to v5.46.0 (#699)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-21 09:25:33 +00:00
homarr-renovate[bot] 7b3143357e chore(deps): update dependency @types/node to ^20.14.7 (#698)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-20 21:24:10 +00:00
homarr-renovate[bot] d5f4bd0aab chore(deps): update dependency typescript to ^5.5.2 (#697)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-20 18:13:25 +00:00
homarr-renovate[bot] 21a2db6019 chore(deps): update dependency node to v20.15.0 (#696)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-20 16:33:02 +00:00
homarr-renovate[bot] d78b581104 fix(deps): update dependency eslint-plugin-jsx-a11y to ^6.9.0 (#695)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-20 06:48:21 +00:00
homarr-renovate[bot] 4291bde94f fix(deps): update dependency @auth/drizzle-adapter to ^1.3.0 (#694)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-20 02:44:06 +00:00
homarr-renovate[bot] 171fe40fd5 fix(deps): update dependency @auth/core to ^0.33.0 (#693)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-20 02:22:20 +00:00
homarr-renovate[bot] 541d745cea fix(deps): update dependency undici to v6.19.2 (#691)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-19 17:36:32 +00:00
ManuelandMeier Lukas 508369cb0b feat: add undici fetch interceptor (#687)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-06-19 19:26:42 +02:00
homarr-renovate[bot] bf607349e0 chore(deps): update dependency @types/node to ^20.14.6 (#690)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-19 16:30:58 +00:00
homarr-renovate[bot] 0bfc0c5d25 fix(deps): update dependency glob to ^10.4.2 (#689)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-19 02:46:21 +00:00
homarr-renovate[bot] fdcf3a89c9 fix(deps): update dependency eslint-plugin-react to ^7.34.3 (#688)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-18 20:33:38 +00:00
homarr-renovate[bot] 86b49414f0 chore(deps): update docker/build-push-action action to v6 (#679)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-18 21:51:53 +02:00
homarr-renovate[bot] 64fe061fb0 chore(deps): update dependency @types/node to ^20.14.5 (#686)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-18 07:09:33 +00:00
homarr-renovate[bot] f96e25c0a2 chore(deps): update dependency @types/node to ^20.14.4 (#685)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-18 03:38:41 +00:00
homarr-renovate[bot] 4526fa6fc4 fix(deps): update dependency sass to ^1.77.6 (#683)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-17 21:23:43 +00:00
homarr-renovate[bot] 05690cc5e3 chore(deps): update dependency @types/node to ^20.14.3 (#682)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-17 20:08:32 +00:00
homarr-renovate[bot] e9d612051d fix(deps): update dependency typescript-eslint to ^7.13.1 (#681)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-17 17:47:57 +00:00
homarr-renovate[bot] 727f97226c chore(deps): update pnpm to v9.4.0 (#680)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-17 16:48:11 +00:00
homarr-renovate[bot] 33e8cc7412 fix(deps): update tanstack-query monorepo to ^5.45.1 (#677)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-16 20:33:50 +00:00
Meier Lukas f358c33bd3 fix: missing translations for apps / integrations #438 (#674)
* fix: missing translations for apps / integrations #438

* chore: remove unnecessary property from integration new dropdown
2024-06-16 22:10:17 +02:00
Meier Lukas e957c6b601 fix: icon selector is a select not an autocomplete #515 (#675) 2024-06-16 22:09:48 +02:00
homarr-renovate[bot] 0d14bad10b fix(deps): update dependency ws to ^8.17.1 (#676)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-16 15:25:33 +00:00
Meier Lukas 25f8564e97 chore: change turbo ui to stream because the other one is unusable (#670) 2024-06-15 22:47:31 +02:00
homarr-renovate[bot] 8b4cc5b994 chore(deps): update dependency eslint to ^9.5.0 (#668)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-14 19:48:24 +00:00
homarr-renovate[bot] 856cb2d005 chore(deps): update turbo monorepo to ^2.0.4 (#666)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-14 17:48:25 +00:00
homarr-renovate[bot] 10acb2bc76 fix(deps): update dependency mysql2 to v3.10.1 (#665)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-13 23:24:12 +00:00
homarr-renovate[bot] e4c6864a94 fix(deps): update dependency @tabler/icons-react to ^3.6.0 (#664)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-13 20:48:14 +00:00
homarr-renovate[bot] 4e9593b859 fix(deps): update mantine monorepo to ^7.10.2 (#662)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-13 09:26:34 +00:00
homarr-renovate[bot] 37ebcfe318 fix(deps): update dependency mantine-react-table to v2.0.0-beta.5 (#661)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-13 06:13:09 +00:00
homarr-renovate[bot] 5547e52001 fix(deps): update tanstack-query monorepo to ^5.45.0 (#660)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-12 13:12:23 +00:00
homarr-renovate[bot] f5f6388361 fix(deps): update dependency sass to ^1.77.5 (#659)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-12 03:48:16 +00:00
homarr-renovate[bot] 476fa59079 fix(deps): update nextjs monorepo to ^14.2.4 (#658)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-11 22:27:00 +00:00
homarr-renovate[bot] 8b3bc03b9b fix(deps): update tanstack-query monorepo to ^5.44.0 (#657)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-11 13:34:47 +00:00
homarr-renovate[bot] d69cda0313 fix(deps): update dependency prettier to ^3.3.2 (#656)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-11 06:32:24 +00:00
Meier Lukas 9cab001451 chore: improve importing of some packages (#647)
* chore: improve importing of some packages

* fix: not same version for all dependencies

* fix: lint and format issue
2024-06-10 21:26:39 +02:00
Meier Lukas c627384c4b test: add pi-hole test container integration test (#649)
* test: add pi-hole test container integration test

* chore: improve waiting logic for test container

* refactor: improve structure of pi hole test

* fix: eslint does not recognize test directory to be part of tsconfig
2024-06-10 21:18:24 +02:00
Meier Lukas 8e4ba37f94 fix: login with revalidate path and wrong preferences link (#654) 2024-06-10 21:17:51 +02:00
Manuel 2e782ae442 feat: add homeassistant integration (#578) 2024-06-10 21:16:39 +02:00
homarr-renovate[bot] 19498854fc fix(deps): update dependency typescript-eslint to ^7.13.0 (#653)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-10 17:34:03 +00:00
homarr-renovate[bot] d985fbe69a fix(deps): update dependency drizzle-kit to ^0.22.7 (#652)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-10 14:38:11 +00:00
homarr-renovate[bot] 6fc27dfce4 chore(deps): update pnpm to v9.3.0 (#651)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-10 13:13:08 +00:00
homarr-renovate[bot] 83b4631cde chore(deps): update dependency @vitejs/plugin-react to ^4.3.1 (#650)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-10 01:49:14 +00:00
homarr-renovate[bot] 84851db333 fix(deps): update dependency @trpc/client to v11.0.0-rc.401 (#648)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-09 19:48:18 +00:00
SeDemal 0710425e78 feat: DNS Hole summary Scalability (#646) 2024-06-09 17:36:18 +02:00
SeDemal 28927cfa28 feat: App tile scalability (#645)
* feat: App tile scalability

* fix: missing translation
2024-06-09 12:54:56 +02:00
Meier Lukas 3a3a8251bd feat: improve user init setup (#643) 2024-06-08 23:35:48 +02:00
SeDemal bac09d8be5 feat: Clock widget scalability (#644) 2024-06-08 23:35:35 +02:00
Meier Lukas 2c1b52aff9 fix: gridstack not working (#642)
* fix: gridstack not working

* chore: remove unneccessary comments
2024-06-08 22:22:15 +02:00
Meier Lukas e46ea71bc2 fix: renovate config with json does not support comments (#641) 2024-06-08 21:38:44 +02:00
Meier Lukas fb92c3e95c fix: pnpm dev did not work caused by tsx update (#638)
* fix: pnpm dev did not work caused by tsx update

* fix: format issues
2024-06-08 21:28:28 +02:00
homarr-renovate[bot] 38c32cbece fix(deps): update dependency @tanstack/react-query-next-experimental to v5.41.0 (#639)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-08 19:19:39 +00:00
homarr-renovate[bot]andMeier Lukas 1bae7352dc chore(deps): update dependency eslint to v9 (#452)
* chore(deps): update dependency eslint to v9

* chore: migrate eslint to v9

* fix: dependency issues

* fix: unit tests not working

* chore: disable lint check for Image component that does not work in ci

* 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-06-08 20:49:57 +02:00
Meier Lukas d7ecdf5567 feat: add simple app ping (#580)
* feat: add simple ping

* refactor: make ping run on server and show errors

* fix: format issues

* fix: missing translation for enabled ping option for app

* refactor: remove ping queue as no longer needed

* chore: address pull request feedback

* test: add some unit tests

* fix: format issues

* fix: deepsource issues
2024-06-08 17:33:16 +02:00
Thomas Camlong 3dca787fa7 chore: configure i18n ally (#606)
Note: TypeScript files are read-only https://github.com/lokalise/i18n-ally/wiki/Locale-Formats#supported-locale-formats
2024-06-08 17:32:46 +02:00
homarr-renovate[bot]andMeier Lukas 1f730ab786 chore(deps): update turbo monorepo to v2 (major) (#621)
* chore(deps): update turbo monorepo to v2

* chore: run codemod

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-06-08 17:28:15 +02:00
homarr-renovate[bot] a3b861e13c chore(deps): update dependency tsx to v4.14.0 (#637)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-08 12:14:49 +00:00
homarr-renovate[bot] 66ddb45975 fix(deps): update dependency drizzle-kit to ^0.22.6 (#636)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-08 11:20:54 +00:00
homarr-renovate[bot] 4de5ab5d41 chore(deps): update dependency tsx to v4.13.3 (#635)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-08 03:26:43 +00:00
homarr-renovate[bot] 475f472cb0 fix(deps): update dependency @trpc/client to v11.0.0-rc.396 (#634)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-07 20:33:39 +00:00
homarr-renovate[bot] efd958d6cb fix(deps): update dependency drizzle-orm to ^0.31.2 (#632)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-07 15:48:32 +00:00
homarr-renovate[bot] 246272669e fix(deps): update dependency drizzle-kit to ^0.22.5 (#631)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-07 15:34:23 +00:00
homarr-renovate[bot] 493c22f9da chore(deps): update dependency tsx to v4.13.2 (#630)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-07 10:29:53 +00:00
homarr-renovate[bot] 1d6745b142 chore(deps): update dependency tsx to v4.13.1 (#629)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-07 07:09:43 +00:00
homarr-renovate[bot] 309111c600 chore(deps): update dependency tsx to v4.13.0 (#628)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-07 03:28:32 +00:00
homarr-renovate[bot] 98078556c3 chore(deps): update dependency tsx to v4.12.1 (#627)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-07 02:28:54 +00:00
homarr-renovate[bot] 5bf1ddde4b fix(deps): update dependency @trpc/client to v11.0.0-rc.394 (#626)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-06 23:35:42 +00:00
homarr-renovate[bot] afa96349c5 chore(deps): update pnpm to v9.2.0 (#624)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-06 18:39:58 +00:00
homarr-renovate[bot] 1aabc4dfb2 chore(deps): update dependency tsx to v4.12.0 (#614)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-06 18:30:41 +00:00
homarr-renovate[bot] 8f551aa485 fix(deps): update dependency prettier to ^3.3.1 (#623)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-06 18:12:17 +00:00
homarr-renovate[bot] dda34e7764 fix(deps): update dependency drizzle-kit to ^0.22.4 (#625)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-06 17:48:14 +00:00
homarr-renovate[bot] 899060c234 chore(deps): update turbo monorepo to ^1.13.4 (#620)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-06 17:35:59 +00:00
homarr-renovate[bot] f1dbc4420f chore(deps): update dependency @types/node to ^20.14.2 (#622)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-06 17:27:56 +00:00
Manuel 251cedf111 ci: use app instead of personal token (#611) 2024-06-06 18:06:28 +02:00
SeDemal cfd8b328b4 feat: weather widget scalable (#574)
* refactor: Make weather widget scalable

* fix: formatting

* fix: map key again

* fix: null assertions
2024-06-05 21:26:59 +02:00
homarr-renovate[bot] 2623708c6d fix(deps): update typescript-eslint monorepo to ^7.12.0 (#616)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-04 21:06:15 +02:00
homarr-renovate[bot] 946ea8753f fix(deps): update tanstack-query monorepo to ^5.40.1 (#617)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-04 21:05:32 +02:00
homarr-renovate[bot] f5aaf89a3d fix(deps): update dependency drizzle-kit to ^0.22.2 (#618)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-04 21:04:24 +02:00
homarr-renovate[bot] 710197b3a0 fix(deps): update dependency drizzle-orm to ^0.31.1 (#619)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-04 21:03:47 +02:00
homarr-renovate[bot] d993e8d811 fix(deps): update dependency jotai to ^2.8.3 (#615)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-04 21:03:03 +02:00
homarr-renovate[bot] 381832685f chore(deps): update dependency @types/node to ^20.14.1 (#613)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-04 21:02:15 +02:00
homarr-renovate[bot] 812b8d8306 fix(deps): update dependency @trpc/client to v11.0.0-rc.390 (#609)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-03 19:09:01 +00:00
Manuel 2b5f873c00 ci: add rider run configuration (#610) 2024-06-02 18:24:29 +02:00
homarr-renovate[bot] 20874cb96d fix(deps): update dependency next-auth to v5.0.0-beta.19 (#605)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-02 09:24:32 +00:00
homarr-renovate[bot]andMeier Lukas dc3b00736e fix(deps): update dependency @auth/core to ^0.32.0 (#603)
* fix(deps): update dependency @auth/core to ^0.32.0

* fix(deps): update dependency @auth/core to ^0.32.0

* fix: type issues with next-auth types

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-06-02 11:01:10 +02:00
homarr-renovate[bot] 869efbc638 fix(deps): update dependency @trpc/client to v11.0.0-rc.383 (#608)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-02 08:48:14 +00:00
homarr-renovate[bot] 2e044ac871 fix(deps): update dependency @auth/drizzle-adapter to ^1.2.0 (#604)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-02 05:33:55 +00:00
homarr-renovate[bot] 04c02d886d chore(deps): update actions/checkout action to v4 (#595)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-01 23:19:54 +02:00
Manuel 2be1ae8011 ci: fix pull request description 2024-06-01 23:10:04 +02:00
Manuel 67d4899ed3 ci: add automerge label to create PR step 2024-06-01 23:04:49 +02:00
Manuel a6a6734097 ci: fix pr number target 2024-06-01 23:01:27 +02:00
Manuel 4ff39b26e9 ci: remove if condition from create PR step 2024-06-01 22:52:32 +02:00
Manuel 4b304c74bc ci: fix fetch depth (#599) 2024-06-01 22:47:10 +02:00
Manuel d08bdf3e2d ci: fix branch to compare tags (#598) 2024-06-01 22:33:41 +02:00
Manuel 7cc2736d68 fix: default branch for tag compare (#597) 2024-06-01 22:16:21 +02:00
Manuel 452308336a refactor: clean up workflows (#596) 2024-06-01 22:04:42 +02:00
Manuel 26146f4037 ci: add automatic release workflow (#583) 2024-06-01 21:46:12 +02:00
homarr-renovate[bot] b1b839fcd8 fix(deps): update dependency prettier to ^3.3.0 (#594)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-06-01 17:25:39 +00:00
homarr-renovate[bot] fef070c42c fix(deps): update dependency mantine-react-table to v2.0.0-beta.4 (#593)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-31 20:24:11 +00:00
homarr-renovate[bot] e832bfbbdb chore(deps): update dependency @types/node to ^20.13.0 (#592)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-31 19:48:14 +00:00
homarr-renovate[bot] 777473ee36 chore(deps): update dependency @types/node to ^20.12.14 (#591)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-31 19:20:05 +00:00
homarr-renovate[bot] 96150f802d fix(deps): update dependency better-sqlite3 to v11 (#587)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-31 20:33:37 +02:00
homarr-renovate[bot] 51fb7cb402 fix(deps): update dependency drizzle-kit to ^0.22.1 (#590)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-31 12:59:18 +00:00
homarr-renovate[bot] 3b39e87212 fix(deps): update dependency drizzle-orm to ^0.31.0 (#589)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-31 08:48:13 +00:00
homarr-renovate[bot] d16281bea7 fix(deps): update dependency drizzle-kit to ^0.22.0 (#588)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-31 08:38:31 +00:00
homarr-renovate[bot] 79d977c008 fix(deps): update dependency sass to ^1.77.4 (#586)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-31 00:40:30 +00:00
homarr-renovate[bot] ff84c2bdb7 fix(deps): update dependency mysql2 to v3.10.0 (#585)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-30 19:22:13 +00:00
homarr-renovate[bot] 6d46e91a7c fix(deps): update dependency better-sqlite3 to ^10.1.0 (#584)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-30 18:13:37 +00:00
homarr-renovate[bot] ec29d77186 chore(deps): update pnpm to v9.1.4 (#582)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-30 12:16:50 +00:00
homarr-renovate[bot] b12d109f6d fix(deps): update mantine monorepo to ^7.10.1 (#581)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-30 10:29:27 +00:00
homarr-renovate[bot] 169fecaba1 fix(deps): update dependency sass to ^1.77.3 (#579)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-29 22:28:30 +00:00
Thomas CamlongandMeier Lukas dd2937a0d6 feat: add docker container table (#520)
* WIP On docker integration

* WIP on adding docker support

* WIP on adding docker support

* chore: Add cacheTime parameter to createCacheChannel function

* bugfix: Add node-loader npm dependency for webpack configuration

* revert changes

* chore: Add node-loader npm dependency for webpack configuration

* feat: Add Docker container list to DockerPage

* chore: apply pr suggestions

* fix: fix printing issue using a Date objext

* chore: Update npm dependencies

* feat: Create DockerTable component for displaying Docker container list

* feat: Refactor DockerPage to use DockerTable component

* feat: Refactor DockerPage to use DockerTable component

* feat: Add useTimeAgo hook for displaying relative timestamps

* feat: Add hooks module to common package

* refactor: Update DockerTable component

Include container actions and state badges

* feat: add information about instance for docker containers

* feat: Add OverflowBadge component for displaying overflowed data

* feat: Refactor DockerSingleton to use host and instance properties

This commit refactors the DockerSingleton class in the `docker.ts` file to use the `host` and `instance` properties instead of the previous `key` and `remoteApi` properties. This change improves clarity and consistency in the codebase.

* feat: Add OverflowBadge component for displaying overflowed data

* feat: Improve DockerTable component with Avatar and Name column

This commit enhances the DockerTable component in the `DockerTable.tsx` file by adding an Avatar and Name column. The Avatar column displays an icon based on the Docker container's image, while the Name column shows the container's name. This improvement provides better visual representation and identification of the containers in the table.

* feat: Enhance DockerTable component with Avatar and Name columns

* refactor: improve docker table and icon resolution

* chore: address pull request feedback

* fix: format issues

* chore: add missing translations

* refactor: remove black background

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-05-29 21:57:02 +02:00
homarr-renovate[bot] e030e06a16 chore(deps): update actions/checkout action to v4 (#571)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-29 21:52:44 +02:00
homarr-renovate[bot] dc7f455fe7 chore(deps): update dependency @types/node to ^20.12.13 (#577)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-29 17:27:00 +00:00
homarr-renovate[bot] 612b8d8c6e fix(deps): update dependency mysql2 to v3.9.9 (#576)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-29 14:48:11 +00:00
homarr-renovate[bot] fe43e74c53 fix(deps): update dependency flag-icons to ^7.2.3 (#575)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-29 11:21:16 +00:00
homarr-renovate[bot] 2cb3e95593 chore(deps): update node.js to v20.14.0 (#573)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-29 03:48:21 +00:00
homarr-renovate[bot] 9e499bd30b fix(deps): update dependency eslint-plugin-react to ^7.34.2 (#567)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-29 03:35:19 +00:00
Meier Lukas c2185e3489 fix: renovate automatic approval typo no file extension (#572) 2024-05-29 05:17:52 +02:00
homarr-renovate[bot] e448eeb0a6 fix(deps): update dependency @trpc/client to v11.0.0-rc.382 (#568)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-28 20:24:49 +00:00
homarr-renovate[bot] deea531607 chore(deps): update dependency node to v20.14.0 (#569)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-28 21:40:23 +02:00
Manuel ba46b6f869 ci: add conventional commits workflow (#541) 2024-05-28 21:40:04 +02:00
Meier Lukas bcbc2d1c56 ci: add github action that approves pull requests from renovate automatically (#566) 2024-05-28 21:39:15 +02:00
homarr-renovate[bot] c20f7a64a0 fix(deps): update typescript-eslint monorepo to ^7.11.0 (#565)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-28 17:48:13 +00:00
homarr-renovate[bot] 6f47adff8a fix(deps): update dependency jotai to ^2.8.2 (#561)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-28 16:11:10 +00:00
homarr-renovate[bot] e325e1c02c chore(deps): update dependency jsdom to ^24.1.0 (#564)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-27 21:52:27 +02:00
homarr-renovate[bot] 12e30c392e chore(deps): update dependency @types/react to ^18.3.3 (#555)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-27 21:51:40 +02:00
homarr-renovate[bot] ad3ef4c313 fix(deps): update mantine monorepo to ^7.10.0 (#538)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-27 19:48:39 +00:00
homarr-renovate[bot] 116d9dda04 chore(deps): update pnpm to v9.1.3 (#559)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-27 21:26:32 +02:00
homarr-renovate[bot] 2bc0431e4e fix(deps): update typescript-eslint monorepo to ^7.10.0 (#562)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-27 21:26:01 +02:00
homarr-renovate[bot] 3f364caced fix(deps): update dependency mysql2 to v3.9.8 (#558)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-27 21:24:32 +02:00
homarr-renovate[bot] abdf9bc122 fix(deps): update dependency @trpc/client to v11.0.0-rc.377 (#553)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-27 21:23:48 +02:00
homarr-renovate[bot] a0322bfd99 fix(deps): update dependency glob to ^10.4.1 (#531)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-27 21:23:27 +02:00
homarr-renovate[bot] 8f50def4c9 fix(deps): update tanstack-query monorepo to ^5.40.0 (#552)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-27 19:09:15 +00:00
homarr-renovate[bot] 56b6d76f43 fix(deps): update dependency drizzle-kit to ^0.21.4 (#535)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-27 20:13:01 +02:00
homarr-renovate[bot] 6e655b7ad2 chore(deps): update dependency @vitejs/plugin-react to ^4.3.0 (#536)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-27 18:12:06 +00:00
Meier Lukas 4d1e4d55bc fix: typo in renovate config name (#557) 2024-05-27 18:05:31 +02:00
homarr-renovate[bot]andMeier Lukas 0ccf258a90 chore(deps): update dependency tsx to v4.11.0 (#537)
* chore(deps): update dependency tsx to v4.11.0

* fix: build not working

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-05-26 17:51:12 +02:00
homarr-renovate[bot] 98ebabc9ca chore(deps): update pnpm to v9.1.2 (#530)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-26 17:22:02 +02:00
homarr-renovate[bot] 30fe9fef84 fix(deps): update typescript-eslint monorepo to v7.10.0 (#529)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-26 17:20:48 +02:00
ManuelandMeier Lukas 7e64d39693 feat: add analytics (#528)
* feat: add analytics

* feat: send data to umami

* refactor: fix format

* fix: click behavior of switches

* refactor: format

* chore: rerun ci

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-05-26 17:18:25 +02:00
Meier Lukas d57b771a17 feat: add pi hole summary integration (#521)
* feat: add pi hole summary integration

* feat: add pi hole summary widget

* fix: type issues with integrations and integrationIds

* feat: add middleware for integrations and improve cache redis channel

* feat: add error boundary for widgets

* fix: broken lock file

* fix: format format issues

* fix: typecheck issue

* fix: deepsource issues

* fix: widget sandbox without error boundary

* chore: address pull request feedback

* chore: remove todo comment and created issue

* fix: format issues

* fix: deepsource issue
2024-05-26 17:13:34 +02:00
homarr-renovate[bot] 96c71aed6e chore(deps): update dependency @vitejs/plugin-react to v4.3.0 (#545)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-25 19:19:26 +00:00
homarr-renovate[bot] 1569862baf fix(deps): update dependency @homarr/gridstack to v1.0.2 (#542)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-25 19:08:36 +00:00
homarr-renovate[bot] cd09d54163 chore(deps): update dependency @types/react to v18.3.3 (#546)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-25 19:57:09 +02:00
homarr-renovate[bot] a029abe6f3 fix(deps): update tanstack-query monorepo to v5.38.0 (#551)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-25 19:56:55 +02:00
Manuel b76fa9d722 ci: report vitest code coverage (#549) 2024-05-25 19:56:24 +02:00
Manuel b678d4f951 ci: validate pr title (#550) 2024-05-25 19:56:11 +02:00
Manuel 4ef668c5ea ci: use semver workflow action (#548) 2024-05-25 19:56:02 +02:00
homarr-renovate[bot] 7b46d92d4f fix(deps): update dependency glob to v10.4.1 (#544)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-24 23:44:38 +02:00
Manuel 9e199b7dff config: format json hbs as json (#540) 2024-05-24 21:49:25 +02:00
homarr-renovate[bot] 114176ee17 chore(deps): update anothrnick/github-tag-action action to v1.69.0 (#348)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-23 16:42:18 +00:00
Manuel 3940d296f2 config; activate automerge (#539) 2024-05-23 18:15:53 +02:00
homarr-renovate[bot] 63f743175e fix(deps): update dependency @tabler/icons-react to ^3.5.0 (#532)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-23 17:53:51 +02:00
homarr-renovate[bot] f45fa50024 fix(deps): update dependency flag-icons to ^7.2.2 (#533)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-23 17:53:36 +02:00
homarr-renovate[bot] c668d0a765 fix(deps): update dependency jotai to ^2.8.1 (#534)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-23 17:53:22 +02:00
Meier Lukas e097cffe59 refactor: improve behaviour of edit mode when navigating away and back (#525) 2024-05-22 21:28:45 +02:00
Thomas Camlong 5a9e8edf70 feat: Add VOLUME instruction to Dockerfile (#527) 2024-05-20 17:34:16 +02:00
Thomas Camlong 048711fe05 chore: Update trpc logger info (#526)
It now includes the username of the person doing the tRPC request (because the user id is not enough)
2024-05-20 17:00:50 +02:00
Thomas Camlong f617c60916 Merge pull request #464 from homarr-labs/ajnart/fix-duplicate-users 2024-05-19 23:44:46 +02:00
Meier Lukas c3b2215c7a Merge branch 'dev' into ajnart/fix-duplicate-users 2024-05-19 23:08:04 +02:00
homarr-renovate[bot] eb0db3ec4e fix(deps): update dependency @trpc/client to v11.0.0-rc.374 (#522)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-19 23:02:09 +02:00
Meier Lukas 26b1c4a319 feat: add custom css for board and custom classes in advanced options for items (#512)
* feat: add custom css for board and custom classes in advanced options for items

* chore: add mysql migration

* fix: test not working

* fix: format issues

* fix: typecheck issue

* fix: build issue

* chore: add missing translations

* fix: merge issues related to migrations

* fix: format issues

* fix: merge issue with migration

* fix: format issue
2024-05-19 23:01:26 +02:00
Thomas CamlongandMeier Lukas f1b1ec59ec chore: update prettier configuration for print width (#519)
* feat: update prettier configuration for print width

* chore: apply code formatting to entire repository

* fix: remove build files

* fix: format issue

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-05-19 22:38:39 +02:00
Manuel 919161798e feat: add server settings (#487)
* feat: add server settings

* feat: remove old migration

* feat: add new migrations

* refactor: format

* fix: build error

* refactor: format

* fix: lint
2024-05-19 22:29:15 +02:00
Meier Lukas d9f5158662 perf: improve typescript performance (#523) 2024-05-19 19:55:44 +02:00
Meier Lukas 7e339c09c8 feat: add home board for users (#505)
* feat: add home board for users

* fix: format issues

* fix: deepsource issue

* chore: address pull request feedback

* fix: typecheck issue
2024-05-18 16:57:00 +02:00
Meier Lukas dfed804f65 feat: add i18n translated form errors (#509) 2024-05-18 16:55:08 +02:00
homarr-renovate[bot] b312032f02 fix(deps): update tanstack-query monorepo to ^5.37.1 (#513)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-18 16:44:35 +02:00
Thomas Camlong a29cc78acd Merge pull request #517 from homarr-labs/create-cache-channel 2024-05-18 15:48:56 +02:00
homarr-renovate[bot] abffc866c0 fix(deps): update mantine monorepo to ^7.9.2 (#510)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-18 15:32:25 +02:00
homarr-renovate[bot] dc9a8dd9c6 chore(deps): update dependency tsx to v4.10.5 (#507)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-18 15:31:34 +02:00
homarr-renovate[bot] 7664b00be0 fix(deps): update dependency sass to ^1.77.2 (#506)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-18 15:30:22 +02:00
Meier Lukas e05deef6e2 feat: add redis cache channel 2024-05-18 15:28:05 +02:00
Manuel 31c2694185 feat: language selector (#484)
* feat: language selector

* refactor: move user general page

* feat: language selector

* refactor: move user general page

* feat: add language combobox in user general
2024-05-18 13:54:43 +02:00
Meier Lukas 7fa120c474 fix: create user async is called twice 2024-05-18 13:50:25 +02:00
Meier Lukas f30f133bfa Merge branch 'dev' into ajnart/fix-duplicate-users 2024-05-18 13:49:25 +02:00
Meier Lukas f0891dda31 Merge branch 'dev' into ajnart/fix-duplicate-users 2024-05-18 13:47:25 +02:00
Meier Lukas 8e8f9081b0 fix: when no dead icons, all icons are removed (#516) 2024-05-18 13:12:16 +02:00
Manuel dcaff1d91c feat: add async suffix eslint rule (#485) 2024-05-18 12:25:33 +02:00
Meier Lukas 5723295856 fix: mantine react table style issue (#508) 2024-05-17 21:42:45 +02:00
homarr-renovate[bot] 1abf441cdb fix(deps): update dependency @trpc/client to v11.0.0-rc.373 (#493)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-16 21:55:05 +02:00
homarr-renovate[bot] db48615096 chore(deps): update dependency tsx to v4.10.3 (#490)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-16 21:46:40 +02:00
homarr-renovate[bot] 8166fdb8e2 fix(deps): update dependency drizzle-kit to ^0.21.2 (#495)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-16 21:43:29 +02:00
Thomas Camlong ff3244d9ac Update run.sh (#498) 2024-05-16 21:26:07 +02:00
Meier Lukas 9a73b13ae5 chore: fix db studio command not working (#503)
* chore: fix db studio command not working

* fix: format issue
2024-05-16 21:12:59 +02:00
homarr-renovate[bot] 023feafafe fix(deps): update tanstack-query monorepo to ^5.36.2 (#496)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-16 21:11:36 +02:00
homarr-renovate[bot] 1b6a9c5aee chore(deps): update dependency @types/node to ^20.12.12 (#492)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-16 21:10:36 +02:00
homarr-renovate[bot] 3fe1a19e37 fix(deps): update dependency @tabler/icons-react to ^3.4.0 (#501)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-16 21:10:04 +02:00
Meier Lukas 669a79ef08 style: improve styling of items on board (#504)
style: improve styling of items on board by defining margin depending on screen size, removing general padding on card of items and reducing size of no app icon
2024-05-16 18:48:20 +02:00
Meier Lukas 333dc571fa perf: improve performance of icon updater from 10s to 300ms (#502) 2024-05-16 17:06:03 +02:00
Manuel f329e2a393 config: fix db push command (#497) 2024-05-15 21:14:27 +02:00
homarr-renovate[bot] a702f7dc67 fix(deps): update typescript-eslint monorepo to ^7.9.0 (#491)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-14 21:59:12 +02:00
homarr-renovate[bot] 8b84695079 fix(deps): update dependency mantine-react-table to v2.0.0-beta.3 (#488)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-14 21:58:45 +02:00
homarr-renovate[bot] 18d1aa6fcd chore(deps): update pnpm to v9.1.1 (#489)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-14 21:50:28 +02:00
homarr-renovate[bot] b18b364270 fix(deps): update tiptap monorepo to v2.4.0 (#494)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-14 21:47:18 +02:00
Manuel 87fe03dd00 feat: add hero banner (#463) 2024-05-12 18:19:01 +02:00
homarr-renovate[bot] 1570faa20a fix(deps): update tanstack-query monorepo to ^5.36.0 (#481)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-12 16:32:21 +02:00
homarr-renovate[bot] 143db86b77 chore(deps): update dependency tsx to v4.10.1 (#480)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-12 16:31:43 +02:00
Meier Lukas fd44ee48f4 fix: rdb files can not be saved because of permission issues bug: redis can not save RDB files #318 (#472)
* fix: rdb files can not be saved because of permission issues bug: redis can not save RDB files #318

* refactor: move db and redis dumps to /appdata directory
2024-05-12 16:28:44 +02:00
Meier Lukas db01301845 feat: user preferences (#470)
* wip: improve user preferences

* wip: fix translations and add user danger zone

* feat: add user delete button to danger zone

* fix: test not working

* refactor: add access checks for user edit page, improve not found behaviour, change user preference link in avatar menu to correct link

* fix: remove invalid bg for container

* chore: address pull request feedback
2024-05-12 16:27:56 +02:00
homarr-renovate[bot] f0da1d81a6 fix(deps): update dependency better-sqlite3 to v10 (#479)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-12 10:18:54 +02:00
homarr-renovate[bot] 333b124f67 fix(deps): update dependency glob to ^10.3.15 (#478)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-12 10:17:41 +02:00
Meier Lukas 94af21abbf feat: add user invite registration (#477) 2024-05-12 10:04:20 +02:00
Thomas Camlong fe7de2cf6d Merge branch 'dev' into ajnart/fix-duplicate-users 2024-05-12 01:02:07 +02:00
homarr-renovate[bot] 82d41abf46 fix(deps): update dependency @auth/drizzle-adapter to ^1.1.0 (#474)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-11 23:12:22 +02:00
homarr-renovate[bot]andMeier Lukas 95ddd03763 fix(deps): update dependency @auth/core to ^0.31.0 (#473)
* fix(deps): update dependency @auth/core to ^0.31.0

* fix: typecheck issue

* fix: more issues with types

* fix: add newest next-auth beta version as types were changed and depend on each other

* 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-05-11 22:44:00 +02:00
homarr-renovate[bot] 99114f0727 chore(deps): update dependency tsx to v4.10.0 (#476)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-11 22:24:15 +02:00
homarr-renovate[bot] d970cb54f5 chore(deps): update dependency @types/react to ^18.3.2 (#471)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-11 13:01:01 +02:00
homarr-renovate[bot] cb6f754492 fix(deps): update dependency sass to ^1.77.1 (#469)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-11 13:00:36 +02:00
homarr-renovate[bot] 8dd5987e5e chore(deps): update dependency tsx to v4.9.4 (#467)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-11 11:03:04 +02:00
homarr-renovate[bot] 627f8957e5 fix(deps): update dependency mantine-react-table to v2.0.0-beta.2 (#466)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-11 10:58:45 +02:00
homarr-renovate[bot] 8e6d143edc fix(deps): update tanstack-query monorepo to ^5.35.5 (#465)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-11 10:50:51 +02:00
homarr-renovate[bot]andMeier Lukas 1ce6fb0d0f fix(deps): update dependency drizzle-kit to ^0.21.1 (#462)
* fix(deps): update dependency drizzle-kit to ^0.21.0

* fix(deps): update dependency drizzle-kit to ^0.21.1

* fix: migrate to drizzle kit 0.21

* fix: format issues

* feat: add default env variables for db dialect and driver

* fix: issue with driver seems to be fixed

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-05-10 23:46:01 +02:00
ajnart 0008ceffed Fix prettier 2024-05-10 20:05:18 +02:00
ajnart 543bfc0835 Fix profile accordion error display 2024-05-10 20:02:57 +02:00
Thomas Camlong 6e2bd0ce11 fix: Handle existing user when editing profile 2024-05-10 15:24:40 +02:00
Thomas Camlong f5d71aebb4 Add successful profile edit message 2024-05-10 15:24:20 +02:00
Thomas Camlong 0ae8a75a5e feat: cannot change username to the current one 2024-05-10 15:23:57 +02:00
Thomas Camlong 65025a5dda fix: Update check for user existence 2024-05-10 14:24:35 +02:00
Thomas Camlong c2026d0962 refactor: Update UserCreateStepperComponent
To use ErrorDisplay component
2024-05-10 14:16:45 +02:00
Thomas Camlong 245c2f483c feat: Add ErrorDisplay component 2024-05-10 14:16:24 +02:00
Thomas Camlong 781247de51 feat: add username validation to prevent duplicate 2024-05-10 13:39:21 +02:00
Thomas Camlong 400fa3d480 propose forcing lowercase usernames 2024-05-10 13:36:46 +02:00
homarr-renovate[bot] 2434e2e758 chore(deps): update dependency node to v20.13.1 (#458)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-10 11:22:07 +02:00
homarr-renovate[bot] 48d7aaaf1d fix(deps): update mantine monorepo to ^7.9.1 (#457)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-10 11:21:02 +02:00
homarr-renovate[bot] 687556ad96 fix(deps): update dependency zod to ^3.23.8 (#460)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-09 21:35:33 +02:00
homarr-renovate[bot] 8bd08d39ca fix(deps): update dependency glob to ^10.3.14 (#461)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-09 21:35:15 +02:00
homarr-renovate[bot] 382be496ff fix(deps): update tiptap monorepo to v2.3.2 (#459)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-08 19:15:27 +02:00
homarr-renovate[bot] 1172d84223 fix(deps): update dependency @trpc/client to v11.0.0-rc.366 (#454)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-08 06:26:48 +02:00
homarr-renovate[bot] cd4762377a chore(deps): update dependency node (#455)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-08 06:07:24 +02:00
homarr-renovate[bot] cab34052f9 fix(deps): update dependency zod to ^3.23.7 (#456)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-08 06:01:34 +02:00
homarr-renovate[bot] 442d45dd15 chore(deps): update dependency dotenv-cli to ^7.4.2 (#453)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-07 16:16:07 +02:00
homarr-renovate[bot] 3ae96d94a2 fix(deps): update dependency sass to ^1.77.0 (#451)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-07 06:26:12 +02:00
homarr-renovate[bot] 5eb1d716de fix(deps): update dependency drizzle-kit to ^0.20.18 (#449)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-06 21:38:32 +02:00
homarr-renovate[bot] 25ce74a82f chore(deps): update pnpm to v9.1.0 (#447)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-06 21:37:27 +02:00
homarr-renovate[bot] 5ae7bc957c chore(deps): update dependency @types/node to ^20.12.10 (#448)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-06 21:35:09 +02:00
homarr-renovate[bot] 35ba58a0d9 chore(deps): update dependency tsx to v4.9.3 (#435)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-06 21:34:41 +02:00
Meier Lukas c88464498f fix: unable to select integration on board page (#450) 2024-05-06 21:12:55 +02:00
Manuel dc0184af07 feat: log sql queries (#444) 2024-05-06 20:14:51 +02:00
ManuelandMeier Lukas 3cda718474 feat: add contributors to about page (#445)
* feat: add contributors to about page

* fix: remove unnecessary client component for generic-contribution-link-card

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-05-06 20:10:40 +02:00
homarr-renovate[bot] 96158bcb5e fix(deps): update tanstack-query monorepo to ^5.35.1 (#446)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-06 12:03:38 +02:00
Meier Lukas 5d30301b95 feat: show creator on board page (#440) 2024-05-05 22:20:51 +02:00
Meier Lukas 7b31684c84 feat: add job on start option for icons updater (#442) 2024-05-05 22:20:38 +02:00
Meier Lukas 4b80b16b53 fix: integration links in manage section do not work (#441) 2024-05-05 22:20:18 +02:00
homarr-renovate[bot] 08d1b6c638 fix(deps): update tanstack-query monorepo to ^5.34.2 (#439)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-05-05 15:00:47 +02:00
Manuel 60a35e2583 feat: #420 reimplement icon picker (#421) 2024-05-04 23:00:15 +02:00
Manuel 51aaab2f23 feat: add login and logout (#436) 2024-05-04 22:57:13 +02:00
548 changed files with 25252 additions and 8996 deletions

No files matched your search

-15
View File
@@ -1,15 +0,0 @@
{
"$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 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"packageRules": [
{
"matchPackagePatterns": [
"^@homarr/",
"tsx" // Disabled for now as version 0.14.4 did not work with the current version of homarr. It resulted in a ERR_MODULE_NOT_FOUND error
],
"enabled": false
},
{
"matchUpdateTypes": [
"minor",
"patch",
"pin",
"digest"
],
"automerge": true
}
],
"updateInternalDeps": true,
"rangeStrategy": "bump",
"automerge": false,
"baseBranches": [
"dev"
],
"dependencyDashboard": false
}
-31
View File
@@ -1,31 +0,0 @@
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
+20 -2
View File
@@ -1,4 +1,4 @@
name: Code quality analysis
name: "[Quality] Code Analysis"
on:
pull_request:
@@ -8,7 +8,7 @@ on:
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
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
@@ -66,3 +66,21 @@ jobs:
- name: Test
run: pnpm test
- name: 'Report Coverage'
# Set if: always() to also generate the report if tests are failing
# Only works if you set `reportOnFailure: true` in your vite config as specified above
if: always()
uses: davelosert/vitest-coverage-report-action@v2
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
@@ -0,0 +1,14 @@
# https://github.com/webiny/action-conventional-commits?tab=readme-ov-file
name: "[Conventions] Semantic Commits"
on:
pull_request:
branches: [ dev ]
jobs:
build:
name: Conventional Commits
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: webiny/action-conventional-commits@v1.3.0
@@ -0,0 +1,19 @@
name: "[Conventions] Semantic PRs"
on:
pull_request_target:
types:
- opened
- edited
- synchronize
permissions:
pull-requests: read
jobs:
validate-pull-request-title:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,4 +1,4 @@
name: Docker image
name: "[Deployment] Release"
on:
pull_request:
@@ -30,8 +30,20 @@ jobs:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: "Deployment of an image has been triggered"
args: "Deployment of an image has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- uses: actions/checkout@v4
- name: Get Next Version
id: semver
uses: ietf-tools/semver-action@v1
with:
token: ${{ github.token }}
branch: master
- name: Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: "Semver computed next tag to be ${{ steps.semver.outputs.next }}. Current is ${{ steps.semver.outputs.current }}"
- uses: pnpm/action-setup@v2
with:
version: 8
@@ -44,34 +56,27 @@ jobs:
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 }}"
args: "Built application artifacts. Building images..."
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
images: "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
tags: |
type=raw,value=latest
type=raw,value=${{ steps.githubTagAction.outputs.new_tag }}
type=raw,value=${{ steps.semver.outputs.next }}
- name: Build and push
id: buildPushAction
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64,linux/riscv64,linux/arm/v7,linux/arm/v6
context: .
@@ -86,4 +91,4 @@ jobs:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: "Image built with ID ${{ steps.buildPushAction.outputs.imageid }}"
args: "Deployment of image has completed. Image ID is '${{ steps.buildPushAction.outputs.imageid }}'. This was a dry run."
@@ -0,0 +1,82 @@
name: "[Deployment] Automatic Weekly Release"
on:
schedule:
- cron: "0 19 * * 5" # https://crontab.guru/#0_19_*_*_5
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
create-and-merge-pr:
runs-on: ubuntu-latest
steps:
- name: Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: "Automatic release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get Next Version
id: semver
uses: ietf-tools/semver-action@v1
with:
token: ${{ github.token }}
branch: dev
- name: Create pull request
uses: devops-infra/action-pull-request@v0.5.5
id: create-pull-request
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
title: "chore(release): automatic release ${{ steps.semver.outputs.next }}"
new_string: "**This is an automatic release**.<br/>Manual action may be required for major bumps.<br/>Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}``"
source_branch: dev
target_branch: main
label: automerge
- name: Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }} (new behaviour: ${{ steps.semver.outputs.bump }})"
- id: automerge
if: ${{ steps.semver.outputs.bump != 'major' }}
name: automerge
uses: "pascalgn/automerge-action@v0.16.3"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
MERGE_METHOD: merge # we prefer merge commits for merging to master
MERGE_COMMIT_MESSAGE: "chore(release): automatic release ${{ steps.semver.outputs.next }}"
MERGE_DELETE_BRANCH: false # never set to true!
PULL_REQUEST: "${{ steps.create-pull-request.outputs.pr_number }}"
MERGE_RETRIES: 20 # 20 retries * MERGE_RETRY_SLEEP until step fails
MERGE_RETRY_SLEEP: 10000 # 10 seconds * MERGE_RETRIES until step fails
MERGE_REQUIRED_APPROVALS: 0 # do not require approvals
- name: Merged Discord notification
if: ${{ steps.automerge.outputs.mergeResult == 'merged' }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: "Merged PR ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
- name: Major Bump Discord notification
if: ${{ steps.semver.outputs.bump == 'major' }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: "The release PR must be manually merged because the next version is a major version: ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
- name: Discord Fail Notification
if: failure()
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: "The automatic release workflow [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) has failed"
@@ -0,0 +1,26 @@
name: "[Dependency Updates] Auto Approve"
on:
pull_request:
types: [opened, synchronize]
jobs:
approve-renovate-prs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Obtain token
id: obtainToken
uses: tibdex/github-app-token@v2
with:
private_key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
app_id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
- name: Install GitHub CLI
run: sudo apt-get install -y gh
- name: Approve Renovate PRs
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
run: |
for pr in $(gh pr list --author homarr-renovate[bot] --json number --jq .[].number); do
gh pr review $pr --approve --body "Automatically approved by GitHub Action"
done
+6 -3
View File
@@ -14,8 +14,8 @@ coverage
out/
next-env.d.ts
# nest.js
apps/nestjs/dist
# artifacts
packages/db/migrations/*/migrate.cjs
# nitro
.nitro/
@@ -51,4 +51,7 @@ yarn-error.log*
db.sqlite
# logs
*.log
*.log
apps/tasks/tasks.cjs
apps/websocket/wssServer.cjs
+1 -1
View File
@@ -1 +1 @@
20.12.2
20.15.0
+13
View File
@@ -0,0 +1,13 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="db:migration:mysql:generate" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="db:migration:mysql:generate" />
</scripts>
<node-interpreter value="project" />
<package-manager value="pnpm" />
<envs />
<method v="2" />
</configuration>
</component>
+13
View File
@@ -0,0 +1,13 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="db:migration:sqlite:generate" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="db:migration:sqlite:generate" />
</scripts>
<node-interpreter value="project" />
<package-manager value="pnpm" />
<envs />
<method v="2" />
</configuration>
</component>
+13
View File
@@ -0,0 +1,13 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="db:push" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="db:push" />
</scripts>
<node-interpreter value="project" />
<package-manager value="pnpm" />
<envs />
<method v="2" />
</configuration>
</component>
+13
View File
@@ -0,0 +1,13 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="db:studio" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="db:studio" />
</scripts>
<node-interpreter value="project" />
<package-manager value="pnpm" />
<envs />
<method v="2" />
</configuration>
</component>
+13
View File
@@ -0,0 +1,13 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="dev" type="js.build_tools.npm" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<node-interpreter value="project" />
<package-manager value="pnpm" />
<envs />
<method v="2" />
</configuration>
</component>
+13
View File
@@ -0,0 +1,13 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="docker:dev" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="docker:dev" />
</scripts>
<node-interpreter value="project" />
<package-manager value="pnpm" />
<envs />
<method v="2" />
</configuration>
</component>
+13
View File
@@ -0,0 +1,13 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="format" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="format" />
</scripts>
<node-interpreter value="project" />
<package-manager value="pnpm" />
<envs />
<method v="2" />
</configuration>
</component>
+13
View File
@@ -0,0 +1,13 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="format:fix" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="format:fix" />
</scripts>
<node-interpreter value="project" />
<package-manager value="pnpm" />
<envs />
<method v="2" />
</configuration>
</component>
+13
View File
@@ -0,0 +1,13 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="test" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="test" />
</scripts>
<node-interpreter value="project" />
<package-manager value="pnpm" />
<envs />
<method v="2" />
</configuration>
</component>
+13
View File
@@ -0,0 +1,13 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="test:ui" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="test:ui" />
</scripts>
<node-interpreter value="project" />
<package-manager value="pnpm" />
<envs />
<method v="2" />
</configuration>
</component>
+15 -3
View File
@@ -4,11 +4,23 @@
"mode": "auto"
}
],
"eslint.experimental.useFlatConfig": true,
"typescript.tsdk": "node_modules\\typescript\\lib",
"js/ts.implicitProjectConfig.experimentalDecorators": true,
"prettier.configPath": "./tooling/prettier/index.mjs",
"cSpell.words": [
"superjson",
"cqmin",
"homarr",
"trpc"
]
"jellyfin",
"superjson",
"trpc",
"Umami",
"Sonarr"
],
"i18n-ally.dirStructure": "auto",
"i18n-ally.enabledFrameworks": ["next-international"],
"i18n-ally.localesPaths": ["./packages/translation/src/lang/"],
"i18n-ally.enabledParsers": ["ts"],
"i18n-ally.extract.keyMaxLength": 0,
"i18n-ally.keystyle": "flat"
}
+13 -3
View File
@@ -1,4 +1,4 @@
FROM node:20.12.2-alpine AS base
FROM node:20.15.0-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
@@ -35,6 +35,7 @@ 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
@@ -42,6 +43,8 @@ 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/ .
# Copy static data as it is not part of the build
COPY static-data ./static-data
ARG SKIP_ENV_VALIDATION=true
RUN corepack enable pnpm && pnpm turbo run build
@@ -49,10 +52,15 @@ FROM base AS runner
WORKDIR /app
RUN apk add --no-cache redis
RUN mkdir /appdata
RUN mkdir /appdata/db
RUN mkdir /appdata/redis
VOLUME /appdata
# Don't run production as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN chown -R nextjs:nodejs /appdata
USER nextjs
COPY --from=installer /app/apps/nextjs/next.config.mjs .
@@ -63,7 +71,6 @@ COPY --from=installer --chown=nextjs:nodejs /app/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
@@ -71,7 +78,10 @@ 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
COPY --chown=nextjs:nodejs packages/redis/redis.conf /app/redis.conf
ENV DB_URL='/app/db/db.sqlite'
ENV DB_URL='/appdata/db/db.sqlite'
ENV DB_DIALECT='sqlite'
ENV DB_DRIVER='better-sqlite3'
CMD ["sh", "run.sh"]
+2
View File
@@ -1,5 +1,7 @@
# 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
+13
View File
@@ -0,0 +1,13 @@
import baseConfig from "@homarr/eslint-config/base";
import nextjsConfig from "@homarr/eslint-config/nextjs";
import reactConfig from "@homarr/eslint-config/react";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [".next/**"],
},
...baseConfig,
...reactConfig,
...nextjsConfig,
];
+11 -14
View File
@@ -1,6 +1,6 @@
// Importing env files here to validate on build
import "./src/env.mjs";
import "@homarr/auth/env.mjs";
import "./src/env.mjs";
/** @type {import("next").NextConfig} */
const config = {
@@ -9,20 +9,17 @@ const config = {
/** 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",
],
webpack: (config) => {
config.module.rules.push({
test: /\.node$/,
loader: "node-loader",
});
return config;
},
transpilePackages: [
"@homarr/ui",
"@homarr/notifications",
"@homarr/modals",
"@homarr/spotlight",
"@homarr/widgets",
],
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"],
},
+34 -29
View File
@@ -7,20 +7,23 @@
"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",
"lint": "eslint",
"format": "prettier --check . --ignore-path ../../.gitignore",
"start": "pnpm with-env next start",
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --"
},
"dependencies": {
"@homarr/analytics": "workspace:^0.1.0",
"@homarr/api": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/cron-job-status": "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/integrations": "workspace:^0.1.0",
"@homarr/log": "workspace:^",
"@homarr/modals": "workspace:^0.1.0",
"@homarr/notifications": "workspace:^0.1.0",
@@ -29,15 +32,18 @@
"@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",
"@mantine/colors-generator": "^7.11.1",
"@mantine/core": "^7.11.1",
"@mantine/hooks": "^7.11.1",
"@mantine/modals": "^7.11.1",
"@mantine/tiptap": "^7.11.1",
"@homarr/server-settings": "workspace:^0.1.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",
"@tanstack/react-query": "^5.50.1",
"@tanstack/react-query-devtools": "^5.50.1",
"@tanstack/react-query-next-experimental": "5.50.1",
"@tabler/icons-react": "^3.9.0",
"@trpc/client": "next",
"@trpc/next": "next",
"@trpc/react-query": "next",
"@trpc/server": "next",
@@ -45,15 +51,21 @@
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^5.5.0",
"chroma-js": "^2.4.2",
"clsx": "^2.1.1",
"dayjs": "^1.11.11",
"dotenv": "^16.4.5",
"glob": "^10.3.12",
"jotai": "^2.8.0",
"next": "^14.2.3",
"flag-icons": "^7.2.3",
"glob": "^10.4.3",
"jotai": "^2.8.4",
"mantine-react-table": "2.0.0-beta.5",
"next": "^14.2.4",
"postcss-preset-mantine": "^1.15.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"sass": "^1.76.0",
"prismjs": "^1.29.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13",
"react-simple-code-editor": "^0.14.1",
"sass": "^1.77.6",
"superjson": "2.2.1",
"use-deep-compare-effect": "^1.8.1"
},
@@ -62,22 +74,15 @@
"@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/node": "^20.14.10",
"@types/prismjs": "^1.26.4",
"@types/react": "^18.3.3",
"@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"
]
"eslint": "^9.6.0",
"node-loader": "^2.0.0",
"prettier": "^3.3.2",
"typescript": "^5.5.3"
},
"prettier": "@homarr/prettier-config"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="1024" height="1024" version="1.1" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="a">
<circle cx="512" cy="512" r="410" fill="#989898" stroke-width="3.5906"/>
</clipPath>
<clipPath id="b">
<path d="m512 636l0-565 -330 565 330 0z" fill="#000"/>
<path d="m512 388l0 565 330-565 -330 0z" fill="#000"/>
</clipPath>
</defs>
<g id="lidarr-group-logo" transform="translate(5e-6 .26738)">
<circle id="lidarr-black-circle" cx="512" cy="512" r="410" stroke-width="1.8"/>
<path id="lidarr-honeycomb-a" d="m852.12 33.49l23.46 14.44 23.22-14.44 0-28.87 -23.22-14.44 -23.46 14.44 0 28.87zm-46.44-28.87l-23.22-14.44 -23.2 14.44 0 28.87 23.2 14.44 23.22-14.44 0-28.87 23.22-14.44 23.22 14.44m-46.44 28.87l23.22 14.44 23.22-14.44m-46.44 57.49l0 28.87 23.22 14.44 23.22-14.44 0-28.87 -23.22-14.18 -23.22 14.18 -23.22-14.18 -23.2 14.18m92.86 28.87l23.46 14.44 23.22-14.44 0-28.87 -23.22-14.18 -23.46 14.18m23.46-43.05l0 28.87m-46.68-28.87l0 28.87m69.9-72.18l23.22-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.2-14.44 23.22 14.44 0 28.87 23.22 14.44 0 28.87 -23.22 14.18 0 28.87 23.22 14.44 0 28.87 -23.22 14.44 0 28.87 23.22 14.44 0 28.87 -23.22 14.16 0 28.87 23.22 14.44 0 28.87 -23.22 14.44 0 28.87 23.22 14.44 0 28.87 -23.22 14.16 0 28.87 23.22 14.44 0 28.87 -23.22 14.45 0 28.87 23.22 14.44 0 28.87 -23.22 14.16 0 28.87 23.22 14.44 0 28.87 -23.22 14.44 0 28.87 23.22 14.44 0 28.87 -23.22 14.16 0 28.87 23.22 14.44 0 28.87 -23.22 14.44 0 28.87 23.22 14.44 0 28.87 -23.22 14.18 0 28.87 23.22 14.44 0 28.87 -23.22 14.44 -23.22-14.44 -23.2 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.46 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.2 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.46 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.2 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.2 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.46 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.22 14.44 -23.2-14.44 -23.22 14.44 -23.22-14.44 -23.46 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 0-28.87 -23.22-14.44 0-28.87 23.22-14.18 0-28.87 -23.22-14.44 0-28.87 23.22-14.44 0-28.87 -23.22-14.44 0-28.87 23.22-14.16 0-28.87 -23.22-14.44 0-28.87 23.22-14.44 0-28.87 -23.22-14.44 0-28.87 23.22-14.16 0-28.87 -23.22-14.44 0-28.87 23.22-14.45 0-28.87 -23.22-14.44 0-28.87 23.22-14.16 0-28.87 -23.22-14.44 0-28.87 23.22-14.44 0-28.87 -23.22-14.44 0-28.87 23.22-14.16 0-28.87 -23.22-14.44 0-28.87 23.22-14.44 0-28.87 -23.22-14.44 0-28.87 23.22-14.18 0-28.87 -23.22-14.44 0-28.87 23.22-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.46-14.44 23.22 14.44 23.22-14.44 23.2 14.44 23.22-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.46-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.2-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.2-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.46-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.22-14.44 23.22 14.44m-46.44 0l0 28.87 23.22 14.44 23.22-14.44m-46.44 57.49l0 28.87 23.22 14.44 23.22-14.44 0-28.87 -23.22-14.18 -23.22 14.18 -23.22-14.18 -23.22 14.18m116.08-43.05l0 28.87m-46.42-28.87l0 28.87m23.22 100.79l0 28.87 23.2 14.44 23.22-14.44 0-28.87 -23.22-14.44 -23.2 14.44 -23.22-14.44 -23.22 14.44 0 28.87 23.22 14.44 23.22-14.44m46.42-86.61l-23.22 14.44 0 28.87m-23.2-43.31l23.2 14.44m-46.42 0l0 28.87m0 57.74l0 28.87 23.22 14.16 23.2-14.16 0-28.87m46.44-86.61l0 28.87 23.22 14.44 23.46-14.44 0-28.87m-46.68 28.87l-23.22 14.44m-23.22 72.18l23.22 14.16 23.22-14.16 0-28.87 -23.22-14.44m46.44-28.87l0 28.87 23.46 14.44 23.22-14.44 0-28.87 -23.22-14.44m0 57.74l0 28.87 23.22 14.16 23.22-14.16 0-28.87 -23.22-14.44m-69.9 14.44l23.22-14.44m-185.74-201.84l0 28.87 23.22 14.44 23.22-14.44m-139.56-28.87l0 28.87 23.22 14.44 23.22-14.44 0-28.87m0 28.87l23.46 14.44 23.22-14.44m-46.68 57.49l0 28.87 23.46 14.44 23.22-14.44 0-28.87 -23.22-14.18 -23.46 14.18 -23.22-14.18 -23.22 14.18 0 28.87 23.22 14.44 23.22-14.44m-23.22-71.92l0 28.87m93.12-28.87l0 28.87m-46.44-28.87l0 28.87m-116.32-72.18l0 28.87 23.2 14.44 23.22-14.44m-92.86-28.87l0 28.87 23.22 14.44 23.22-14.44m-23.22 43.31l-23.22 14.18 0 28.87 23.22 14.44 23.22-14.44 0-28.87 -23.22-14.18zm69.64 14.18l-23.22-14.18 -23.2 14.18m-23.22-43.05l0 28.87m46.42-28.87l0 28.87m23.22 43.05l-23.22 14.44 0 28.87 23.22 14.44 23.22-14.44 0-28.87m-92.86 0l0 28.87 23.22 14.44 23.2-14.44m-46.42 86.61l23.22 14.16 23.2-14.16 0-28.87 -23.2-14.44 -23.22 14.44 0 28.87 -23.22 14.16 0 28.87 23.22 14.44 23.22-14.44 0-28.87m23.2-43.03l23.22-14.44 0-28.87m-46.42-57.74l23.2 14.44m-23.2 72.18l0-28.87m116.32-43.31l0 28.87 23.22 14.44 23.22-14.44 0-28.87 -23.22-14.44m-93.12 86.61l23.22 14.44 23.22-14.44 0-28.87 -23.22-14.44m46.68 0l-23.46 14.44m23.46 72.18l23.22 14.16 23.22-14.16 0-28.87 -23.22-14.44 -23.22 14.44 0 28.87 -23.46 14.16 0 28.87 -23.22 14.44 0 28.87 23.22 14.44 23.46-14.44 0-28.87 -23.46-14.44m-69.66-43.03l23.22 14.16 23.22-14.16 0-28.87m23.22-14.44l23.46 14.44m23.22-14.44l0-28.87m0 86.34l0 28.87 23.22 14.44 23.22-14.44 0-28.87 -23.22-14.16m-69.9 100.77l0 28.87 23.46 14.44 23.22-14.44 0-28.87 -23.22-14.44m23.22 43.31l23.22 14.44 23.22-14.44 0-28.87 -23.22-14.44 -23.22 14.44m-23.22-43.31l23.22-14.44m23.22 14.44l0 28.87m-93.12-28.87l-23.22-14.44 -23.22 14.44 0 28.87 23.22 14.44 23.22-14.44m-23.22-43.31l0-28.87m-69.64 72.18l-23.22 14.44 0 28.87 23.22 14.44 23.22-14.44 0-28.87 -23.22-14.44zm23.22 4Line truncated
<ellipse cx="512" cy="512" rx="460" ry="460" style="fill:none;stroke-width:99;stroke:#e5e5e5"/>
<ellipse cx="512" cy="512" rx="270" ry="270" style="fill:#e5e5e5;stroke-width:76;stroke:#e5e5e5"/>
<ellipse ry="410" rx="410" cy="512" cx="512" style="fill-opacity:null;fill:none;stroke-width:12;stroke:#009252"/>
<path d="m512 636l0-565 -330 565 330 0z" fill="#000"/>
<path d="m512 388l0 565 330-565 -330 0z" fill="#000"/>
<path id="lidarr-honeycomb-b" d="m852.12 33.49l23.46 14.44 23.22-14.44 0-28.87 -23.22-14.44 -23.46 14.44 0 28.87zm-46.44-28.87l-23.22-14.44 -23.2 14.44 0 28.87 23.2 14.44 23.22-14.44 0-28.87 23.22-14.44 23.22 14.44m-46.44 28.87l23.22 14.44 23.22-14.44m-46.44 57.49l0 28.87 23.22 14.44 23.22-14.44 0-28.87 -23.22-14.18 -23.22 14.18 -23.22-14.18 -23.2 14.18m92.86 28.87l23.46 14.44 23.22-14.44 0-28.87 -23.22-14.18 -23.46 14.18m23.46-43.05l0 28.87m-46.68-28.87l0 28.87m69.9-72.18l23.22-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.2-14.44 23.22 14.44 0 28.87 23.22 14.44 0 28.87 -23.22 14.18 0 28.87 23.22 14.44 0 28.87 -23.22 14.44 0 28.87 23.22 14.44 0 28.87 -23.22 14.16 0 28.87 23.22 14.44 0 28.87 -23.22 14.44 0 28.87 23.22 14.44 0 28.87 -23.22 14.16 0 28.87 23.22 14.44 0 28.87 -23.22 14.45 0 28.87 23.22 14.44 0 28.87 -23.22 14.16 0 28.87 23.22 14.44 0 28.87 -23.22 14.44 0 28.87 23.22 14.44 0 28.87 -23.22 14.16 0 28.87 23.22 14.44 0 28.87 -23.22 14.44 0 28.87 23.22 14.44 0 28.87 -23.22 14.18 0 28.87 23.22 14.44 0 28.87 -23.22 14.44 -23.22-14.44 -23.2 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.46 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.2 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.46 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.2 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.2 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.46 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.22 14.44 -23.2-14.44 -23.22 14.44 -23.22-14.44 -23.46 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 -23.22 14.44 -23.22-14.44 0-28.87 -23.22-14.44 0-28.87 23.22-14.18 0-28.87 -23.22-14.44 0-28.87 23.22-14.44 0-28.87 -23.22-14.44 0-28.87 23.22-14.16 0-28.87 -23.22-14.44 0-28.87 23.22-14.44 0-28.87 -23.22-14.44 0-28.87 23.22-14.16 0-28.87 -23.22-14.44 0-28.87 23.22-14.45 0-28.87 -23.22-14.44 0-28.87 23.22-14.16 0-28.87 -23.22-14.44 0-28.87 23.22-14.44 0-28.87 -23.22-14.44 0-28.87 23.22-14.16 0-28.87 -23.22-14.44 0-28.87 23.22-14.44 0-28.87 -23.22-14.44 0-28.87 23.22-14.18 0-28.87 -23.22-14.44 0-28.87 23.22-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.46-14.44 23.22 14.44 23.22-14.44 23.2 14.44 23.22-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.46-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.2-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.2-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.46-14.44 23.22 14.44 23.22-14.44 23.22 14.44 23.22-14.44 23.22 14.44m-46.44 0l0 28.87 23.22 14.44 23.22-14.44m-46.44 57.49l0 28.87 23.22 14.44 23.22-14.44 0-28.87 -23.22-14.18 -23.22 14.18 -23.22-14.18 -23.22 14.18m116.08-43.05l0 28.87m-46.42-28.87l0 28.87m23.22 100.79l0 28.87 23.2 14.44 23.22-14.44 0-28.87 -23.22-14.44 -23.2 14.44 -23.22-14.44 -23.22 14.44 0 28.87 23.22 14.44 23.22-14.44m46.42-86.61l-23.22 14.44 0 28.87m-23.2-43.31l23.2 14.44m-46.42 0l0 28.87m0 57.74l0 28.87 23.22 14.16 23.2-14.16 0-28.87m46.44-86.61l0 28.87 23.22 14.44 23.46-14.44 0-28.87m-46.68 28.87l-23.22 14.44m-23.22 72.18l23.22 14.16 23.22-14.16 0-28.87 -23.22-14.44m46.44-28.87l0 28.87 23.46 14.44 23.22-14.44 0-28.87 -23.22-14.44m0 57.74l0 28.87 23.22 14.16 23.22-14.16 0-28.87 -23.22-14.44m-69.9 14.44l23.22-14.44m-185.74-201.84l0 28.87 23.22 14.44 23.22-14.44m-139.56-28.87l0 28.87 23.22 14.44 23.22-14.44 0-28.87m0 28.87l23.46 14.44 23.22-14.44m-46.68 57.49l0 28.87 23.46 14.44 23.22-14.44 0-28.87 -23.22-14.18 -23.46 14.18 -23.22-14.18 -23.22 14.18 0 28.87 23.22 14.44 23.22-14.44m-23.22-71.92l0 28.87m93.12-28.87l0 28.87m-46.44-28.87l0 28.87m-116.32-72.18l0 28.87 23.2 14.44 23.22-14.44m-92.86-28.87l0 28.87 23.22 14.44 23.22-14.44m-23.22 43.31l-23.22 14.18 0 28.87 23.22 14.44 23.22-14.44 0-28.87 -23.22-14.18zm69.64 14.18l-23.22-14.18 -23.2 14.18m-23.22-43.05l0 28.87m46.42-28.87l0 28.87m23.22 43.05l-23.22 14.44 0 28.87 23.22 14.44 23.22-14.44 0-28.87m-92.86 0l0 28.87 23.22 14.44 23.2-14.44m-46.42 86.61l23.22 14.16 23.2-14.16 0-28.87 -23.2-14.44 -23.22 14.44 0 28.87 -23.22 14.16 0 28.87 23.22 14.44 23.22-14.44 0-28.87m23.2-43.03l23.22-14.44 0-28.87m-46.42-57.74l23.2 14.44m-23.2 72.18l0-28.87m116.32-43.31l0 28.87 23.22 14.44 23.22-14.44 0-28.87 -23.22-14.44m-93.12 86.61l23.22 14.44 23.22-14.44 0-28.87 -23.22-14.44m46.68 0l-23.46 14.44m23.46 72.18l23.22 14.16 23.22-14.16 0-28.87 -23.22-14.44 -23.22 14.44 0 28.87 -23.46 14.16 0 28.87 -23.22 14.44 0 28.87 23.22 14.44 23.46-14.44 0-28.87 -23.46-14.44m-69.66-43.03l23.22 14.16 23.22-14.16 0-28.87m23.22-14.44l23.46 14.44m23.22-14.44l0-28.87m0 86.34l0 28.87 23.22 14.44 23.22-14.44 0-28.87 -23.22-14.16m-69.9 100.77l0 28.87 23.46 14.44 23.22-14.44 0-28.87 -23.22-14.44m23.22 43.31l23.22 14.44 23.22-14.44 0-28.87 -23.22-14.44 -23.22 14.44m-23.22-43.31l23.22-14.44m23.22 14.44l0 28.87m-93.12-28.87l-23.22-14.44 -23.22 14.44 0 28.87 23.22 14.44 23.22-14.44m-23.22-43.31l0-28.87m-69.64 72.18l-23.22 14.44 0 28.87 23.22 14.44 23.22-14.44 0-28.87 -23.22-14.44zm23.22 4Line truncated
<path d="m512 636l0-565 -330 565 330 0z" style="fill:none;stroke-width:16;stroke:#009252;"/>
<path d="m512 388l0 565 330-565 -330 0z" style="fill:none;stroke-width:16;stroke:#009252;"/>
<ellipse cx="512" cy="512" rx="150" ry="150" style="fill-opacity:null;fill:#009252;stroke-opacity:null;stroke-width:null;stroke:null"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 54 KiB

@@ -0,0 +1 @@
<svg height="1024" viewBox="0 0 1024 1024" width="1024" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="m0 0h1024v1024h-1024z"/><clipPath id="b"><use clip-rule="evenodd" xlink:href="#a"/></clipPath></defs><g clip-path="url(#b)"><use fill="none" xlink:href="#a"/><g transform="translate(70 21.00012)"><path d="m105.302 154.943 7.522 714.549c-60.173 7.522-105.30242-22.565-105.30242-82.737l-7.52158-594.205c0-188.03894 172.996-233.1684 278.298-157.9526l534.032 308.3846c75.216 52.651 90.259 150.431 52.651 218.125-7.521-52.651-30.086-82.737-75.216-112.823l-601.726-338.471c-45.129-30.0862-82.737-22.5646-82.737 45.13z" fill="#24292e"/><path d="m0 376.079c45.1295 15.043 90.259 7.521 127.867-15.043l616.769-361.036c37.608 52.651 30.087 105.302-15.043 135.388l-518.989 300.863c-75.216 37.608-172.9961 0-210.604-60.172z" fill="#24292e" transform="translate(60.17249 531.0214)"/><path d="m0 413.687 368.557-210.604-361.03543-203.083z" fill="#ffc230" transform="translate(240.6902 282.8092)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1 @@
<svg version="1.1" viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.97895 0 0 .97895 -2.2026 -2.2026)"><g stroke="#443c3c" stroke-width="1.5"><circle cx="513" cy="513" r="510" fill="#eee"/><circle cx="513" cy="513" r="440" fill="#443c3c"/><circle cx="513" cy="513" r="387" fill="#8e2222"/></g><g stroke-width="1.5"><circle cx="513" cy="513" r="378" fill="#eee" stroke="#8e2222"/><circle cx="511.67" cy="514.33" r="265" fill="#443c3c" stroke="#443c3c"/></g><g stroke="#8e2222"><path d="m176.71 682.24-5.71-356.67c0.634-53.106 17.5-47.829 30.454-49.405 198.58 10.83 270.91 71.252 275.35 73.499 13.323 5.018 20.937 31.782 20.302 31.123 0.634 0.658 4.441 420.6 3.807 419.94 3.172 22.455-13.323 21.002-13.958 20.343-124.99-98.152-297.56-122.85-298.19-123.51-12.055-0.795-12.055-15.326-12.055-15.326zm670.08 0.82 5.711-357.54c-0.635-53.236-17.501-47.946-30.456-49.526-198.6 10.857-270.93 71.426-275.38 73.679-13.325 5.03-20.939 31.859-20.304 31.199-0.635 0.66-4.442 421.63-3.807 420.97-3.173 22.51 13.325 21.053 13.959 20.393 125-98.392 297.58-123.15 298.22-123.81 12.056-0.797 12.056-15.363 12.056-15.363z" fill="#eee" stroke-width="10"/><path d="m174.14 739.57-5.802-356.67c0.645-53.106 17.782-47.829 30.945-49.405 201.79 10.83 275.28 71.252 279.8 73.499 13.539 5.018 21.275 31.782 20.63 31.123 0.645 0.658 4.513 420.6 3.868 419.94 3.224 22.455-13.539 21.002-14.183 20.343-127-98.152-302.36-122.85-303.01-123.51-12.249-0.795-12.249-15.326-12.249-15.326zm675.22 0.49 5.803-357.54c-0.645-53.236-17.784-47.946-30.948-49.526-201.81 10.857-275.31 71.426-279.82 73.679-13.54 5.03-21.277 31.859-20.632 31.199-0.645 0.66-4.513 421.63-3.869 420.97-3.224 22.51 13.54 21.053 14.184 20.393 127.02-98.392 302.39-123.15 303.03-123.81 12.25-0.797 12.25-15.363 12.25-15.363z" fill="#8e2222" stroke-width="5"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><g clip-rule="evenodd"><path fill="#eee" fill-rule="evenodd" d="M47.978 24c0 6.602-2.331 12.26-6.993 16.974a3.773 3.773 0 0 1-.52.509 20.53 20.53 0 0 1-2.435 2.047C33.988 46.51 29.318 48 24.022 48c-5.304 0-9.966-1.49-13.986-4.47a21.726 21.726 0 0 1-2.988-2.556c-3.622-3.6-5.846-7.783-6.672-12.548-.162-.93-.27-1.874-.32-2.833a38.27 38.27 0 0 1 0-3.197c0-.052.014-.104.044-.155.346-5.887 2.662-10.973 6.948-15.259C11.762 2.327 17.42 0 24.022 0c6.624 0 12.279 2.327 16.963 6.982 4.662 4.743 6.993 10.416 6.993 17.018z"/><path fill="#3a3f51" fill-rule="evenodd" d="m43.098 9.405-4.957 4.957c-2.899 2.899-3.153 5.422-3.153 9.87 0 3.97.63 7.602 3.585 10.556 2.156 2.157 4.204 4.194 4.204 4.194a27.962 27.962 0 0 1-1.792 1.992 3.773 3.773 0 0 1-.52.509 20.05 20.05 0 0 1-1.749 1.538l-3.883-3.884c-3.452-3.452-6.196-3.784-10.756-3.784-4.375 0-7.352.403-10.556 3.607a2715.831 2715.831 0 0 0-4.105 4.116 21.196 21.196 0 0 1-2.368-2.102 27.739 27.739 0 0 1-1.737-1.903s2.168-2.18 4.238-4.25c3.066-3.065 3.563-6.62 3.563-10.589 0-3.872-.636-7.485-3.452-10.301C7.705 11.975 5 9.284 5 9.284a25.954 25.954 0 0 1 2.047-2.302A29.761 29.761 0 0 1 9.04 5.201l4.504 4.503c2.877 2.877 6.565 3.618 10.533 3.618 4.087 0 7.763-.791 10.756-3.784 1.84-1.841 4.27-4.26 4.27-4.26a25.168 25.168 0 0 1 1.882 1.704c.767.782 1.471 1.59 2.113 2.423z"/><path fill="#0cf" fill-rule="evenodd" d="M17.438 25.228a6.986 6.986 0 0 1-.1-1.228c0-.155.005-.303.012-.443 0-.014.004-.029.011-.044.096-1.63.738-3.039 1.925-4.227 1.306-1.29 2.874-1.936 4.703-1.936 1.837 0 3.404.645 4.703 1.936 1.29 1.313 1.936 2.884 1.936 4.714s-.645 3.397-1.936 4.703c-.045.051-.093.1-.144.143a6.056 6.056 0 0 1-.675.565c-1.121.826-2.416 1.239-3.884 1.239s-2.759-.413-3.873-1.24a5.818 5.818 0 0 1-.83-.707c-1.003-.996-1.619-2.155-1.848-3.475z"/><path fill="none" stroke="#0cf" stroke-miterlimit="1" stroke-width=".4426" d="m34.943 13.223-3.32 3.242M6.834 7.198l9.044 9.012M34.6 34.855l6.154 6.369m.41-34.056-6.22 6.056M7.18 41.107l6.053-6.063"/><path fill="none" stroke="#0cf" stroke-miterlimit="1" stroke-width="1.5491" d="m34.943 13.223-3.75 3.806m-18.12-3.617 3.806 3.795m-3.662 17.854 3.706-3.851m13.705-.309 3.99 3.971"/></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="100px" height="54px" viewBox="0 0 100 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Logo tvdb</title>
<g id="Logo-tvdb" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M0,5.09590006 C0,1.81024006 2.9636,-0.441498938 6.46228,0.0733078623 L6.46228,0.0733078623 L52.10124,6.03470006 C54.15254,6.33652006 55.78724,8.54666006 55.78724,10.9536001 L55.78724,10.9536001 L55.78654,17.1835001 C51.94104,19.7605001 49.42044,24.0737001 49.42044,28.9596001 C49.42044,33.8924001 51.87974,38.1680001 55.78724,40.7361001 L55.78724,40.7361001 L55.78724,43.4756001 C55.78724,45.8825001 54.15254,48.0927001 52.10124,48.3945001 L52.10124,48.3945001 L11.60314,53.9266001 C8.10444,54.4417001 5.14084,52.1897001 5.14084,48.9040001 L5.14084,48.9040001 Z M19.68044,10.8218001 L13.66114,10.8218001 L13.66114,18.7064001 L9.84244,18.7064001 L9.84244,23.2621001 L13.66114,23.2621001 L13.66114,32.0227001 C13.4846091,37.5274601 15.6467584,39.9923503 20.6149401,40.0386142 L25.25134,40.0387001 L25.25134,35.4830001 L22.87064,35.4830001 C20.17484,35.3516001 19.59134,34.5631001 19.68074,31.0149001 L19.68074,23.2617001 L27.08014,23.2617001 L33.93424,40.0384001 L40.40294,40.0384001 L49.83694,18.7061001 L43.45734,18.7061001 L37.34794,33.3806001 L31.77694,18.7064001 L19.68044,18.7064001 L19.68044,10.8218001 Z" id="Combined-Shape" fill="#6CD591" fill-rule="nonzero"></path>
<path d="M88.60974,18.2771001 C92.51784,18.2771001 95.12314,19.2407001 97.09994,21.4310001 C98.71734,23.1831001 99.57074,25.7677001 99.57074,28.6584001 C99.57074,32.8634001 97.86394,36.1487001 94.76414,38.0323001 C92.74234,39.2590001 90.99054,39.6094001 87.03734,39.6094001 L77.24404,39.6094001 L77.24404,10.3925001 L83.26404,10.3925001 L83.26404,18.2771001 L88.60974,18.2771001 Z M83.26404,35.0537001 L87.71094,35.0537001 C91.26004,35.0537001 93.41634,32.6884001 93.41634,28.8334001 C93.41634,24.8035001 91.52964,22.8324001 87.71094,22.8324001 L83.26404,22.8324001 L83.26404,35.0537001 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
<path d="M68.01354,10.3925001 L74.03354,10.3925001 L74.03354,39.6094001 L63.65594,39.6094001 C59.43354,39.6094001 57.41174,38.9962001 55.25524,37.1126001 C53.05394,35.1416001 51.93124,32.3384001 51.93124,28.7898001 C51.93124,25.1102001 53.14404,22.3070001 55.70494,20.2481001 C57.32204,18.9342001 59.52364,18.2771001 62.35354,18.2771001 L68.01384,18.2771001 L68.01384,10.3925001 L68.01354,10.3925001 Z M68.01354,22.8327001 L63.65594,22.8327001 C60.15224,22.8327001 58.04064,25.0667001 58.04064,28.7898001 C58.04064,32.6884001 60.19654,35.0537001 63.65594,35.0537001 L68.01354,35.0537001 L68.01354,22.8327001 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

@@ -0,0 +1 @@
<svg viewBox="0 0 90 90" xmlns="http://www.w3.org/2000/svg"><g fill="none"><path fill="#31BEEC" d="M90 38.197v19.137L48.942 80.999V61.864z"/><path d="M41.086 61.863V81L0 57.333V38.197l18.566 10.687c.02.016.043.03.067.04l22.453 12.94Z" fill="#0095D5"/><path fill="#AEADAE" d="m61.621 45.506-16.607 9.576-16.622-9.576 16.622-9.575z"/><path fill="#0095D5" d="M86.086 31.416 69.464 40.99 48.942 29.15V10z"/><path fill="#31BEEC" d="M41.086 10v19.15l-20.55 11.827-16.621-9.561z"/></g></svg>

After

Width:  |  Height:  |  Size: 484 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><linearGradient id="a" x1="100%" x2="0" y1="0" y2="100%" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff8d30"/><stop offset="1" stop-color="#e32929"/></linearGradient></defs><circle cx="50%" cy="50%" r="50%" fill="url(#a)"/><path fill="#fff" d="M246.6 200.8h18.7v110.6h-18.7zm-182.3 0H83v110.7H64.3zm91.1 123.9h18.7V367h-18.7zm-45.7-47.5h18.7v68.5h-18.7zm91.2 0h18.6v68.4h-18.6zm228.2-76.5h18.7v110.7h-18.7zM338 145.5h18.7v42.3H338zm45.7 21.2h18.7v68.2h-18.7zm-91.5 0h18.7v68.1h-18.7z"/></svg>

After

Width:  |  Height:  |  Size: 577 B

@@ -3,10 +3,7 @@ import type { PropsWithChildren } from "react";
import { defaultLocale } from "@homarr/translation";
import { I18nProviderClient } from "@homarr/translation/client";
export const NextInternationalProvider = ({
children,
locale,
}: PropsWithChildren<{ locale: string }>) => {
export const NextInternationalProvider = ({ children, locale }: PropsWithChildren<{ locale: string }>) => {
return (
<I18nProviderClient locale={locale} fallback={defaultLocale}>
{children}
@@ -9,9 +9,6 @@ interface AuthProviderProps {
session: Session | null;
}
export const AuthProvider = ({
children,
session,
}: PropsWithChildren<AuthProviderProps>) => {
export const AuthProvider = ({ children, session }: PropsWithChildren<AuthProviderProps>) => {
return <SessionProvider session={session}>{children}</SessionProvider>;
};
@@ -5,12 +5,7 @@ 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 { createWSClient, loggerLink, unstable_httpBatchStreamLink, wsLink } from "@trpc/client";
import superjson from "superjson";
import type { AppRouter } from "@homarr/api";
@@ -37,8 +32,7 @@ export function TRPCReactProvider(props: PropsWithChildren) {
links: [
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === "development" ||
(opts.direction === "down" && opts.result instanceof Error),
process.env.NODE_ENV === "development" || (opts.direction === "down" && opts.result instanceof Error),
}),
(args) => {
return ({ op, next }) => {
@@ -69,9 +63,7 @@ export function TRPCReactProvider(props: PropsWithChildren) {
return (
<clientApi.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<ReactQueryStreamedHydration transformer={superjson}>
{props.children}
</ReactQueryStreamedHydration>
<ReactQueryStreamedHydration transformer={superjson}>{props.children}</ReactQueryStreamedHydration>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</clientApi.Provider>
@@ -0,0 +1,85 @@
"use client";
import { useRouter } from "next/navigation";
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import { useZodForm } 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";
interface RegistrationFormProps {
invite: {
id: string;
token: string;
};
}
export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
const t = useScopedI18n("user");
const router = useRouter();
const { mutate, isPending } = clientApi.user.register.useMutation();
const form = useZodForm(validation.user.registration, {
initialValues: {
username: "",
password: "",
confirmPassword: "",
},
});
const handleSubmit = (values: z.infer<typeof validation.user.registration>) => {
mutate(
{
...values,
inviteId: invite.id,
token: invite.token,
},
{
onSuccess() {
showSuccessNotification({
title: t("action.register.notification.success.title"),
message: t("action.register.notification.success.message"),
});
router.push("/auth/login");
},
onError(error) {
const message =
error.data?.code === "CONFLICT"
? t("error.usernameTaken")
: t("action.register.notification.error.message");
showErrorNotification({
title: t("action.register.notification.error.title"),
message,
});
},
},
);
};
return (
<Stack gap="xl">
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="lg">
<TextInput label={t("field.username.label")} autoComplete="off" {...form.getInputProps("username")} />
<PasswordInput
label={t("field.password.label")}
autoComplete="new-password"
{...form.getInputProps("password")}
/>
<PasswordInput
label={t("field.passwordConfirm.label")}
autoComplete="new-password"
{...form.getInputProps("confirmPassword")}
/>
<Button type="submit" fullWidth loading={isPending}>
{t("action.register.label")}
</Button>
</Stack>
</form>
</Stack>
);
};
@@ -0,0 +1,66 @@
import { notFound } from "next/navigation";
import { Card, Center, Stack, Text, Title } from "@mantine/core";
import { auth } from "@homarr/auth/next";
import { and, db, eq } from "@homarr/db";
import { invites } from "@homarr/db/schema/sqlite";
import { getScopedI18n } from "@homarr/translation/server";
import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo";
import { RegistrationForm } from "./_registration-form";
interface InviteUsagePageProps {
params: {
id: string;
};
searchParams: {
token: string;
};
}
export default async function InviteUsagePage({ params, searchParams }: InviteUsagePageProps) {
const session = await auth();
if (session) notFound();
const invite = await db.query.invites.findFirst({
where: and(eq(invites.id, params.id), eq(invites.token, searchParams.token)),
columns: {
id: true,
token: true,
expirationDate: true,
},
with: {
creator: {
columns: {
name: true,
},
},
},
});
if (!invite || invite.expirationDate < new Date()) notFound();
const t = await getScopedI18n("user.page.invite");
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">
<RegistrationForm invite={invite} />
</Card>
<Text size="xs" c="gray.5" ta="center">
{t("description", { username: invite.creator.name })}
</Text>
</Stack>
</Center>
);
}
@@ -2,36 +2,31 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import {
Alert,
Button,
PasswordInput,
rem,
Stack,
TextInput,
} from "@mantine/core";
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 { useZodForm } 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";
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
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),
const form = useZodForm(validation.user.signIn, {
initialValues: {
name: "",
password: "",
},
});
const handleSubmit = async (values: FormType) => {
const handleSubmitAsync = async (values: z.infer<typeof validation.user.signIn>) => {
setIsLoading(true);
setError(undefined);
await signIn("credentials", {
@@ -39,33 +34,36 @@ export const LoginForm = () => {
redirect: false,
callbackUrl: "/",
})
.then((response) => {
.then(async (response) => {
if (!response?.ok || response.error) {
throw response?.error;
}
void router.push("/");
showSuccessNotification({
title: t("action.login.notification.success.title"),
message: t("action.login.notification.success.message"),
});
await revalidatePathActionAsync("/");
router.push("/");
})
.catch((error: Error | string) => {
setIsLoading(false);
setError(error.toString());
showErrorNotification({
title: t("action.login.notification.error.title"),
message: t("action.login.notification.error.message"),
});
});
};
return (
<Stack gap="xl">
<form onSubmit={form.onSubmit((values) => void handleSubmit(values))}>
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
<Stack gap="lg">
<TextInput
label={t("field.username.label")}
{...form.getInputProps("name")}
/>
<PasswordInput
label={t("field.password.label")}
{...form.getInputProps("password")}
/>
<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")}
{t("action.login.label")}
</Button>
</Stack>
</form>
@@ -78,5 +76,3 @@ export const LoginForm = () => {
</Stack>
);
};
type FormType = z.infer<typeof validation.user.signIn>;
@@ -3,7 +3,7 @@ import { api } from "@homarr/api/server";
import { createBoardContentPage } from "../_creator";
export default createBoardContentPage<{ locale: string }>({
async getInitialBoard() {
return await api.board.getDefaultBoard();
async getInitialBoardAsync() {
return await api.board.getHomeBoard();
},
});
@@ -1,6 +1,6 @@
import definition from "./_definition";
const { generateMetadata, page } = definition;
const { generateMetadataAsync: generateMetadata, page } = definition;
export default page;
@@ -3,7 +3,7 @@ import { api } from "@homarr/api/server";
import { createBoardContentPage } from "../_creator";
export default createBoardContentPage<{ locale: string; name: string }>({
async getInitialBoard({ name }) {
async getInitialBoardAsync({ name }) {
return await api.board.getBoardByName({ name });
},
});
@@ -1,6 +1,6 @@
import definition from "./_definition";
const { generateMetadata, page } = definition;
const { generateMetadataAsync: generateMetadata, page } = definition;
export default page;
@@ -1,10 +1,11 @@
"use client";
import { useCallback, useRef } from "react";
import { useCallback, useEffect, useRef } from "react";
import { Box, LoadingOverlay, Stack } from "@mantine/core";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { DDGridStack } from "@homarr/gridstack";
import { BoardCategorySection } from "~/components/board/sections/category-section";
import { BoardEmptySection } from "~/components/board/sections/empty-section";
@@ -18,9 +19,7 @@ export const updateBoardName = (name: string | null) => {
boardName = name;
};
type UpdateCallback = (
prev: RouterOutputs["board"]["getDefaultBoard"],
) => RouterOutputs["board"]["getDefaultBoard"];
type UpdateCallback = (prev: RouterOutputs["board"]["getHomeBoard"]) => RouterOutputs["board"]["getHomeBoard"];
export const useUpdateBoard = () => {
const utils = clientApi.useUtils();
@@ -42,15 +41,47 @@ export const useUpdateBoard = () => {
};
};
const dd = new DDGridStack();
export const ClientBoard = () => {
const board = useRequiredBoard();
const isReady = useIsBoardReady();
const { updateBoard } = useUpdateBoard();
const sortedSections = board.sections.sort(
(sectionA, sectionB) => sectionA.position - sectionB.position,
);
const sortedSections = board.sections.sort((sectionA, sectionB) => sectionA.position - sectionB.position);
const ref = useRef<HTMLDivElement>(null);
const boxRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!boxRef.current) return;
dd.droppable(boxRef.current, {
accept: () => true,
})
.on(boxRef.current, "dropover", (event, el, helper) => {
console.log("dropover", event, el, helper);
})
.on(boxRef.current, "dropout", (event, el, helper) => {
console.log("dropout", event, el, helper);
})
.on(boxRef.current, "drop", (event, el, helper) => {
console.log("drop", event, el, helper);
const itemId = el.getAttribute("data-id");
if (!itemId) return;
// Same as removeItem
updateBoard((previous) => {
return {
...previous,
// Filter removed item out of items array
sections: previous.sections.map((section) => ({
...section,
items: section.items.filter((item) => item.id !== itemId),
})),
};
});
});
}, []);
return (
<Box h="100%" pos="relative">
@@ -61,24 +92,13 @@ export const ClientBoard = () => {
loaderProps={{ size: "lg" }}
h={fullHeightWithoutHeaderAndFooter}
/>
<Stack
ref={ref}
h="100%"
style={{ visibility: isReady ? "visible" : "hidden" }}
>
<Stack ref={ref} h="100%" style={{ visibility: isReady ? "visible" : "hidden" }}>
<Box ref={boxRef} w="100%" h={80} bg="red"></Box>
{sortedSections.map((section) =>
section.kind === "empty" ? (
<BoardEmptySection
key={section.id}
section={section}
mainRef={ref}
/>
<BoardEmptySection key={section.id} section={section} mainRef={ref} />
) : (
<BoardCategorySection
key={section.id}
section={section}
mainRef={ref}
/>
<BoardCategorySection key={section.id} section={section} mainRef={ref} />
),
)}
</Stack>
@@ -1,13 +1,7 @@
"use client";
import type { PropsWithChildren } from "react";
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import type { Dispatch, PropsWithChildren, SetStateAction } from "react";
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import type { RouterOutputs } from "@homarr/api";
@@ -16,9 +10,11 @@ import { clientApi } from "@homarr/api/client";
import { updateBoardName } from "./_client";
const BoardContext = createContext<{
board: RouterOutputs["board"]["getDefaultBoard"];
board: RouterOutputs["board"]["getHomeBoard"];
isReady: boolean;
markAsReady: (id: string) => void;
isEditMode: boolean;
setEditMode: Dispatch<SetStateAction<boolean>>;
} | null>(null);
export const BoardProvider = ({
@@ -30,11 +26,11 @@ export const BoardProvider = ({
const pathname = usePathname();
const utils = clientApi.useUtils();
const [readySections, setReadySections] = useState<string[]>([]);
const [isEditMode, setEditMode] = useState(false);
const { data } = clientApi.board.getBoardByName.useQuery(
{ name: initialBoard.name },
{
initialData: initialBoard,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
},
@@ -52,18 +48,11 @@ export const BoardProvider = ({
}, [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
setReadySections((previous) => previous.filter((id) => data.sections.some((section) => section.id === id)));
}, [data.sections.length, setReadySections]);
const markAsReady = useCallback((id: string) => {
setReadySections((previous) =>
previous.includes(id) ? previous : [...previous, id],
);
setReadySections((previous) => (previous.includes(id) ? previous : [...previous, id]));
}, []);
return (
@@ -72,6 +61,8 @@ export const BoardProvider = ({
board: data,
isReady: data.sections.length === readySections.length,
markAsReady,
isEditMode,
setEditMode,
}}
>
{children}
@@ -114,3 +105,13 @@ export const useOptionalBoard = () => {
return context?.board;
};
export const useEditMode = () => {
const context = useContext(BoardContext);
if (!context) {
throw new Error("Board is required");
}
return [context.isEditMode, context.setEditMode] as const;
};
@@ -1,11 +1,12 @@
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 { getI18n } from "@homarr/translation/server";
import { createMetaTitle } from "~/metadata";
import { createBoardLayout } from "../_layout-creator";
import type { Board } from "../_types";
import { ClientBoard } from "./_client";
@@ -14,33 +15,28 @@ import { BoardContentHeaderActions } from "./_header-actions";
export type Params = Record<string, unknown>;
interface Props<TParams extends Params> {
getInitialBoard: (params: TParams) => Promise<Board>;
getInitialBoardAsync: (params: TParams) => Promise<Board>;
}
export const createBoardContentPage = <
TParams extends Record<string, unknown>,
>({
getInitialBoard,
export const createBoardContentPage = <TParams extends Record<string, unknown>>({
getInitialBoardAsync: getInitialBoard,
}: Props<TParams>) => {
return {
layout: createBoardLayout({
headerActions: <BoardContentHeaderActions />,
getInitialBoard,
getInitialBoardAsync: getInitialBoard,
isBoardContentPage: true,
}),
page: () => {
return <ClientBoard />;
},
generateMetadata: async ({
params,
}: {
params: TParams;
}): Promise<Metadata> => {
generateMetadataAsync: async ({ params }: { params: TParams }): Promise<Metadata> => {
try {
const board = await getInitialBoard(params);
const t = await getI18n();
return {
title: board.metaTitle ?? `${capitalize(board.name)} board | Homarr`,
title: board.metaTitle ?? createMetaTitle(t("board.content.metaTitle", { boardName: board.name })),
icons: {
icon: board.faviconImageUrl ? board.faviconImageUrl : undefined,
},
@@ -0,0 +1,9 @@
"use client";
import { useRequiredBoard } from "./_context";
export const CustomCss = () => {
const board = useRequiredBoard();
return <style>{board.customCss}</style>;
};
@@ -12,27 +12,22 @@ import {
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 { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { revalidatePathAction } from "~/app/revalidatePathAction";
import { editModeAtom } from "~/components/board/editMode";
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
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";
import { useEditMode, useRequiredBoard } from "./_context";
export const BoardContentHeaderActions = () => {
const isEditMode = useAtomValue(editModeAtom);
const [isEditMode] = useEditMode();
const board = useRequiredBoard();
const { hasChangeAccess } = useBoardPermissions(board);
@@ -54,8 +49,7 @@ export const BoardContentHeaderActions = () => {
};
const AddMenu = () => {
const { openModal: openCategoryEditModal } =
useModalAction(CategoryEditModal);
const { openModal: openCategoryEditModal } = useModalAction(CategoryEditModal);
const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal);
const { addCategoryToEnd } = useCategoryActions();
const t = useI18n();
@@ -95,22 +89,14 @@ const AddMenu = () => {
</HeaderButton>
</Menu.Target>
<Menu.Dropdown style={{ transform: "translate(-3px, 0)" }}>
<Menu.Item
leftSection={<IconBox size={20} />}
onClick={handleSelectItem}
>
<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.Item leftSection={<IconPackageImport size={20} />}>{t("item.action.import")}</Menu.Item>
<Menu.Divider />
<Menu.Item
leftSection={<IconBoxAlignTop size={20} />}
onClick={handleAddCategory}
>
<Menu.Item leftSection={<IconBoxAlignTop size={20} />} onClick={handleAddCategory}>
{t("section.category.action.create")}
</Menu.Item>
</Menu.Dropdown>
@@ -119,28 +105,27 @@ const AddMenu = () => {
};
const EditModeMenu = () => {
const [isEditMode, setEditMode] = useAtom(editModeAtom);
const [isEditMode, setEditMode] = useEditMode();
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 { 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 revalidatePathActionAsync(`/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);
@@ -149,11 +134,7 @@ const EditModeMenu = () => {
return (
<HeaderButton onClick={toggle} loading={isPending}>
{isEditMode ? (
<IconPencilOff stroke={1.5} />
) : (
<IconPencil stroke={1.5} />
)}
{isEditMode ? <IconPencilOff stroke={1.5} /> : <IconPencil stroke={1.5} />}
</HeaderButton>
);
};
@@ -22,9 +22,7 @@ export const BoardMantineProvider = ({ children }: PropsWithChildren) => {
};
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 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);
@@ -35,6 +33,7 @@ export const generateColors = (hex: string) => {
return rgbaColors.map((color) => {
return (
"#" +
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
color
.split("(")[1]!
.replaceAll(" ", "")
@@ -5,7 +5,7 @@ import { createBoardLayout } from "../_layout-creator";
export default createBoardLayout<{ locale: string; name: string }>({
headerActions: <BoardOtherHeaderActions />,
async getInitialBoard({ name }) {
async getInitialBoardAsync({ name }) {
return await api.board.getBoardByName({ name });
},
isBoardContentPage: false,
@@ -44,11 +44,7 @@ export const AccessSettingsContent = ({ board, initialPermissions }: Props) => {
<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}
/>
<TabItem value="inherited" count={initialPermissions.inherited.length} icon={IconUserDown} />
</Tabs.List>
<Tabs.Panel value="user">
@@ -1,21 +1,8 @@
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 { 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";
@@ -38,12 +25,7 @@ interface BoardAccessSelectRowProps {
onCountChange: OnCountChange;
}
export const BoardAccessSelectRow = ({
itemContent,
permission,
index,
onCountChange,
}: BoardAccessSelectRowProps) => {
export const BoardAccessSelectRow = ({ itemContent, permission, index, onCountChange }: BoardAccessSelectRowProps) => {
const tRoot = useI18n();
const tPermissions = useScopedI18n("board.setting.section.access.permission");
const form = useFormContext();
@@ -61,11 +43,7 @@ export const BoardAccessSelectRow = ({
<TableTr>
<TableTd w={{ sm: 128, lg: 256 }}>{itemContent}</TableTd>
<TableTd>
<Flex
direction={{ base: "column", xs: "row" }}
align={{ base: "end", xs: "center" }}
wrap="nowrap"
>
<Flex direction={{ base: "column", xs: "row" }} align={{ base: "end", xs: "center" }} wrap="nowrap">
<Select
allowDeselect={false}
flex="1"
@@ -93,10 +71,7 @@ interface BoardAccessDisplayRowProps {
permission: BoardPermission | "board-full";
}
export const BoardAccessDisplayRow = ({
itemContent,
permission,
}: BoardAccessDisplayRowProps) => {
export const BoardAccessDisplayRow = ({ itemContent, permission }: BoardAccessDisplayRowProps) => {
const tPermissions = useScopedI18n("board.setting.section.access.permission");
const Icon = icons[permission];
@@ -106,10 +81,7 @@ export const BoardAccessDisplayRow = ({
<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))"
/>
<Icon size="1rem" color="var(--input-section-color, var(--mantine-color-dimmed))" />
</Flex>
<Text size="sm">{tPermissions(`item.${permission}.label`)}</Text>
</Group>
@@ -131,9 +103,7 @@ const RenderOption: SelectProps["renderOption"] = ({ option, checked }) => {
<Group flex="1" gap="xs" wrap="nowrap">
<Icon {...iconProps} />
{option.label}
{checked && (
<IconCheck style={{ marginInlineStart: "auto" }} {...iconProps} />
)}
{checked && <IconCheck style={{ marginInlineStart: "auto" }} {...iconProps} />}
</Group>
);
};
@@ -8,7 +8,6 @@ export interface BoardAccessFormType {
}[];
}
export const [FormProvider, useFormContext, useForm] =
createFormContext<BoardAccessFormType>();
export const [FormProvider, useFormContext, useForm] = createFormContext<BoardAccessFormType>();
export type OnCountChange = (callback: (prev: number) => number) => void;
@@ -1,16 +1,6 @@
import { useCallback, useState } from "react";
import Link from "next/link";
import {
Anchor,
Button,
Group,
Stack,
Table,
TableTbody,
TableTh,
TableThead,
TableTr,
} from "@mantine/core";
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";
@@ -24,30 +14,21 @@ 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();
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]),
),
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,
}),
),
items: initialPermissions.groupPermissions.map(({ group, permission }) => ({
itemId: group.id,
permission,
})),
},
});
@@ -92,9 +73,7 @@ export const GroupsForm = ({
<Table>
<TableThead>
<TableTr>
<TableTh style={{ whiteSpace: "nowrap" }}>
{tPermissions("field.group.label")}
</TableTh>
<TableTh style={{ whiteSpace: "nowrap" }}>{tPermissions("field.group.label")}</TableTh>
<TableTh>{tPermissions("field.permission.label")}</TableTh>
</TableTr>
</TableThead>
@@ -102,9 +81,8 @@ export const GroupsForm = ({
{form.values.items.map((row, index) => (
<BoardAccessSelectRow
key={row.itemId}
itemContent={
<GroupItemContent group={groups.get(row.itemId)!} />
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
itemContent={<GroupItemContent group={groups.get(row.itemId)!} />}
permission={row.permission}
index={index}
onCountChange={onCountChange}
@@ -114,11 +92,7 @@ export const GroupsForm = ({
</Table>
<Group justify="space-between">
<Button
rightSection={<IconPlus size="1rem" />}
variant="light"
onClick={handleAddUser}
>
<Button rightSection={<IconPlus size="1rem" />} variant="light" onClick={handleAddUser}>
{t("common.action.add")}
</Button>
<Button type="submit" loading={isPending} color="teal">
@@ -133,16 +107,10 @@ export const GroupsForm = ({
export const GroupItemContent = ({ group }: { group: Group }) => {
return (
<Anchor
component={Link}
href={`/manage/users/groups/${group.id}`}
size="sm"
style={{ whiteSpace: "nowrap" }}
>
<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"];
type Group = RouterOutputs["board"]["getBoardPermissions"]["groupPermissions"][0]["group"];
@@ -16,57 +16,52 @@ 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,
});
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 handleSubmitAsync = 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();
};
setLoading(false);
actions.closeModal();
};
const confirmLabel = innerProps.confirmLabel ?? t("common.action.add");
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"),
return (
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(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"),
});
@@ -1,11 +1,4 @@
import {
Stack,
Table,
TableTbody,
TableTh,
TableThead,
TableTr,
} from "@mantine/core";
import { Stack, Table, TableTbody, TableTh, TableThead, TableTr } from "@mantine/core";
import type { RouterOutputs } from "@homarr/api";
import { getPermissionsWithChildren } from "@homarr/definitions";
@@ -41,9 +34,7 @@ export const InheritTable = ({ initialPermissions }: InheritTableProps) => {
const boardPermission =
permission in mapPermissions
? mapPermissions[permission as keyof typeof mapPermissions]
: getPermissionsWithChildren([permission]).includes(
"board-full-access",
)
: getPermissionsWithChildren([permission]).includes("board-full-access")
? "board-full"
: null;
@@ -1,17 +1,6 @@
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 { 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";
@@ -21,10 +10,7 @@ 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 { BoardAccessDisplayRow, BoardAccessSelectRow } from "./board-access-table-rows";
import type { BoardAccessFormType, OnCountChange } from "./form";
import { FormProvider, useForm } from "./form";
import { UserSelectModal } from "./user-select-modal";
@@ -35,18 +21,11 @@ export interface FormProps {
onCountChange: OnCountChange;
}
export const UsersForm = ({
board,
initialPermissions,
onCountChange,
}: FormProps) => {
const { mutate, isPending } =
clientApi.board.saveUserBoardPermissions.useMutation();
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]),
),
new Map(initialPermissions.userPermissions.map(({ user }) => [user.id, user])),
);
const { openModal } = useModalAction(UserSelectModal);
const t = useI18n();
@@ -81,9 +60,7 @@ export const UsersForm = ({
const presentUserIds = form.values.items.map(({ itemId: id }) => id);
openModal({
presentUserIds: board.creatorId
? presentUserIds.concat(board.creatorId)
: presentUserIds,
presentUserIds: board.creatorId ? presentUserIds.concat(board.creatorId) : presentUserIds,
onSelect: (user) => {
setUsers((prev) => new Map(prev).set(user.id, user));
form.setFieldValue("items", [
@@ -111,17 +88,13 @@ export const UsersForm = ({
</TableThead>
<TableTbody>
{board.creator && (
<BoardAccessDisplayRow
itemContent={<UserItemContent user={board.creator} />}
permission="board-full"
/>
<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)!} />
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
itemContent={<UserItemContent user={users.get(row.itemId)!} />}
permission={row.permission}
index={index}
onCountChange={onCountChange}
@@ -131,11 +104,7 @@ export const UsersForm = ({
</Table>
<Group justify="space-between">
<Button
rightSection={<IconPlus size="1rem" />}
variant="light"
onClick={handleAddUser}
>
<Button rightSection={<IconPlus size="1rem" />} variant="light" onClick={handleAddUser}>
{t("common.action.add")}
</Button>
<Button type="submit" loading={isPending} color="teal">
@@ -154,12 +123,7 @@ const UserItemContent = ({ user }: { user: User }) => {
<Box visibleFrom="xs">
<UserAvatar user={user} size="sm" />
</Box>
<Anchor
component={Link}
href={`/manage/users/${user.id}`}
size="sm"
style={{ whiteSpace: "nowrap" }}
>
<Anchor component={Link} href={`/manage/users/${user.id}`} size="sm" style={{ whiteSpace: "nowrap" }}>
{user.name}
</Anchor>
</Group>
@@ -12,11 +12,7 @@ import { UserAvatar } from "@homarr/ui";
interface InnerProps {
presentUserIds: string[];
onSelect: (props: {
id: string;
name: string;
image: string;
}) => void | Promise<void>;
onSelect: (props: { id: string; name: string; image: string }) => void | Promise<void>;
confirmLabel?: string;
}
@@ -24,66 +20,59 @@ 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 ?? "",
});
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 handleSubmitAsync = 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();
};
setLoading(false);
actions.closeModal();
};
const confirmLabel = innerProps.confirmLabel ?? t("common.action.add");
const currentUser = users?.find((user) => user.id === form.values.userId);
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"),
return (
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(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 = {
@@ -93,9 +82,7 @@ const iconProps = {
size: "1rem",
};
const createRenderOption = (
users: RouterOutputs["user"]["selectable"],
): SelectProps["renderOption"] =>
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;
@@ -104,9 +91,7 @@ const createRenderOption = (
<Group flex="1" gap="xs">
<UserAvatar user={user} size="xs" />
{option.label}
{checked && (
<IconCheck style={{ marginInlineStart: "auto" }} {...iconProps} />
)}
{checked && <IconCheck style={{ marginInlineStart: "auto" }} {...iconProps} />}
</Group>
);
};
@@ -2,16 +2,13 @@
import { Button, Grid, Group, Stack, TextInput } from "@mantine/core";
import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
} from "@homarr/definitions";
import { useForm } from "@homarr/form";
import { backgroundImageAttachments, backgroundImageRepeats, backgroundImageSizes } from "@homarr/definitions";
import { useZodForm } 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 { validation } from "@homarr/validation";
import type { Board } from "../../_types";
import { useSavePartialSettingsMutation } from "./_shared";
@@ -21,9 +18,8 @@ interface Props {
}
export const BackgroundSettingsContent = ({ board }: Props) => {
const t = useI18n();
const { mutate: savePartialSettings, isPending } =
useSavePartialSettingsMutation(board);
const form = useForm({
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
const form = useZodForm(validation.board.savePartialSettings, {
initialValues: {
backgroundImageUrl: board.backgroundImageUrl ?? "",
backgroundImageAttachment: board.backgroundImageAttachment,
@@ -36,14 +32,8 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
"backgroundImageAttachment",
backgroundImageAttachments,
);
const backgroundImageSizeData = useBackgroundOptionData(
"backgroundImageSize",
backgroundImageSizes,
);
const backgroundImageRepeatData = useBackgroundOptionData(
"backgroundImageRepeat",
backgroundImageRepeats,
);
const backgroundImageSizeData = useBackgroundOptionData("backgroundImageSize", backgroundImageSizes);
const backgroundImageRepeatData = useBackgroundOptionData("backgroundImageRepeat", backgroundImageRepeats);
return (
<form
@@ -95,13 +85,9 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
);
};
type BackgroundImageKey =
| "backgroundImageAttachment"
| "backgroundImageSize"
| "backgroundImageRepeat";
type BackgroundImageKey = "backgroundImageAttachment" | "backgroundImageSize" | "backgroundImageRepeat";
type inferOptions<TKey extends BackgroundImageKey> =
TranslationObject["board"]["field"][TKey]["option"];
type inferOptions<TKey extends BackgroundImageKey> = TranslationObject["board"]["field"][TKey]["option"];
const useBackgroundOptionData = <
TKey extends BackgroundImageKey,
@@ -119,9 +105,7 @@ const useBackgroundOptionData = <
(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,
),
description: t(`board.field.${key}.option.${value as string}.description` as never),
value: value as string,
badge:
data.defaultValue === value
@@ -17,8 +17,9 @@ import {
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { useForm } from "@homarr/form";
import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import type { Board } from "../../_types";
import { generateColors } from "../../(content)/_theme";
@@ -33,7 +34,7 @@ const hexRegex = /^#[0-9a-fA-F]{6}$/;
const progressPercentageLabel = (value: number) => `${value}%`;
export const ColorSettingsContent = ({ board }: Props) => {
const form = useForm({
const form = useZodForm(validation.board.savePartialSettings, {
initialValues: {
primaryColor: board.primaryColor,
secondaryColor: board.secondaryColor,
@@ -43,8 +44,7 @@ export const ColorSettingsContent = ({ board }: Props) => {
const [showPreview, { toggle }] = useDisclosure(false);
const t = useI18n();
const theme = useMantineTheme();
const { mutate: savePartialSettings, isPending } =
useSavePartialSettingsMutation(board);
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
return (
<form
@@ -76,11 +76,7 @@ export const ColorSettingsContent = ({ board }: Props) => {
/>
</Grid.Col>
<Grid.Col span={12}>
<Anchor onClick={toggle}>
{showPreview
? t("common.preview.hide")
: t("common.preview.show")}
</Anchor>
<Anchor onClick={toggle}>{showPreview ? t("common.preview.hide") : t("common.preview.show")}</Anchor>
</Grid.Col>
<Grid.Col span={12}>
<Collapse in={showPreview}>
@@ -114,15 +110,13 @@ export const ColorSettingsContent = ({ board }: Props) => {
};
interface ColorsPreviewProps {
previewColor: string;
previewColor: string | undefined;
}
const ColorsPreview = ({ previewColor }: ColorsPreviewProps) => {
const theme = useMantineTheme();
const colors = hexRegex.test(previewColor)
? generateColors(previewColor)
: generateColors("#000000");
const colors = previewColor && hexRegex.test(previewColor) ? generateColors(previewColor) : generateColors("#000000");
return (
<Group gap={0} wrap="nowrap">
@@ -1,7 +1,91 @@
"use client";
// TODO: add some sort of store (maybe directory on GitHub)
import { Alert, Button, Group, Input, Stack } from "@mantine/core";
import { highlight, languages } from "prismjs";
import Editor from "react-simple-code-editor";
export const CustomCssSettingsContent = () => {
return null;
import "~/styles/prismjs.scss";
import { IconInfoCircle } from "@tabler/icons-react";
import { useForm } from "@homarr/form";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { Board } from "../../_types";
import { useSavePartialSettingsMutation } from "./_shared";
import classes from "./customcss.module.css";
interface Props {
board: Board;
}
export const CustomCssSettingsContent = ({ board }: Props) => {
const t = useI18n();
const customCssT = useScopedI18n("board.field.customCss");
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
const form = useForm({
initialValues: {
customCss: board.customCss ?? "",
},
});
return (
<form
onSubmit={form.onSubmit((values) => {
savePartialSettings({
id: board.id,
...values,
});
})}
>
<Stack>
<CustomCssInput {...form.getInputProps("customCss")} />
<Alert variant="light" color="cyan" title={customCssT("customClassesAlert.title")} icon={<IconInfoCircle />}>
{customCssT("customClassesAlert.description")}
</Alert>
<Group justify="end">
<Button type="submit" loading={isPending} color="teal">
{t("common.action.saveChanges")}
</Button>
</Group>
</Stack>
</form>
);
};
interface CustomCssInputProps {
value?: string;
onChange: (value: string) => void;
}
const CustomCssInput = ({ value, onChange }: CustomCssInputProps) => {
const customCssT = useScopedI18n("board.field.customCss");
return (
<Input.Wrapper
label={customCssT("label")}
labelProps={{
htmlFor: "custom-css",
}}
description={customCssT("description")}
inputWrapperOrder={["label", "description", "input", "error"]}
>
<div className={classes.codeEditorRoot}>
<Editor
textareaId="custom-css"
onValueChange={onChange}
value={value ?? ""}
highlight={(code) => highlight(code, languages.extend("css", {}), "css")}
padding={10}
style={{
fontFamily: '"Fira code", "Fira Mono", monospace',
fontSize: 12,
minHeight: 250,
}}
/>
</div>
</Input.Wrapper>
);
};
@@ -20,8 +20,7 @@ export const DangerZoneSettingsContent = () => {
const { openModal } = useModalAction(BoardRenameModal);
const { mutate: changeVisibility, isPending: isChangeVisibilityPending } =
clientApi.board.changeBoardVisibility.useMutation();
const { mutate: deleteBoard, isPending: isDeletePending } =
clientApi.board.deleteBoard.useMutation();
const { mutate: deleteBoard, isPending: isDeletePending } = clientApi.board.deleteBoard.useMutation();
const utils = clientApi.useUtils();
const visibility = board.isPublic ? "public" : "private";
@@ -37,12 +36,8 @@ export const DangerZoneSettingsContent = () => {
const onVisibilityClick = useCallback(() => {
openConfirmModal({
title: t(
`section.dangerZone.action.visibility.confirm.${visibility}.title`,
),
children: t(
`section.dangerZone.action.visibility.confirm.${visibility}.description`,
),
title: t(`section.dangerZone.action.visibility.confirm.${visibility}.title`),
children: t(`section.dangerZone.action.visibility.confirm.${visibility}.description`),
onConfirm: () => {
changeVisibility(
{
@@ -52,7 +47,7 @@ export const DangerZoneSettingsContent = () => {
{
onSettled() {
void utils.board.getBoardByName.invalidate({ name: board.name });
void utils.board.getDefaultBoard.invalidate();
void utils.board.getHomeBoard.invalidate();
},
},
);
@@ -64,7 +59,7 @@ export const DangerZoneSettingsContent = () => {
changeVisibility,
t,
utils.board.getBoardByName,
utils.board.getDefaultBoard,
utils.board.getHomeBoard,
visibility,
openConfirmModal,
]);
@@ -98,12 +93,8 @@ export const DangerZoneSettingsContent = () => {
<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}`,
)}
description={t(`section.dangerZone.action.visibility.description.${visibility}`)}
buttonText={t(`section.dangerZone.action.visibility.button.${visibility}`)}
onClick={onVisibilityClick}
isPending={isChangeVisibilityPending}
/>
@@ -127,13 +118,7 @@ interface DangerZoneRowProps {
onClick: () => void;
}
const DangerZoneRow = ({
label,
description,
buttonText,
onClick,
isPending,
}: DangerZoneRowProps) => {
const DangerZoneRow = ({ label, description, buttonText, onClick, isPending }: DangerZoneRowProps) => {
return (
<Group justify="space-between" px="md" className={classes.dangerZoneGroup}>
<Stack gap={0}>
@@ -143,12 +128,7 @@ const DangerZoneRow = ({
<Text size="sm">{description}</Text>
</Stack>
<Group justify="end" w={{ base: "100%", xs: "auto" }}>
<Button
variant="subtle"
color="red"
loading={isPending}
onClick={onClick}
>
<Button variant="subtle" color="red" loading={isPending} onClick={onClick}>
{buttonText}
</Button>
</Group>
@@ -1,25 +1,15 @@
"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 { 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 { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { createMetaTitle } from "~/metadata";
import type { Board } from "../../_types";
import { useUpdateBoard } from "../../(content)/_client";
import { useSavePartialSettingsMutation } from "./_shared";
@@ -36,22 +26,31 @@ export const GeneralSettingsContent = ({ board }: Props) => {
});
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 ?? "",
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
const form = useZodForm(
validation.board.savePartialSettings
.pick({
pageTitle: true,
logoImageUrl: true,
metaTitle: true,
faviconImageUrl: true,
})
.required(),
{
initialValues: {
pageTitle: board.pageTitle ?? "",
logoImageUrl: board.logoImageUrl ?? "",
metaTitle: board.metaTitle ?? "",
faviconImageUrl: board.faviconImageUrl ?? "",
},
onValuesChange({ pageTitle }) {
updateBoard((previous) => ({
...previous,
pageTitle,
}));
},
},
onValuesChange({ pageTitle }) {
updateBoard((previous) => ({
...previous,
pageTitle,
}));
},
});
);
const metaTitleStatus = useMetaTitlePreview(form.values.metaTitle);
const faviconStatus = useFaviconPreview(form.values.faviconImageUrl);
@@ -94,7 +93,7 @@ export const GeneralSettingsContent = ({ board }: Props) => {
<Grid.Col span={{ xs: 12, md: 6 }}>
<TextInput
label={t("board.field.metaTitle.label")}
placeholder="Default Board | Homarr"
placeholder={createMetaTitle(t("board.content.metaTitle", { boardName: board.name }))}
rightSection={<PendingOrInvalidIndicator {...metaTitleStatus} />}
{...form.getInputProps("metaTitle")}
/>
@@ -126,22 +125,12 @@ export const GeneralSettingsContent = ({ board }: Props) => {
);
};
const PendingOrInvalidIndicator = ({
isPending,
isInvalid,
}: {
isPending: boolean;
isInvalid?: boolean;
}) => {
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")}
>
<Tooltip multiline w={220} label={t("board.setting.section.general.unrecognizedLink")}>
<IconAlertTriangle size="1rem" color="red" />
</Tooltip>
);
@@ -183,8 +172,7 @@ const useMetaTitlePreview = (title: string | null) => {
const validFaviconExtensions = ["ico", "png", "svg", "gif"];
const isValidUrl = (url: string) =>
url.includes("/") &&
validFaviconExtensions.some((extension) => url.endsWith(`.${extension}`));
url.includes("/") && validFaviconExtensions.some((extension) => url.endsWith(`.${extension}`));
const useFaviconPreview = (url: string | null) => {
const [faviconDebounced] = useDebouncedValue(url ?? "", 500);
@@ -2,8 +2,9 @@
import { Button, Grid, Group, Input, Slider, Stack } from "@mantine/core";
import { useForm } from "@homarr/form";
import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import type { Board } from "../../_types";
import { useSavePartialSettingsMutation } from "./_shared";
@@ -13,9 +14,8 @@ interface Props {
}
export const LayoutSettingsContent = ({ board }: Props) => {
const t = useI18n();
const { mutate: savePartialSettings, isPending } =
useSavePartialSettingsMutation(board);
const form = useForm({
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
const form = useZodForm(validation.board.savePartialSettings.pick({ columnCount: true }).required(), {
initialValues: {
columnCount: board.columnCount,
},
@@ -34,13 +34,7 @@ export const LayoutSettingsContent = ({ board }: Props) => {
<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")}
/>
<Slider mt="xs" min={1} max={24} step={1} {...form.getInputProps("columnCount")} />
</Input.Wrapper>
</Grid.Col>
</Grid>
@@ -7,7 +7,7 @@ export const useSavePartialSettingsMutation = (board: Board) => {
return clientApi.board.savePartialBoardSettings.useMutation({
onSettled() {
void utils.board.getBoardByName.invalidate({ name: board.name });
void utils.board.getDefaultBoard.invalidate();
void utils.board.getHomeBoard.invalidate();
},
});
};
@@ -0,0 +1,22 @@
.codeEditorFooter {
border-bottom-left-radius: var(--mantine-radius-sm);
border-bottom-right-radius: var(--mantine-radius-sm);
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-7));
}
.codeEditorRoot {
margin-top: 4px;
border-color: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4));
border-width: 1px;
border-style: solid;
border-radius: var(--mantine-radius-sm);
}
.codeEditor {
background-color: light-dark(white, var(--mantine-color-dark-6));
font-size: var(--mantine-font-size-xs);
}
.codeEditor ::placeholder {
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
}
@@ -1,14 +1,6 @@
import type { PropsWithChildren } from "react";
import { notFound } from "next/navigation";
import {
AccordionControl,
AccordionItem,
AccordionPanel,
Container,
Stack,
Text,
Title,
} from "@mantine/core";
import { AccordionControl, AccordionItem, AccordionPanel, Container, Stack, Text, Title } from "@mantine/core";
import {
IconAlertTriangle,
IconBrush,
@@ -26,7 +18,7 @@ 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 { getBoardPermissionsAsync } from "~/components/board/permissions/server";
import { ActiveTabAccordion } from "../../../../../components/active-tab-accordion";
import { AccessSettingsContent } from "./_access";
import { BackgroundSettingsContent } from "./_background";
@@ -45,10 +37,10 @@ interface Props {
};
}
const getBoardAndPermissions = async (params: Props["params"]) => {
const getBoardAndPermissionsAsync = async (params: Props["params"]) => {
try {
const board = await api.board.getBoardByName({ name: params.name });
const { hasFullAccess } = await getBoardPermissions(board);
const { hasFullAccess } = await getBoardPermissionsAsync(board);
const permissions = hasFullAccess
? await api.board.getBoardPermissions({ id: board.id })
: {
@@ -69,22 +61,16 @@ const getBoardAndPermissions = async (params: Props["params"]) => {
}
};
export default async function BoardSettingsPage({
params,
searchParams,
}: Props) {
const { board, permissions } = await getBoardAndPermissions(params);
const { hasFullAccess } = await getBoardPermissions(board);
export default async function BoardSettingsPage({ params, searchParams }: Props) {
const { board, permissions } = await getBoardAndPermissionsAsync(params);
const { hasFullAccess } = await getBoardPermissionsAsync(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"}
>
<ActiveTabAccordion variant="separated" defaultValue={searchParams.tab ?? "general"}>
<AccordionItemFor value="general" icon={IconSettings}>
<GeneralSettingsContent board={board} />
</AccordionItemFor>
@@ -98,22 +84,14 @@ export default async function BoardSettingsPage({
<ColorSettingsContent board={board} />
</AccordionItemFor>
<AccordionItemFor value="customCss" icon={IconFileTypeCss}>
<CustomCssSettingsContent />
<CustomCssSettingsContent board={board} />
</AccordionItemFor>
{hasFullAccess && (
<>
<AccordionItemFor value="access" icon={IconUser}>
<AccessSettingsContent
board={board}
initialPermissions={permissions}
/>
<AccessSettingsContent board={board} initialPermissions={permissions} />
</AccordionItemFor>
<AccordionItemFor
value="dangerZone"
icon={IconAlertTriangle}
danger
noPadding
>
<AccordionItemFor value="dangerZone" icon={IconAlertTriangle} danger noPadding>
<DangerZoneSettingsContent />
</AccordionItemFor>
</>
@@ -131,13 +109,7 @@ type AccordionItemForProps = PropsWithChildren<{
noPadding?: boolean;
}>;
const AccordionItemFor = async ({
value,
children,
icon: Icon,
danger,
noPadding,
}: AccordionItemForProps) => {
const AccordionItemFor = async ({ value, children, icon: Icon, danger, noPadding }: AccordionItemForProps) => {
const t = await getScopedI18n("board.setting.section");
return (
<AccordionItem
@@ -158,13 +130,7 @@ const AccordionItemFor = async ({
{t(`${value}.title`)}
</Text>
</AccordionControl>
<AccordionPanel
styles={
noPadding
? { content: { paddingRight: 0, paddingLeft: 0 } }
: undefined
}
>
<AccordionPanel styles={noPadding ? { content: { paddingRight: 0, paddingLeft: 0 } } : undefined}>
{children}
</AccordionPanel>
</AccordionItem>
@@ -12,17 +12,18 @@ import { ClientShell } from "~/components/layout/shell";
import type { Board } from "./_types";
import { BoardProvider } from "./(content)/_context";
import type { Params } from "./(content)/_creator";
import { CustomCss } from "./(content)/_custom-css";
import { BoardMantineProvider } from "./(content)/_theme";
interface CreateBoardLayoutProps<TParams extends Params> {
headerActions: JSX.Element;
getInitialBoard: (params: TParams) => Promise<Board>;
getInitialBoardAsync: (params: TParams) => Promise<Board>;
isBoardContentPage: boolean;
}
export const createBoardLayout = <TParams extends Params>({
headerActions,
getInitialBoard,
getInitialBoardAsync: getInitialBoard,
isBoardContentPage,
}: CreateBoardLayoutProps<TParams>) => {
const Layout = async ({
@@ -41,12 +42,10 @@ export const createBoardLayout = <TParams extends Params>({
});
return (
<GlobalItemServerDataRunner
board={initialBoard}
shouldRun={isBoardContentPage}
>
<GlobalItemServerDataRunner board={initialBoard} shouldRun={isBoardContentPage}>
<BoardProvider initialBoard={initialBoard}>
<BoardMantineProvider>
<CustomCss />
<ClientShell hasNavigation={false}>
<MainHeader
logo={<BoardLogoWithTitle size="md" hideTitleOnMobile />}
@@ -1,14 +1,11 @@
import type { RouterOutputs } from "@homarr/api";
import type { WidgetKind } from "@homarr/definitions";
export type Board = RouterOutputs["board"]["getDefaultBoard"];
export type Board = RouterOutputs["board"]["getHomeBoard"];
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 }
>;
export type ItemOfKind<TKind extends WidgetKind> = Extract<Item, { kind: TKind }>;
+7 -10
View File
@@ -5,14 +5,11 @@ 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>
);
});
return wrappers.reverse().reduce((Acc, Current): React.FunctionComponent<PropsWithChildren> => {
return (props) => (
<Current>
<Acc {...props} />
</Current>
);
});
};
@@ -4,11 +4,8 @@ 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 { useZodForm } 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";
@@ -16,12 +13,8 @@ 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,
const { mutateAsync, error, isPending } = clientApi.user.initUser.useMutation();
const form = useZodForm(validation.user.init, {
initialValues: {
username: "",
password: "",
@@ -29,7 +22,7 @@ export const InitUserForm = () => {
},
});
const handleSubmit = async (values: FormType) => {
const handleSubmitAsync = async (values: FormType) => {
await mutateAsync(values, {
onSuccess: () => {
showSuccessNotification({
@@ -51,23 +44,14 @@ export const InitUserForm = () => {
<Stack gap="xl">
<form
onSubmit={form.onSubmit(
(values) => void handleSubmit(values),
(values) => void handleSubmitAsync(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")}
/>
<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>
+5 -8
View File
@@ -1,9 +1,9 @@
import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import "@homarr/ui/styles.css";
import "@homarr/notifications/styles.css";
import "@homarr/spotlight/styles.css";
import "@mantine/core/styles.css";
import { ColorSchemeScript, createTheme, MantineProvider } from "@mantine/core";
@@ -11,6 +11,7 @@ import { auth } from "@homarr/auth/next";
import { ModalProvider } from "@homarr/modals";
import { Notifications } from "@homarr/notifications";
import { Analytics } from "~/components/layout/analytics";
import { JotaiProvider } from "./_client-providers/jotai";
import { NextInternationalProvider } from "./_client-providers/next-international";
import { AuthProvider } from "./_client-providers/session";
@@ -48,10 +49,7 @@ export const viewport: Viewport = {
],
};
export default function Layout(props: {
children: React.ReactNode;
params: { locale: string };
}) {
export default function Layout(props: { children: React.ReactNode; params: { locale: string } }) {
const colorScheme = "dark";
const StackedProvider = composeWrappers([
@@ -61,9 +59,7 @@ export default function Layout(props: {
},
(innerProps) => <JotaiProvider {...innerProps} />,
(innerProps) => <TRPCReactProvider {...innerProps} />,
(innerProps) => (
<NextInternationalProvider {...innerProps} locale={props.params.locale} />
),
(innerProps) => <NextInternationalProvider {...innerProps} locale={props.params.locale} />,
(innerProps) => (
<MantineProvider
{...innerProps}
@@ -81,6 +77,7 @@ export default function Layout(props: {
<html lang="en" suppressHydrationWarning>
<head>
<ColorSchemeScript defaultColorScheme={colorScheme} />
<Analytics />
</head>
<body className={["font-sans", fontSans.variable].join(" ")}>
<StackedProvider>
@@ -1,17 +1,5 @@
import { Center, Stack, Text, Title } from "@mantine/core";
import { notFound } from "next/navigation";
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>
);
export default function NotFound() {
return notFound();
}
@@ -0,0 +1,36 @@
.bannerContainer {
border-radius: 8px;
overflow: hidden;
background: linear-gradient(
130deg,
#fa52521f 0%,
var(--mantine-color-dark-6) 35%,
var(--mantine-color-dark-6) 100%
) !important;
}
.scrollContainer {
height: 100%;
transform: rotateZ(10deg);
}
@keyframes scrolling {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-50%);
}
}
.scrollAnimationContainer {
animation: scrolling;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
@media (prefers-reduced-motion: reduce) {
.scrollAnimationContainer {
animation: none !important;
}
}
@@ -0,0 +1,78 @@
import { Box, Grid, GridCol, Group, Image, Stack, Title } from "@mantine/core";
import { splitToNChunks } from "@homarr/common";
import classes from "./hero-banner.module.css";
const icons = [
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/homarr.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/sabnzbd.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/deluge.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/radarr.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/sonarr.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/lidarr.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/pihole.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/dashdot.png",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/overseerr.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/plex.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/jellyfin.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/homeassistant.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/freshrss.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/readarr.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/transmission.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/qbittorrent.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nzbget.png",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/openmediavault.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/docker.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/jellyseerr.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/adguardhome.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/tdarr.png",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/prowlarr.svg",
];
const countIconGroups = 3;
const animationDurationInSeconds = 12;
export const HeroBanner = () => {
const arrayInChunks = splitToNChunks(icons, countIconGroups);
const gridSpan = 12 / countIconGroups;
return (
<Box className={classes.bannerContainer} p={{ base: "lg", md: "3rem" }} bg="dark.6" pos="relative">
<Stack gap={0}>
<Title fz={{ base: "h4", md: "h2" }} c="dimmed">
Welcome back to your
</Title>
<Group gap="xs" wrap="nowrap">
<Image src="/logo/logo.png" w={{ base: 32, md: 40 }} h={{ base: 32, md: 40 }} />
<Title fz={{ base: "h3", md: "h1" }}>Homarr Board</Title>
</Group>
</Stack>
<Box visibleFrom="md" className={classes.scrollContainer} w={"30%"} top={0} right={0} pos="absolute">
<Grid>
{Array(countIconGroups)
.fill(0)
.map((_, columnIndex) => (
<GridCol key={`grid-column-${columnIndex}`} span={gridSpan}>
<Stack
className={classes.scrollAnimationContainer}
style={{
animationDuration: `${animationDurationInSeconds - columnIndex}s`,
}}
>
{arrayInChunks[columnIndex]?.map((icon, index) => (
<Image key={`grid-column-${columnIndex}-scroll-1-${index}`} src={icon} radius="md" w={50} h={50} />
))}
{/* This is used for making the animation seem seamless */}
{arrayInChunks[columnIndex]?.map((icon, index) => (
<Image key={`grid-column-${columnIndex}-scroll-2-${index}`} src={icon} radius="md" w={50} h={50} />
))}
</Stack>
</GridCol>
))}
</Grid>
</Box>
</Box>
);
};
@@ -0,0 +1,3 @@
.contributorCard {
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
}
@@ -1,36 +0,0 @@
.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);
}
}
@@ -4,7 +4,11 @@ import {
AccordionControl,
AccordionItem,
AccordionPanel,
AspectRatio,
Avatar,
Card,
Center,
Flex,
Group,
List,
ListItem,
@@ -17,16 +21,19 @@ import { setStaticParamsLocale } from "next-international/server";
import { getScopedI18n, getStaticParams } from "@homarr/translation/server";
import { homarrLogoPath } from "~/components/layout/logo/homarr-logo";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { createMetaTitle } from "~/metadata";
import { getPackageAttributesAsync } from "~/versions/package-reader";
import logo from "../../../../../public/logo/logo.png";
import classes from "./accordion.module.css";
import contributorsData from "../../../../../../../static-data/contributors.json";
import translatorsData from "../../../../../../../static-data/translators.json";
import classes from "./about.module.css";
export async function generateMetadata() {
const t = await getScopedI18n("management");
const metaTitle = `${t("metaTitle")} • Homarr`;
return {
title: metaTitle,
title: createMetaTitle(t("metaTitle")),
};
}
@@ -42,33 +49,68 @@ export default async function AboutPage({ params: { locale } }: PageProps) {
const attributes = await getPackageAttributesAsync();
return (
<div>
<DynamicBreadcrumb />
<Center w="100%">
<Group py="lg">
<Image src={logo} width={100} height={100} alt="" />
<Image src={homarrLogoPath} width={100} height={100} alt="" />
<Stack gap={0}>
<Title order={1} tt="uppercase">
Homarr
</Title>
<Title order={2}>
{t("version", { version: attributes.version })}
</Title>
<Title order={2}>{t("version", { version: attributes.version })}</Title>
</Stack>
</Group>
</Center>
<Text mb="xl">{t("text")}</Text>
<Accordion classNames={classes}>
<Accordion defaultValue="contributors" variant="filled" radius="md">
<AccordionItem value="contributors">
<AccordionControl icon={<IconUsers size="1rem" />}>
{t("accordion.contributors.title")}
<Stack gap={0}>
<Text>{t("accordion.contributors.title")}</Text>
<Text size="sm" c="dimmed">
{t("accordion.contributors.subtitle", {
count: contributorsData.length,
})}
</Text>
</Stack>
</AccordionControl>
<AccordionPanel></AccordionPanel>
<AccordionPanel>
<Flex wrap="wrap" gap="xs">
{contributorsData.map((contributor) => (
<GenericContributorLinkCard
key={contributor.login}
link={`https://github.com/${contributor.login}`}
image={contributor.avatar_url}
name={contributor.login}
/>
))}
</Flex>
</AccordionPanel>
</AccordionItem>
<AccordionItem value="translators">
<AccordionControl icon={<IconLanguage size="1rem" />}>
{t("accordion.translators.title")}
<Stack gap={0}>
<Text>{t("accordion.translators.title")}</Text>
<Text size="sm" c="dimmed">
{t("accordion.translators.subtitle", {
count: translatorsData.length,
})}
</Text>
</Stack>
</AccordionControl>
<AccordionPanel></AccordionPanel>
<AccordionPanel>
<Flex wrap="wrap" gap="xs">
{translatorsData.map((translator) => (
<GenericContributorLinkCard
key={translator.username}
link={`https://crowdin.com/profile/${translator.username}`}
image={translator.avatarUrl}
name={translator.username}
/>
))}
</Flex>
</AccordionPanel>
</AccordionItem>
<AccordionItem value="libraries">
<AccordionControl icon={<IconLibrary size="1rem" />}>
@@ -102,6 +144,27 @@ export default async function AboutPage({ params: { locale } }: PageProps) {
);
}
interface GenericContributorLinkCardProps {
name: string;
link: string;
image: string;
}
const GenericContributorLinkCard = ({ name, image, link }: GenericContributorLinkCardProps) => {
return (
<AspectRatio ratio={1}>
<Card className={classes.contributorCard} component="a" href={link} target="_blank" w={100}>
<Stack align="center">
<Avatar src={image} alt={name} size={40} display="block" />
<Text lineClamp={1} size="sm">
{name}
</Text>
</Stack>
</Card>
</AspectRatio>
);
};
export function generateStaticParams() {
return getStaticParams();
}
@@ -7,13 +7,10 @@ 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 { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { revalidatePathAction } from "../../../revalidatePathAction";
import { revalidatePathActionAsync } from "../../../revalidatePathAction";
interface AppDeleteButtonProps {
app: RouterOutputs["app"]["all"][number];
@@ -37,7 +34,7 @@ export const AppDeleteButton = ({ app }: AppDeleteButtonProps) => {
title: t("notification.success.title"),
message: t("notification.success.message"),
});
void revalidatePathAction("/manage/apps");
void revalidatePathActionAsync("/manage/apps");
},
onError: () => {
showErrorNotification({
@@ -52,13 +49,7 @@ export const AppDeleteButton = ({ app }: AppDeleteButtonProps) => {
}, [app, mutate, t, openConfirmModal]);
return (
<ActionIcon
loading={isPending}
variant="subtle"
color="red"
onClick={onClick}
aria-label="Delete app"
>
<ActionIcon loading={isPending} variant="subtle" color="red" onClick={onClick} aria-label={t("title")}>
<IconTrash color="red" size={16} stroke={1.5} />
</ActionIcon>
);
@@ -3,13 +3,14 @@
import Link from "next/link";
import { Button, Group, Stack, Textarea, TextInput } from "@mantine/core";
import { useForm, zodResolver } from "@homarr/form";
import { useZodForm } 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
import { IconPicker } from "~/components/icons/picker/icon-picker";
type FormType = z.infer<typeof validation.app.manage>;
interface AppFormProps {
@@ -20,31 +21,25 @@ interface AppFormProps {
}
export const AppForm = (props: AppFormProps) => {
const { submitButtonTranslation, handleSubmit, initialValues, isPending } =
props;
const { submitButtonTranslation, handleSubmit, initialValues, isPending } = props;
const t = useI18n();
const form = useForm({
const form = useZodForm(validation.app.manage, {
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" />
<TextInput {...form.getInputProps("name")} withAsterisk label={t("app.field.name.label")} />
<IconPicker initialValue={initialValues?.iconUrl} {...form.getInputProps("iconUrl")} />
<Textarea {...form.getInputProps("description")} label={t("app.field.description.label")} />
<TextInput {...form.getInputProps("href")} label={t("app.field.url.label")} />
<Group justify="end">
<Button variant="default" component={Link} href="/manage/apps">
@@ -5,15 +5,12 @@ import { useRouter } from "next/navigation";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import {
showErrorNotification,
showSuccessNotification,
} from "@homarr/notifications";
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 { revalidatePathActionAsync } from "~/app/revalidatePathAction";
import { AppForm } from "../../_form";
interface AppEditFormProps {
@@ -30,7 +27,7 @@ export const AppEditForm = ({ app }: AppEditFormProps) => {
title: t("success.title"),
message: t("success.message"),
});
void revalidatePathAction("/manage/apps").then(() => {
void revalidatePathActionAsync("/manage/apps").then(() => {
router.push("/manage/apps");
});
},
@@ -52,10 +49,7 @@ export const AppEditForm = ({ app }: AppEditFormProps) => {
[mutate, app.id],
);
const submitButtonTranslation = useCallback(
(t: TranslationFunction) => t("common.action.save"),
[],
);
const submitButtonTranslation = useCallback((t: TranslationFunction) => t("common.action.save"), []);
return (
<AppForm
@@ -3,6 +3,7 @@ import { Container, Stack, Title } from "@mantine/core";
import { api } from "@homarr/api/server";
import { getI18n } from "@homarr/translation/server";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { AppEditForm } from "./_app-edit-form";
interface AppEditPageProps {
@@ -14,11 +15,14 @@ export default async function AppEditPage({ params }: AppEditPageProps) {
const t = await getI18n();
return (
<Container>
<Stack>
<Title>{t("app.page.edit.title")}</Title>
<AppEditForm app={app} />
</Stack>
</Container>
<>
<DynamicBreadcrumb dynamicMappings={new Map([[params.id, app.name]])} nonInteractable={["edit"]} />
<Container>
<Stack>
<Title>{t("app.page.edit.title")}</Title>
<AppEditForm app={app} />
</Stack>
</Container>
</>
);
}
@@ -4,15 +4,12 @@ import { useCallback } from "react";
import { useRouter } from "next/navigation";
import { clientApi } from "@homarr/api/client";
import {
showErrorNotification,
showSuccessNotification,
} from "@homarr/notifications";
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 { revalidatePathActionAsync } from "~/app/revalidatePathAction";
import { AppForm } from "../_form";
export const AppNewForm = () => {
@@ -25,7 +22,7 @@ export const AppNewForm = () => {
title: t("success.title"),
message: t("success.message"),
});
void revalidatePathAction("/manage/apps").then(() => {
void revalidatePathActionAsync("/manage/apps").then(() => {
router.push("/manage/apps");
});
},
@@ -44,16 +41,9 @@ export const AppNewForm = () => {
[mutate],
);
const submitButtonTranslation = useCallback(
(t: TranslationFunction) => t("common.action.create"),
[],
);
const submitButtonTranslation = useCallback((t: TranslationFunction) => t("common.action.create"), []);
return (
<AppForm
submitButtonTranslation={submitButtonTranslation}
handleSubmit={handleSubmit}
isPending={isPending}
/>
<AppForm submitButtonTranslation={submitButtonTranslation} handleSubmit={handleSubmit} isPending={isPending} />
);
};
@@ -1,14 +1,22 @@
import { Container, Stack, Title } from "@mantine/core";
import { getI18n } from "@homarr/translation/server";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { AppNewForm } from "./_app-new-form";
export default function AppNewPage() {
export default async function AppNewPage() {
const t = await getI18n();
return (
<Container>
<Stack>
<Title>New app</Title>
<AppNewForm />
</Stack>
</Container>
<>
<DynamicBreadcrumb />
<Container>
<Stack>
<Title>{t("app.page.create.title")}</Title>
<AppNewForm />
</Stack>
</Container>
</>
);
}
@@ -1,36 +1,29 @@
import Link from "next/link";
import {
ActionIcon,
ActionIconGroup,
Anchor,
Avatar,
Button,
Card,
Container,
Group,
Stack,
Text,
Title,
} from "@mantine/core";
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, 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 { getI18n, getScopedI18n } from "@homarr/translation/server";
import { ManageContainer } from "~/components/manage/manage-container";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { AppDeleteButton } from "./_app-delete-button";
export default async function AppsPage() {
const apps = await api.app.all();
const t = await getScopedI18n("app");
return (
<Container>
<ManageContainer>
<DynamicBreadcrumb />
<Stack>
<Group justify="space-between" align="center">
<Title>Apps</Title>
<Button component={Link} href="/manage/apps/new">
New app
</Button>
<Title>{t("page.list.title")}</Title>
<MobileAffixButton component={Link} href="/manage/apps/new">
{t("page.create.title")}
</MobileAffixButton>
</Group>
{apps.length === 0 && <AppNoResults />}
{apps.length > 0 && (
@@ -41,7 +34,7 @@ export default async function AppsPage() {
</Stack>
)}
</Stack>
</Container>
</ManageContainer>
);
}
@@ -49,10 +42,12 @@ interface AppCardProps {
app: RouterOutputs["app"]["all"][number];
}
const AppCard = ({ app }: AppCardProps) => {
const AppCard = async ({ app }: AppCardProps) => {
const t = await getScopedI18n("app");
return (
<Card>
<Group justify="space-between">
<Group justify="space-between" wrap="nowrap">
<Group align="top" justify="start" wrap="nowrap">
<Avatar
size="sm"
@@ -65,14 +60,16 @@ const AppCard = ({ app }: AppCardProps) => {
}}
/>
<Stack gap={0}>
<Text fw={500}>{app.name}</Text>
<Text fw={500} lineClamp={1}>
{app.name}
</Text>
{app.description && (
<Text size="sm" c="gray.6">
<Text size="sm" c="gray.6" lineClamp={4}>
{app.description}
</Text>
)}
{app.href && (
<Anchor href={app.href} size="sm" w="min-content">
<Anchor href={app.href} lineClamp={1} size="sm" w="min-content">
{app.href}
</Anchor>
)}
@@ -85,7 +82,7 @@ const AppCard = ({ app }: AppCardProps) => {
href={`/manage/apps/edit/${app.id}`}
variant="subtle"
color="gray"
aria-label="Edit app"
aria-label={t("page.edit.title")}
>
<IconPencil size={16} stroke={1.5} />
</ActionIcon>
@@ -107,9 +104,7 @@ const AppNoResults = async () => {
<Text fw={500} size="lg">
{t("app.page.list.noResults.title")}
</Text>
<Anchor href="/manage/apps/new">
{t("app.page.list.noResults.description")}
</Anchor>
<Anchor href="/manage/apps/new">{t("app.page.list.noResults.description")}</Anchor>
</Stack>
</Card>
);
@@ -3,14 +3,14 @@
import { useCallback } from "react";
import Link from "next/link";
import { Menu } from "@mantine/core";
import { IconSettings, IconTrash } from "@tabler/icons-react";
import { IconHome, 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 { revalidatePathActionAsync } from "~/app/revalidatePathAction";
import { useBoardPermissions } from "~/components/board/permissions/client";
const iconProps = {
@@ -21,18 +21,11 @@ const iconProps = {
interface BoardCardMenuDropdownProps {
board: Pick<
RouterOutputs["board"]["getAllBoards"][number],
| "id"
| "name"
| "creator"
| "userPermissions"
| "groupPermissions"
| "isPublic"
"id" | "name" | "creator" | "userPermissions" | "groupPermissions" | "isPublic"
>;
}
export const BoardCardMenuDropdown = ({
board,
}: BoardCardMenuDropdownProps) => {
export const BoardCardMenuDropdown = ({ board }: BoardCardMenuDropdownProps) => {
const t = useScopedI18n("management.page.board.action");
const tCommon = useScopedI18n("common");
@@ -40,9 +33,15 @@ export const BoardCardMenuDropdown = ({
const { openConfirmModal } = useConfirmModal();
const { mutateAsync, isPending } = clientApi.board.deleteBoard.useMutation({
const setHomeBoardMutation = clientApi.board.setHomeBoard.useMutation({
onSettled: async () => {
await revalidatePathAction("/manage/boards");
// Revalidate all as it's part of the user settings, /boards page and board manage page
await revalidatePathActionAsync("/");
},
});
const deleteBoardMutation = clientApi.board.deleteBoard.useMutation({
onSettled: async () => {
await revalidatePathActionAsync("/manage/boards");
},
});
@@ -52,36 +51,45 @@ export const BoardCardMenuDropdown = ({
children: t("delete.confirm.description", {
name: board.name,
}),
// eslint-disable-next-line no-restricted-syntax
onConfirm: async () => {
await mutateAsync({
await deleteBoardMutation.mutateAsync({
id: board.id,
});
},
});
}, [board.id, board.name, mutateAsync, openConfirmModal, t]);
}, [board.id, board.name, deleteBoardMutation, openConfirmModal, t]);
const handleSetHomeBoard = useCallback(async () => {
await setHomeBoardMutation.mutateAsync({ id: board.id });
}, [board.id, setHomeBoardMutation]);
return (
<Menu.Dropdown>
<Menu.Item onClick={handleSetHomeBoard} leftSection={<IconHome {...iconProps} />}>
{t("setHomeBoard.label")}
</Menu.Item>
{hasChangeAccess && (
<Menu.Item
component={Link}
href={`/boards/${board.name}/settings`}
leftSection={<IconSettings {...iconProps} />}
>
{t("settings.label")}
</Menu.Item>
<>
<Menu.Divider />
<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.Label c="red.7">{tCommon("dangerZone")}</Menu.Label>
<Menu.Item
c="red.7"
leftSection={<IconTrash {...iconProps} />}
onClick={handleDeletion}
disabled={isPending}
disabled={deleteBoardMutation.isPending}
>
{t("delete.label")}
</Menu.Item>
@@ -1,83 +0,0 @@
"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>
);
};
@@ -1,15 +1,15 @@
"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 { revalidatePathActionAsync } from "~/app/revalidatePathAction";
import { AddBoardModal } from "~/components/manage/boards/add-board-modal";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
interface CreateBoardButtonProps {
boardNames: string[];
@@ -21,7 +21,7 @@ export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => {
const { mutateAsync, isPending } = clientApi.board.createBoard.useMutation({
onSettled: async () => {
await revalidatePathAction("/manage/boards");
await revalidatePathActionAsync("/manage/boards");
},
});
@@ -37,12 +37,8 @@ export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => {
}, [mutateAsync, boardNames, openModal]);
return (
<Button
leftSection={<IconCategoryPlus size="1rem" />}
onClick={onClick}
loading={isPending}
>
<MobileAffixButton leftSection={<IconCategoryPlus size="1rem" />} onClick={onClick} loading={isPending}>
{t("management.page.board.action.new.label")}
</Button>
</MobileAffixButton>
);
};
@@ -1,6 +1,7 @@
import Link from "next/link";
import {
ActionIcon,
Badge,
Button,
Card,
CardSection,
@@ -9,22 +10,22 @@ import {
Group,
Menu,
MenuTarget,
Stack,
Text,
Title,
Tooltip,
} from "@mantine/core";
import { IconDotsVertical, IconLock, IconWorld } from "@tabler/icons-react";
import "~/styles/gridstack.scss";
import { IconDotsVertical, IconHomeFilled, IconLock, IconWorld } from "@tabler/icons-react";
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 { getBoardPermissionsAsync } from "~/components/board/permissions/server";
import { ManageContainer } from "~/components/manage/manage-container";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
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() {
@@ -33,20 +34,23 @@ export default async function ManageBoardsPage() {
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>
<ManageContainer>
<DynamicBreadcrumb />
<Stack>
<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>
</>
<Grid mb={{ base: "xl", md: 0 }}>
{boards.map((board) => (
<GridCol span={{ base: 12, md: 6 }} key={board.id}>
<BoardCard board={board} />
</GridCol>
))}
</Grid>
</Stack>
</ManageContainer>
);
}
@@ -56,7 +60,7 @@ interface BoardCardProps {
const BoardCard = async ({ board }: BoardCardProps) => {
const t = await getScopedI18n("management.page.board");
const { hasChangeAccess: isMenuVisible } = await getBoardPermissions(board);
const { hasChangeAccess: isMenuVisible } = await getBoardPermissionsAsync(board);
const visibility = board.isPublic ? "public" : "private";
const VisibilityIcon = board.isPublic ? IconWorld : IconLock;
@@ -73,29 +77,28 @@ const BoardCard = async ({ board }: BoardCardProps) => {
</Text>
</Group>
{board.creator && (
<Group>
<Group>
{board.isHome && (
<Tooltip label={t("action.setHomeBoard.badge.tooltip")}>
<Badge tt="none" color="yellow" variant="light" leftSection={<IconHomeFilled size=".7rem" />}>
{t("action.setHomeBoard.badge.label")}
</Badge>
</Tooltip>
)}
{board.creator && (
<Group gap="xs">
<UserAvatar user={board.creator} size="sm" />
<Text>{board.creator?.name}</Text>
<Text>{board.creator.name}</Text>
</Group>
</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
>
<Button component={Link} href={`/boards/${board.name}`} variant="default" fullWidth>
{t("action.open.label")}
</Button>
{isMenuVisible && (
@@ -2,17 +2,7 @@
import { useState } from "react";
import { useParams } from "next/navigation";
import {
ActionIcon,
Avatar,
Button,
Card,
Collapse,
Group,
Kbd,
Stack,
Text,
} from "@mantine/core";
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";
@@ -22,7 +12,7 @@ import type { RouterOutputs } from "@homarr/api";
import { integrationSecretKindObject } from "@homarr/definitions";
import { useI18n } from "@homarr/translation/client";
import { integrationSecretIcons } from "./_integration-secret-icons";
import { integrationSecretIcons } from "./integration-secret-icons";
dayjs.extend(relativeTime);
@@ -36,8 +26,7 @@ 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 [publicSecretDisplayOpened, { toggle: togglePublicSecretDisplay }] = useDisclosure(false);
const [editMode, setEditMode] = useState(false);
const DisplayIcon = publicSecretDisplayOpened ? IconEye : IconEyeOff;
const KindIcon = integrationSecretIcons[secret.kind];
@@ -50,9 +39,7 @@ export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => {
<Avatar>
<KindIcon size={16} />
</Avatar>
<Text fw={500}>
{t(`integration.secrets.kind.${secret.kind}.label`)}
</Text>
<Text fw={500}>{t(`integration.secrets.kind.${secret.kind}.label`)}</Text>
{publicSecretDisplayOpened ? <Kbd>{secret.value}</Kbd> : null}
</Group>
<Group>
@@ -62,11 +49,7 @@ export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => {
})}
</Text>
{isPublic ? (
<ActionIcon
color="gray"
variant="subtle"
onClick={togglePublicSecretDisplay}
>
<ActionIcon color="gray" variant="subtle" onClick={togglePublicSecretDisplay}>
<DisplayIcon size={16} stroke={1.5} />
</ActionIcon>
) : null}
@@ -7,9 +7,10 @@ import { integrationSecretKindObject } from "@homarr/definitions";
import type { IntegrationSecretKind } from "@homarr/definitions";
import { useI18n } from "@homarr/translation/client";
import { integrationSecretIcons } from "./_integration-secret-icons";
import { integrationSecretIcons } from "./integration-secret-icons";
interface IntegrationSecretInputProps {
withAsterisk?: boolean;
label?: string;
kind: IntegrationSecretKind;
value?: string;
@@ -41,10 +42,7 @@ const PublicSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
);
};
const PrivateSecretInput = ({
kind,
...props
}: IntegrationSecretInputProps) => {
const PrivateSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
const t = useI18n();
const Icon = integrationSecretIcons[kind];
@@ -52,7 +50,7 @@ const PrivateSecretInput = ({
<PasswordInput
{...props}
label={props.label ?? t(`integration.secrets.kind.${kind}.label`)}
description={t(`integration.secrets.secureNotice`)}
description={t("integration.secrets.secureNotice")}
w="100%"
leftSection={<Icon size={20} stroke={1.5} />}
/>
@@ -6,23 +6,17 @@ import { IconTrash } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useConfirmModal } from "@homarr/modals";
import {
showErrorNotification,
showSuccessNotification,
} from "@homarr/notifications";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { revalidatePathAction } from "../../../revalidatePathAction";
import { revalidatePathActionAsync } from "../../../revalidatePathAction";
interface DeleteIntegrationActionButtonProps {
count: number;
integration: { id: string; name: string };
}
export const DeleteIntegrationActionButton = ({
count,
integration,
}: DeleteIntegrationActionButtonProps) => {
export const DeleteIntegrationActionButton = ({ count, integration }: DeleteIntegrationActionButtonProps) => {
const t = useScopedI18n("integration.page.delete");
const router = useRouter();
const { openConfirmModal } = useConfirmModal();
@@ -47,9 +41,9 @@ export const DeleteIntegrationActionButton = ({
message: t("notification.success.message"),
});
if (count === 1) {
router.replace("/integrations");
router.replace("/manage/integrations");
}
void revalidatePathAction("/integrations");
void revalidatePathActionAsync("/manage/integrations");
},
onError: () => {
showErrorNotification({
@@ -62,7 +56,7 @@ export const DeleteIntegrationActionButton = ({
},
});
}}
aria-label="Delete integration"
aria-label={t("title")}
>
<IconTrash color="red" size={16} stroke={1.5} />
</ActionIcon>
@@ -1,154 +0,0 @@
"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>
);
};
@@ -6,28 +6,18 @@ 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 { getAllSecretKindOptions, getDefaultSecretKinds } from "@homarr/definitions";
import { useZodForm } from "@homarr/form";
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
import { useConfirmModal } from "@homarr/modals";
import {
showErrorNotification,
showSuccessNotification,
} from "@homarr/notifications";
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";
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
import { SecretCard } from "../../_components/secrets/integration-secret-card";
import { IntegrationSecretInput } from "../../_components/secrets/integration-secret-inputs";
interface EditIntegrationForm {
integration: RouterOutputs["integration"]["byId"];
@@ -40,34 +30,23 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
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 form = useZodForm(validation.integration.update.omit({ id: true }), {
initialValues: {
name: integration.name,
url: integration.url,
secrets: secretsKinds.map((kind) => ({
kind,
value: integration.secrets.find((secret) => secret.kind === kind)?.value ?? "",
})),
},
});
const { mutateAsync, isPending } = clientApi.integration.update.useMutation();
const secretsMap = new Map(
integration.secrets.map((secret) => [secret.kind, secret]),
);
const secretsMap = new Map(integration.secrets.map((secret) => [secret.kind, secret]));
const handleSubmit = async (values: FormType) => {
if (isDirty) return;
const handleSubmitAsync = async (values: FormType) => {
await mutateAsync(
{
id: integration.id,
@@ -83,11 +62,21 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
title: t("integration.page.edit.notification.success.title"),
message: t("integration.page.edit.notification.success.message"),
});
void revalidatePathAction("/integrations").then(() =>
router.push("/integrations"),
);
void revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
},
onError: () => {
onError: (error) => {
const testConnectionError = convertIntegrationTestConnectionError(error.data?.error);
if (testConnectionError) {
showErrorNotification({
title: t(`integration.testConnection.notification.${testConnectionError.key}.title`),
message: testConnectionError.message
? testConnectionError.message
: t(`integration.testConnection.notification.${testConnectionError.key}.message`),
});
return;
}
showErrorNotification({
title: t("integration.page.edit.notification.error.title"),
message: t("integration.page.edit.notification.error.message"),
@@ -98,45 +87,32 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
};
return (
<form onSubmit={form.onSubmit((values) => void handleSubmit(values))}>
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
<Stack>
<TestConnectionNoticeAlert />
<TextInput withAsterisk label={t("integration.field.name.label")} {...form.getInputProps("name")} />
<TextInput
label={t("integration.field.name.label")}
{...form.getInputProps("name")}
/>
<TextInput
label={t("integration.field.url.label")}
{...form.getInputProps("url")}
/>
<TextInput withAsterisk 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}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
secret={secretsMap.get(kind)!}
onCancel={() =>
new Promise((res) => {
new Promise((resolve) => {
// When nothing changed, just close the secret card
if (
(form.values.secrets[index]?.value ?? "") ===
(secretsMap.get(kind)?.value ?? "")
) {
return res(true);
if ((form.values.secrets[index]?.value ?? "") === (secretsMap.get(kind)?.value ?? "")) {
return resolve(true);
}
openConfirmModal({
title: t("integration.secrets.reset.title"),
children: t("integration.secrets.reset.message"),
onCancel: () => res(false),
onCancel: () => resolve(false),
onConfirm: () => {
form.setFieldValue(
`secrets.${index}.value`,
secretsMap.get(kind)!.value ?? "",
);
res(true);
form.setFieldValue(`secrets.${index}.value`, secretsMap.get(kind)?.value ?? "");
resolve(true);
},
});
})
@@ -153,24 +129,13 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
</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 justify="end" align="center">
<Button variant="default" component={Link} href="/manage/integrations">
{t("common.action.backToOverview")}
</Button>
<Button type="submit" loading={isPending}>
{t("integration.testConnection.action.edit")}
</Button>
</Group>
</Stack>
</form>
@@ -4,6 +4,7 @@ import { api } from "@homarr/api/server";
import { getIntegrationName } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { IntegrationAvatar } from "../../_integration-avatar";
import { EditIntegrationForm } from "./_integration-edit-form";
@@ -11,23 +12,22 @@ interface EditIntegrationPageProps {
params: { id: string };
}
export default async function EditIntegrationPage({
params,
}: EditIntegrationPageProps) {
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>
<>
<DynamicBreadcrumb dynamicMappings={new Map([[params.id, integration.name]])} nonInteractable={["edit"]} />
<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>
</>
);
}
Loaded 100 of 548 files, more files were not shown because too many files have changed in this diff. Show more