Compare commits
432
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
024ec9235d | ||
|
|
58fdea0245 | ||
|
|
4ead238462 | ||
|
|
6305c74f90 | ||
|
|
0780080681 | ||
|
|
aa15a705d7 | ||
|
|
cc70175125 | ||
|
|
bc5e25e9cd | ||
|
|
d43557e165 | ||
|
|
b6c96f73a5 | ||
|
|
ee8831affe | ||
|
|
6e21863d04 | ||
|
|
ca39ca66f0 | ||
|
|
cc7d53e84e | ||
|
|
99c409cbb7 | ||
|
|
1fcf9d0146 | ||
|
|
a234fc844e | ||
|
|
168b587c18 | ||
|
|
dc12a82805 | ||
|
|
f5076454cd | ||
|
|
aeb681a858 | ||
|
|
56b57ad171 | ||
|
|
91e1cef611 | ||
|
|
e53b06e8c2 | ||
|
|
0ebf4bc55e | ||
|
|
b8a155dd08 | ||
|
|
63786eed06 | ||
|
|
598f0047eb | ||
|
|
eacda4c397 | ||
|
|
5cb726f793 | ||
|
|
e8d29420e0 | ||
|
|
23c7d0bbf0 | ||
|
|
75aefcb0d5 | ||
|
|
b52a8a7935 | ||
|
|
6d90209e00 | ||
|
|
8f0945f51a | ||
|
|
5b04b93f23 | ||
|
|
c8228b920c | ||
|
|
abf7942fb7 | ||
|
|
50746eb3e4 | ||
|
|
76abfca11b | ||
|
|
fcab6a347d | ||
|
|
c91aae53ac | ||
|
|
4943e7b851 | ||
|
|
88c423fca6 | ||
|
|
f238663ec7 | ||
|
|
e9870e7ee7 | ||
|
|
9c0a0ced04 | ||
|
|
4c27635066 | ||
|
|
3d66ff9148 | ||
|
|
ec257e5f60 | ||
|
|
ef24370a8c | ||
|
|
1fa0fc50c5 | ||
|
|
e9a1dae857 | ||
|
|
e37279fe80 | ||
|
|
82c6ce7b77 | ||
|
|
555212e2dc | ||
|
|
c831539772 | ||
|
|
3e2ffbd7bf | ||
|
|
22d7f676eb | ||
|
|
23bf262f4e | ||
|
|
ecae80d741 | ||
|
|
162e8adf4c | ||
|
|
620ec0315f | ||
|
|
8df398c3c7 | ||
|
|
425359be02 | ||
|
|
8893b72fe3 | ||
|
|
b9483379dd | ||
|
|
0336803550 | ||
|
|
e220087e96 | ||
|
|
f127c9325c | ||
|
|
c410b06a15 | ||
|
|
ce755efe4d | ||
|
|
8b416a6dd3 | ||
|
|
fe95334aa7 | ||
|
|
b0facfd77a | ||
|
|
01db27c34f | ||
|
|
999cf58625 | ||
|
|
9223dd06ee | ||
|
|
42da2b9b06 | ||
|
|
a73eed63eb | ||
|
|
44dcc72d43 | ||
|
|
d02ded5511 | ||
|
|
dfacb08dbd | ||
|
|
4e85bbbad0 | ||
|
|
d78c6ed529 | ||
|
|
210ce3669f | ||
|
|
44e5346d75 | ||
|
|
d34d665229 | ||
|
|
b6dc38483d | ||
|
|
a7e387e54c | ||
|
|
189f35f3cb | ||
|
|
3c8d4f5018 | ||
|
|
2b59f19a7f | ||
|
|
fff1da88bd | ||
|
|
720fdb772c | ||
|
|
a225bdd70c | ||
|
|
e40c348352 | ||
|
|
28e1175565 | ||
|
|
b450b00934 | ||
|
|
8e0bf87c56 | ||
|
|
9e061f8fc7 | ||
|
|
10855bd84c | ||
|
|
93d5ca683b | ||
|
|
d2584b63c5 | ||
|
|
8c1156240c | ||
|
|
bb7f751481 | ||
|
|
1e8a5359b7 | ||
|
|
550bca6dee | ||
|
|
2ae3d938ee | ||
|
|
2b4bc8352e | ||
|
|
c0f681637d | ||
|
|
92cd1c5bd0 | ||
|
|
3b7e6cc083 | ||
|
|
61afab518f | ||
|
|
a7bd664c5c | ||
|
|
a2c3bee66c | ||
|
|
b6dad438c2 | ||
|
|
044c60bdec | ||
|
|
de8485d5c3 | ||
|
|
26b6ca2445 | ||
|
|
63a7be35a4 | ||
|
|
1baa9fc74c | ||
|
|
a51999457b | ||
|
|
5fbbf037ce | ||
|
|
3c0f272707 | ||
|
|
d2b4bf5ae4 | ||
|
|
d803087345 | ||
|
|
d3a883a252 | ||
|
|
694350583a | ||
|
|
ecc4586d02 | ||
|
|
ce4fa5ff67 | ||
|
|
649506b56a | ||
|
|
9ab36f8956 | ||
|
|
bf59a2ad87 | ||
|
|
463607c805 | ||
|
|
50697eb21f | ||
|
|
70e0a11819 | ||
|
|
b384ebd342 | ||
|
|
4543a5fd2c | ||
|
|
52cd6fc913 | ||
|
|
daf7ea56c5 | ||
|
|
b47cfb2892 | ||
|
|
0f029b384d | ||
|
|
ada60a7004 | ||
|
|
93abe8f6a2 | ||
|
|
8b0e6435d0 | ||
|
|
2b5ed83de3 | ||
|
|
692ef7f6dc | ||
|
|
15d47d0585 | ||
|
|
c050ec4fe9 | ||
|
|
28667c5e1a | ||
|
|
dfa2e83238 | ||
|
|
21126b6adc | ||
|
|
02777d104c | ||
|
|
613d1f9662 | ||
|
|
8e4d67c3eb | ||
|
|
b7b5da654b | ||
|
|
e3bf3f371c | ||
|
|
73f7df771b | ||
|
|
8c596bf5fa | ||
|
|
922101dcbd | ||
|
|
032509e462 | ||
|
|
6de74d9525 | ||
|
|
82ec77d2da | ||
|
|
8ca00161fa | ||
|
|
d5399d20e3 | ||
|
|
f330c7e4f1 | ||
|
|
5bdc669e07 | ||
|
|
f857105994 | ||
|
|
60311918ce | ||
|
|
2ef36366ec | ||
|
|
3347886f1e | ||
|
|
dd9d8b5261 | ||
|
|
cf9656d91e | ||
|
|
da63d8ebf4 | ||
|
|
e0152f4ac7 | ||
|
|
732f4d45bd | ||
|
|
6418dfed38 | ||
|
|
0bd8e2c085 | ||
|
|
7d0a224a3f | ||
|
|
75c0f70ef6 | ||
|
|
4c7058b159 | ||
|
|
51948677fa | ||
|
|
d40ec67aa9 | ||
|
|
5e466e80c5 | ||
|
|
db7058fdd9 | ||
|
|
b6733c678c | ||
|
|
e692cc8917 | ||
|
|
99bb68012f | ||
|
|
6f874e87ab | ||
|
|
8a2546704b | ||
|
|
3bee3fe3f6 | ||
|
|
51c87c1a33 | ||
|
|
72e81f05a6 | ||
|
|
2825c9bf45 | ||
|
|
46ce06155b | ||
|
|
753e0ecad2 | ||
|
|
84e500279d | ||
|
|
6a79d01d79 | ||
|
|
42ab783085 | ||
|
|
364e399d62 | ||
|
|
5c11a95ddc | ||
|
|
3521d2d51a | ||
|
|
c840ff85bc | ||
|
|
aaa3e7ce0c | ||
|
|
58008c1e2f | ||
|
|
6d6840f85e | ||
|
|
79d52b6905 | ||
|
|
71f4f8c4c8 | ||
|
|
86cd1017b9 | ||
|
|
a5b366a92e | ||
|
|
ef65270778 | ||
|
|
f38f3438c9 | ||
|
|
096f03432c | ||
|
|
bf45860966 | ||
|
|
2686dad630 | ||
|
|
1037a747ea | ||
|
|
6c7cdb3adb | ||
|
|
d8006e7dcc | ||
|
|
770bfc65e3 | ||
|
|
feb93ef0c7 | ||
|
|
bbec01385f | ||
|
|
2e23b3dad8 | ||
|
|
825b1a3643 | ||
|
|
2193b6d8f8 | ||
|
|
31cc1abb51 | ||
|
|
fb1f0003e6 | ||
|
|
b0d623047e | ||
|
|
16cf818449 | ||
|
|
3b705f43b8 | ||
|
|
a8e045b223 | ||
|
|
57bbd43c74 | ||
|
|
716a2ab9d1 | ||
|
|
23cc4659c4 | ||
|
|
a1a6dc9d6e | ||
|
|
0e01dd8695 | ||
|
|
70ab29454a | ||
|
|
5ecf93a317 | ||
|
|
7f2713b0b9 | ||
|
|
838f7aae4d | ||
|
|
7343bdde13 | ||
|
|
3ffdc05442 | ||
|
|
a53a2cdd34 | ||
|
|
0ab9d0bd69 | ||
|
|
fb4a19bfe4 | ||
|
|
b277f444b2 | ||
|
|
d5f76218cd | ||
|
|
9968f149c6 | ||
|
|
6ad5fd5529 | ||
|
|
9b6eafc5fa | ||
|
|
cebc925b87 | ||
|
|
fd42bde76d | ||
|
|
2d7efc3ffa | ||
|
|
44e2105a8c | ||
|
|
4df6006f0e | ||
|
|
c1f2ede834 | ||
|
|
85a7b7df55 | ||
|
|
09dae45ae8 | ||
|
|
52d2a88b61 | ||
|
|
f2e7349cbd | ||
|
|
fc734e5931 | ||
|
|
9f982906f2 | ||
|
|
f2268c8041 | ||
|
|
c1fe6551ce | ||
|
|
91a199f7d6 | ||
|
|
27b8f65238 | ||
|
|
37f961c8cc | ||
|
|
63f863748e | ||
|
|
907c28c945 | ||
|
|
2e9b99dff7 | ||
|
|
93aedd994f | ||
|
|
bfe284cdd1 | ||
|
|
591c5b5cff | ||
|
|
3cbc20bdfe | ||
|
|
b6c28da32b | ||
|
|
7e349bb04b | ||
|
|
b68977c52c | ||
|
|
74e1b6b544 | ||
|
|
1c4d4f4e9d | ||
|
|
ecf299e03b | ||
|
|
225356cc4f | ||
|
|
32e6b8ee45 | ||
|
|
2e2814707e | ||
|
|
24daba743e | ||
|
|
d76b4d0ec1 | ||
|
|
982ab4393e | ||
|
|
5a3d9745d9 | ||
|
|
4a94d602c0 | ||
|
|
9fb14df210 | ||
|
|
38143b679e | ||
|
|
1fc48f9db0 | ||
|
|
8fea983c2e | ||
|
|
32ee9f3dcc | ||
|
|
cdfb61fb28 | ||
|
|
482ca497f4 | ||
|
|
219a6adedd | ||
|
|
50c96833a6 | ||
|
|
6018b20d4e | ||
|
|
ba4c9a683e | ||
|
|
86ae346979 | ||
|
|
06e4623f8d | ||
|
|
85a69b13e3 | ||
|
|
a9a3a4cdce | ||
|
|
2ffa9d0e71 | ||
|
|
73cb0b99ac | ||
|
|
563c5d8f3c | ||
|
|
72eda1f225 | ||
|
|
a2a65abd3d | ||
|
|
ed74604a54 | ||
|
|
ed10d63a04 | ||
|
|
ae6137710f | ||
|
|
a27a2c3da1 | ||
|
|
860d2e4f69 | ||
|
|
35f13d2a01 | ||
|
|
21dc2138b7 | ||
|
|
441cbbe717 | ||
|
|
0a46a8e477 | ||
|
|
fa625df16d | ||
|
|
18b579eb5f | ||
|
|
2f76e537b7 | ||
|
|
7ea9154b67 | ||
|
|
e233d60f87 | ||
|
|
b8877137ac | ||
|
|
6e772e0585 | ||
|
|
4ba9e94dd6 | ||
|
|
0ee343b99e | ||
|
|
879aa1152f | ||
|
|
1105f0029f | ||
|
|
f3ebd6ccdf | ||
|
|
775d0a53de | ||
|
|
70f9dc5460 | ||
|
|
27931be982 | ||
|
|
e9a74a01b9 | ||
|
|
d4f0aa761d | ||
|
|
6dcf1ad344 | ||
|
|
41f816be5b | ||
|
|
2fb014f2df | ||
|
|
908d0bbc50 | ||
|
|
36fc707705 | ||
|
|
5d38030841 | ||
|
|
4264f2320e | ||
|
|
12c3bb9848 | ||
|
|
9bc585648c | ||
|
|
acc440bb89 | ||
|
|
ccee3dd217 | ||
|
|
2edb9c701a | ||
|
|
07f89afea8 | ||
|
|
684b542979 | ||
|
|
3194733e95 | ||
|
|
3ca97fef0b | ||
|
|
7a9e9a1e0d | ||
|
|
9bd0e0f4ee | ||
|
|
a1fddaa6a6 | ||
|
|
11ce6faa37 | ||
|
|
f220bb6244 | ||
|
|
53acff9143 | ||
|
|
75d764bca8 | ||
|
|
2a45c5b946 | ||
|
|
4ab40c6fa8 | ||
|
|
ce32b4839d | ||
|
|
ceafd3747b | ||
|
|
15abfbbdf7 | ||
|
|
fc4cddcbac | ||
|
|
04895da08a | ||
|
|
747922f558 | ||
|
|
0aa72d118c | ||
|
|
2306ecda9f | ||
|
|
87221093d2 | ||
|
|
c51c5db3d7 | ||
|
|
aa503992af | ||
|
|
2a7d648049 | ||
|
|
27b8692a27 | ||
|
|
fe4dfc484b | ||
|
|
23574359f0 | ||
|
|
c6a1aeef4d | ||
|
|
7cd3156bd6 | ||
|
|
7966476cad | ||
|
|
10cebff4ed | ||
|
|
e05567d100 | ||
|
|
cd18458d0c | ||
|
|
31c661bc69 | ||
|
|
0809d5aa74 | ||
|
|
9535154128 | ||
|
|
319fb47744 | ||
|
|
7e1305c6a9 | ||
|
|
4643c0f004 | ||
|
|
68a72ce5e3 | ||
|
|
e332a6fcbe | ||
|
|
b7fc1ae4bb | ||
|
|
cf5bcab732 | ||
|
|
97eb4c54e2 | ||
|
|
8f393aa9e8 | ||
|
|
b45eeb95e9 | ||
|
|
90c105541e | ||
|
|
7a07f57d0c | ||
|
|
f31ccf2d6a | ||
|
|
aec9ec1efd | ||
|
|
8983734408 | ||
|
|
0c067c4e6f | ||
|
|
34506b6339 | ||
|
|
0ff7c8903b | ||
|
|
b294bf68cf | ||
|
|
deb5a716db | ||
|
|
326b769c23 | ||
|
|
49c0ebea6d | ||
|
|
f8bdd9c5c7 | ||
|
|
d908b3f676 | ||
|
|
baabf71707 | ||
|
|
02ddb1c9c6 | ||
|
|
3ff8539fec | ||
|
|
3bfdc2ded4 | ||
|
|
747660ee69 | ||
|
|
17401d1d72 | ||
|
|
134ecf1ee7 | ||
|
|
4e9b0af54c | ||
|
|
fd2ce2eee2 | ||
|
|
74a8495903 | ||
|
|
d3f9696127 | ||
|
|
3c3dbf51dc | ||
|
|
f03688144f | ||
|
|
8da1dd0d5f | ||
|
|
7a4c9a9b2e | ||
|
|
958c62c926 | ||
|
|
8d2d151787 | ||
|
|
87f4298d47 | ||
|
|
7ca67a13f1 | ||
|
|
9df0f2b5b5 | ||
|
|
5db973f935 | ||
|
|
a4ae38b5e4 | ||
|
|
4502569223 | ||
|
|
db198c6dab |
No files matched your search
+2
-1
@@ -6,4 +6,5 @@ README.md
|
||||
.next
|
||||
.git
|
||||
dev
|
||||
.build
|
||||
.build
|
||||
e2e
|
||||
+8
-5
@@ -4,6 +4,14 @@
|
||||
# This file will be committed to version control, so make sure not to have any secrets in it.
|
||||
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
|
||||
|
||||
# The below secret is not used anywhere but required for Auth.js (Would encrypt JWTs and Mail hashes, both not used)
|
||||
AUTH_SECRET="supersecret"
|
||||
|
||||
# The below secret is used to encrypt integration secrets in the database.
|
||||
# It should be a 32-byte string, generated by running `openssl rand -hex 32` on Unix
|
||||
# or starting the project without any (which will show a randomly generated one).
|
||||
SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
|
||||
# This is how you can use the sqlite driver:
|
||||
DB_DRIVER='better-sqlite3'
|
||||
DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
|
||||
@@ -20,11 +28,6 @@ DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
|
||||
# DB_PASSWORD='password'
|
||||
# DB_NAME='name-of-database'
|
||||
|
||||
|
||||
# You can generate the secret via 'openssl rand -base64 32' on Unix
|
||||
# @see https://next-auth.js.org/configuration/options#secret
|
||||
AUTH_SECRET='supersecret'
|
||||
|
||||
TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
# Configure logging to use winston logger
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
@@ -1,37 +1,78 @@
|
||||
name: 🐞 Bug Report
|
||||
description: Create a bug report to help us improve
|
||||
description: Report that something is broken, not working as intended or causes side-effects
|
||||
title: "bug: "
|
||||
labels: ["🐞❔ unconfirmed bug"]
|
||||
labels: ["needs triage"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Provide environment information
|
||||
description: |
|
||||
Run this command in your project root and paste the results in a code block:
|
||||
```bash
|
||||
npx envinfo --system --binaries
|
||||
```
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of the bug, as well as what you expected to happen when encountering it.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Link to reproduction
|
||||
description: Please provide a link to a reproduction of the bug. Issues without a reproduction repo may be ignored.
|
||||
label: Steps to reproduce
|
||||
description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To reproduce
|
||||
description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc.
|
||||
label: Impact
|
||||
description: How big is the impact of this bug? Does it make Homarr unusable? Is there any workaround that you're aware of?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the bug here, screenshots if applicable.
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of Homarr are you running?
|
||||
options:
|
||||
- 1.0.0-beta
|
||||
- Other (describe in "additional information")
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: installationMethod
|
||||
attributes:
|
||||
label: Installation method
|
||||
description: How do you run Homarr? Post docker-compose, configs or screenshots if applicable.
|
||||
options:
|
||||
- Docker Run
|
||||
- Docker Compose
|
||||
- Portainer
|
||||
- Helm
|
||||
- QNAP
|
||||
- Saltbox
|
||||
- EasyPanel
|
||||
- Unraid Apps
|
||||
- TrueNAS Apps
|
||||
- Synology
|
||||
- HomeAssistant Addon
|
||||
- From Source
|
||||
- Other (describe in "additional information")
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browser
|
||||
attributes:
|
||||
label: Browser
|
||||
description: If relevant, what browser do you use?
|
||||
options:
|
||||
- Firefox
|
||||
- Edge (Chromium)
|
||||
- Edge (Proprietary)
|
||||
- Chrome
|
||||
- Safari
|
||||
- Vivaldi
|
||||
- Brave
|
||||
- Samsung Internet
|
||||
- Other (describe in "additional information")
|
||||
default: 0
|
||||
validations:
|
||||
required: false
|
||||
@@ -2,26 +2,16 @@
|
||||
# See here: https://github.com/vercel/next.js/blob/canary/.github/ISSUE_TEMPLATE/3.feature_request.yml
|
||||
|
||||
name: 🛠 Feature Request
|
||||
description: Create a feature request for the core packages
|
||||
description: Request a new feature that you would like to have implemented
|
||||
title: "feat: "
|
||||
labels: ["✨ enhancement"]
|
||||
labels: ["needs triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to file a feature request. Please fill out this form as completely as possible.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature you'd like to request
|
||||
description: Please describe the feature as clear and concise as possible. Remember to add context as to why you believe this feature is needed.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like to see
|
||||
description: Please describe the solution you would like to see. Adding example usage is a good way to provide context.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
name: 🛠 Integration request
|
||||
description: Request support for a new integration (eg. Sonarr, Radarr)
|
||||
title: "feat: "
|
||||
labels: ["needs triage"]
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Project Website
|
||||
description: Post the link to the website of the application. Paste the official link.
|
||||
placeholder: ex. https://sonarr.tv/
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe what data should be consumed by Homarr
|
||||
description: Please describe what data Homarr should fetch from the integration. Specify in what interval data should be fetched and whether the user can also perform write operations (eg. deleting a movie or adding a user).
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the integration.
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Public API available?
|
||||
description: Is there a public API available, that we can consume in Homarr?
|
||||
options:
|
||||
- Yes, available on a website
|
||||
- Yes, available in the application itself
|
||||
- No
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Are you willing to contribute this yourself?
|
||||
options:
|
||||
- Yes
|
||||
- No
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,30 @@
|
||||
name: 🛠 Widget request
|
||||
description: Request a new widget (eg. Clock, Calendar, ...)
|
||||
title: "feat: "
|
||||
labels: ["needs triage"]
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Compatible integrations
|
||||
description: Post a list of the integrations that should be compatible with this widget. Divide using comma. Leave empty if no integration is needed.
|
||||
placeholder: ex. Sonarr, Radarr, Lidarr, Readarr, Nextcloud
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe what data should be displayed
|
||||
description: Please describe what data Homarr should display. Describe how elements should be intractable and what actions the user can perform.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the widget.
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Are you willing to contribute this yourself?
|
||||
options:
|
||||
- Yes
|
||||
- No
|
||||
validations:
|
||||
required: true
|
||||
+3
-3
@@ -4,9 +4,9 @@ on:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
approve-renovate-prs:
|
||||
approve-automatic-prs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor_id == 158783068 # Id of renovate bot see https://api.github.com/users/homarr-renovate%5Bbot%5D
|
||||
if: github.actor_id == 158783068 || github.actor_id == 190541745 # Id of renovate bot and crowdin bot see https://api.github.com/users/homarr-renovate%5Bbot%5D and https://api.github.com/users/homarr-crowdin%5Bbot%5D
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
app_id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
||||
- name: Install GitHub CLI
|
||||
run: sudo apt-get install -y gh
|
||||
- name: Approve Renovate PRs
|
||||
- name: Approve automatic PRs
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||
run: |
|
||||
@@ -67,11 +67,35 @@ jobs:
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
|
||||
- name: 'Report Coverage'
|
||||
- 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
|
||||
if: always()
|
||||
uses: davelosert/vitest-coverage-report-action@v2
|
||||
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup
|
||||
uses: ./tooling/github/setup
|
||||
- name: Build docker image
|
||||
id: build-docker-image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
context: .
|
||||
push: false
|
||||
tags: homarr-e2e
|
||||
network: host
|
||||
env:
|
||||
SKIP_ENV_VALIDATION: true
|
||||
- name: Install playwright browsers
|
||||
run: pnpm exec playwright install chromium
|
||||
- name: Run E2E Tests
|
||||
shell: bash
|
||||
run: pnpm test:e2e
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
name: "[Crowdin] Download translations"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *" # every day at midnight
|
||||
|
||||
jobs:
|
||||
download-crowdin-translations:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Obtain token
|
||||
id: obtainToken
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
private_key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
|
||||
app_id: ${{ secrets.CROWDIN_APP_ID }}
|
||||
|
||||
- name: Download Crowdin translations
|
||||
id: crowdin-download
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
localization_branch_name: crowdin
|
||||
create_pull_request: true
|
||||
pull_request_title: "chore(lang): updated translations from crowdin"
|
||||
pull_request_body: "New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)"
|
||||
commit_message: "chore(lang): update translations from crowdin"
|
||||
pull_request_base_branch_name: "dev"
|
||||
github_user_name: "Crowdin Homarr"
|
||||
github_user_email: "190541745+homarr-crowdin[bot]@users.noreply.github.com"
|
||||
skip_untranslated_strings: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Enable auto-merge
|
||||
if: steps.crowdin-download.outputs.pull_request_number != '' && steps.crowdin-download.outputs.pull_request_number != null
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||
run: |
|
||||
gh pr merge ${{steps.crowdin-download.outputs.pull_request_number}} --auto --squash --delete-branch --subject "chore(lang): updated translations from crowdin"
|
||||
@@ -0,0 +1,28 @@
|
||||
name: "[Crowdin] Upload translations"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- "packages/translation/src/lang/**"
|
||||
branches: [dev]
|
||||
|
||||
jobs:
|
||||
upload-crowdin-translations:
|
||||
# Don't run this action if the downloaded translations are being pushed
|
||||
if: "!contains(github.event.head_commit.message, 'chore(lang)')"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Upload Crowdin translations
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: true
|
||||
download_translations: false
|
||||
env:
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
@@ -4,6 +4,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- dev
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
send-notifications:
|
||||
@@ -11,11 +13,6 @@ on:
|
||||
required: false
|
||||
default: true
|
||||
description: Send notifications
|
||||
push-image:
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
description: Push Docker Image
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -27,51 +24,100 @@ env:
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
TURBO_TELEMETRY_DISABLED: 1
|
||||
|
||||
concurrency: production
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Create tag and release
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SKIP_RELEASE: ${{ github.event_name == 'workflow_dispatch' || github.ref_name == 'dev' }}
|
||||
outputs:
|
||||
version: ${{ steps.read-semver.outputs.version || steps.version-fallback.outputs.version }}
|
||||
git_ref: ${{ steps.read-git-ref.outputs.ref || github.ref }}
|
||||
|
||||
steps:
|
||||
- run: echo "Skipping release for workflow_dispatch event"
|
||||
if: env.SKIP_RELEASE == 'true'
|
||||
# The below generated version fallback represents a normalized branch name, for example "feature/branch-name" -> "feature-branch-name"
|
||||
- run: echo "version="$(echo ${{github.ref_name}} | sed 's/[^a-zA-Z0-9\-]/-/g') >> "$GITHUB_OUTPUT"
|
||||
id: version-fallback
|
||||
if: env.SKIP_RELEASE == 'true' && github.ref_name != 'main' && github.ref_name != 'beta'
|
||||
|
||||
- name: Obtain token
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
id: obtainToken
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
private_key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||
app_id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||
- uses: actions/checkout@v4
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v4
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
with:
|
||||
node-version: 22
|
||||
- run: npm i -g pnpm
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
- name: Install dependencies
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
run: |
|
||||
pnpm install
|
||||
- name: Run Semantic Release
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||
GIT_AUTHOR_NAME: "Releases Homarr"
|
||||
GIT_AUTHOR_EMAIL: "175486441+homarr-releases[bot]@users.noreply.github.com"
|
||||
GIT_COMMITTER_NAME: "Releases Homarr"
|
||||
GIT_COMMITTER_EMAIL: "175486441+homarr-releases[bot]@users.noreply.github.com"
|
||||
run: |
|
||||
pnpm release
|
||||
- name: Read semver output
|
||||
# We read the last tag either from the created release or from the current branch, this is to rerun the deployment job for the currently released version when it failed
|
||||
if: env.SKIP_RELEASE == 'false' || github.ref_name == 'main' || github.ref_name == 'beta'
|
||||
id: read-semver
|
||||
run: |
|
||||
git fetch --tags
|
||||
echo "version=$(git describe --tags --abbrev=0)" >> "$GITHUB_OUTPUT"
|
||||
- name: Read git ref
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
id: read-git-ref
|
||||
run: |
|
||||
echo "ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
- name: Update dev branch
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
continue-on-error: true # Prevent pipeline from failing when merge fails
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||
run: |
|
||||
git fetch origin dev
|
||||
git checkout dev
|
||||
git pull origin dev
|
||||
git merge ${{ github.ref_name }}
|
||||
git push origin dev
|
||||
deploy:
|
||||
name: Deploy docker image
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20]
|
||||
env:
|
||||
NEXT_VERSION: ${{ needs.release.outputs.version }}
|
||||
DEPLOY_LATEST: ${{ github.ref_name == 'main' }}
|
||||
DEPLOY_BETA: ${{ github.ref_name == 'beta' }}
|
||||
steps:
|
||||
- name: Discord notification
|
||||
if: ${{ github.events.inputs.send-notifications != false }}
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@master
|
||||
with:
|
||||
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: dev
|
||||
ref: ${{ needs.release.outputs.git_ref }}
|
||||
- name: Discord notification
|
||||
if: ${{ github.events.inputs.send-notifications != false }}
|
||||
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
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Discord notification
|
||||
if: ${{ github.events.inputs.send-notifications != false }}
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@master
|
||||
with:
|
||||
args: "Built application artifacts. Building images..."
|
||||
args: "Deployment of an image for version '${{env.NEXT_VERSION}}' has been triggered: [run ${{ github.run_number }}](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>)"
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -88,15 +134,12 @@ jobs:
|
||||
with:
|
||||
images: "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
|
||||
tags: |
|
||||
type=raw,value=alpha
|
||||
type=raw,value=early-adopters
|
||||
# tags: |
|
||||
# type=raw,value=latest
|
||||
# type=raw,value=${{ steps.semver.outputs.next }}
|
||||
${{ env.DEPLOY_LATEST == 'true' && 'type=raw,value=latest' || null }}
|
||||
${{ env.DEPLOY_BETA == 'true' && 'type=raw,value=beta' || null }}
|
||||
type=raw,value=${{ env.NEXT_VERSION }}
|
||||
- name: Build and push
|
||||
id: buildPushAction
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ github.events.inputs.push-image == 'true' || github.events.inputs.push-image == null }}
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
@@ -106,30 +149,9 @@ jobs:
|
||||
network: host
|
||||
env:
|
||||
SKIP_ENV_VALIDATION: true
|
||||
- name: Build
|
||||
id: buildPushDryAction
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ github.events.inputs.push-image == 'false' }}
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
push: false
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
network: host
|
||||
env:
|
||||
SKIP_ENV_VALIDATION: true
|
||||
- name: Discord notification
|
||||
if: ${{ github.events.inputs.send-notifications != false && (github.events.inputs.push-image == 'true' || github.events.inputs.push-image == null) }}
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@master
|
||||
with:
|
||||
args: "Deployment of image has completed. Image ID is '${{ steps.buildPushAction.outputs.imageid }}'."
|
||||
- name: Discord notification
|
||||
if: ${{ github.events.inputs.send-notifications != false && !(github.events.inputs.push-image == 'true' || github.events.inputs.push-image == null) }}
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@master
|
||||
with:
|
||||
args: "Deployment of image has completed. Image ID is '${{ steps.buildPushDryAction.outputs.imageid }}'. This was a dry run."
|
||||
args: "Deployment of image has completed for branch ${{ github.ref_name }}. Image ID is '${{ steps.buildPushAction.outputs.imageid }}'."
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
token: ${{ github.token }}
|
||||
branch: dev
|
||||
- name: Create pull request
|
||||
run: "gh pr create --title \"chore(release): automatic release ${{ steps.semver.outputs.next }}\" --body \"**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 }}``\" --base main --head dev --label automerge"
|
||||
run: 'gh pr create --title "chore(release): automatic release ${{ steps.semver.outputs.next }}" --body "**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 }}``" --base beta --head dev --label automerge'
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Discord notification
|
||||
|
||||
@@ -58,6 +58,8 @@ apps/websocket/wssServer.cjs
|
||||
apps/nextjs/.million/
|
||||
packages/cli/cli.cjs
|
||||
|
||||
# e2e mounts
|
||||
e2e/shared/tmp
|
||||
|
||||
#personal backgrounds
|
||||
apps/nextjs/public/images/background.png
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"branches": [
|
||||
"main",
|
||||
{
|
||||
"name": "beta",
|
||||
"prerelease": true,
|
||||
"channel": "beta"
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"@semantic-release/commit-analyzer",
|
||||
{
|
||||
"preset": "conventionalcommits"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/release-notes-generator",
|
||||
{
|
||||
"preset": "conventionalcommits"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/changelog",
|
||||
{
|
||||
"changelogFile": "CHANGELOG.md"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"npmPublish": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": ["package.json", "CHANGELOG.md"],
|
||||
"message": "chore(release): ${nextRelease.version} [skip ci]"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"successComment": false,
|
||||
"failComment": false,
|
||||
"releaseBodyTemplate": "<%= _.truncate(nextRelease.notes, { 'length': 124000, 'omission': '' }) %>"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
Vendored
+2
-1
@@ -3,6 +3,7 @@
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"yoavbls.pretty-ts-errors",
|
||||
"million.million-lint"
|
||||
"million.million-lint",
|
||||
"lokalise.i18n-ally"
|
||||
]
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# .vscode/i18n-ally-custom-framework.yml
|
||||
|
||||
# An array of strings which contain Language Ids defined by VS Code
|
||||
# You can check available language ids here: https://code.visualstudio.com/docs/languages/identifiers
|
||||
languageIds:
|
||||
- javascript
|
||||
- typescript
|
||||
- javascriptreact
|
||||
- typescriptreact
|
||||
|
||||
# An array of RegExes to find the key usage. **The key should be captured in the first match group**.
|
||||
# You should unescape RegEx strings in order to fit in the YAML file
|
||||
# To help with this, you can use https://www.freeformatter.com/json-escape.html
|
||||
usageMatchRegex:
|
||||
# The following example shows how to detect `t("your.i18n.keys")`
|
||||
# the `{key}` will be placed by a proper keypath matching regex,
|
||||
# you can ignore it and use your own matching rules as well
|
||||
- "[^\\w\\d]t\\(['\"`]({key})['\"`]"
|
||||
|
||||
# A RegEx to set a custom scope range. This scope will be used as a prefix when detecting keys
|
||||
# and works like how the i18next framework identifies the namespace scope from the
|
||||
# useTranslation() hook.
|
||||
# You should unescape RegEx strings in order to fit in the YAML file
|
||||
# To help with this, you can use https://www.freeformatter.com/json-escape.html
|
||||
scopeRangeRegex: "(getScopedI18n|useScopedI18n)\\(\\s*['\"](.*?)['\"]\\)"
|
||||
|
||||
# An array of strings containing refactor templates.
|
||||
# The "$1" will be replaced by the keypath specified.
|
||||
# Optional: uncomment the following two lines to use
|
||||
|
||||
# refactorTemplates:
|
||||
# - i18n.get("$1")
|
||||
|
||||
|
||||
# If set to true, only enables this custom framework (will disable all built-in frameworks)
|
||||
monopoly: true
|
||||
Vendored
+13
-5
@@ -14,6 +14,11 @@
|
||||
"gridstack",
|
||||
"homarr",
|
||||
"jellyfin",
|
||||
"llen",
|
||||
"lpop",
|
||||
"lpush",
|
||||
"lrange",
|
||||
"ltrim",
|
||||
"mantine",
|
||||
"manuel-rw",
|
||||
"Meierschlumpf",
|
||||
@@ -27,9 +32,12 @@
|
||||
"Umami"
|
||||
],
|
||||
"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"
|
||||
"i18n-ally.displayLanguage": "en",
|
||||
"i18n-ally.enabledFrameworks": [
|
||||
"custom"
|
||||
],
|
||||
"i18n-ally.localesPaths": [
|
||||
"packages/translation/src/lang",
|
||||
],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
}
|
||||
+1190
File diff suppressed because it is too large.
Load diff
+25
-67
@@ -1,4 +1,4 @@
|
||||
FROM node:20.18.0-alpine AS base
|
||||
FROM node:22.12.0-alpine AS base
|
||||
|
||||
FROM base AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
@@ -6,46 +6,15 @@ RUN apk update
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN npm i -g turbo
|
||||
RUN turbo prune @homarr/nextjs --docker --out-dir ./next-out
|
||||
RUN turbo prune @homarr/tasks --docker --out-dir ./tasks-out
|
||||
RUN turbo prune @homarr/websocket --docker --out-dir ./websocket-out
|
||||
RUN turbo prune @homarr/db --docker --out-dir ./migration-out
|
||||
RUN turbo prune @homarr/cli --docker --out-dir ./cli-out
|
||||
|
||||
# Add lockfile and package.json's of isolated subworkspace
|
||||
FROM base AS installer
|
||||
RUN apk add --no-cache libc6-compat curl bash
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
# First install the dependencies (as they change less often)
|
||||
COPY .gitignore .gitignore
|
||||
|
||||
COPY --from=builder /app/tasks-out/json/ .
|
||||
COPY --from=builder /app/websocket-out/json/ .
|
||||
COPY --from=builder /app/migration-out/json/ .
|
||||
COPY --from=builder /app/cli-out/json/ .
|
||||
COPY --from=builder /app/next-out/json/ .
|
||||
COPY --from=builder /app/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
|
||||
# Is used for postinstall of docs definitions
|
||||
COPY --from=builder /app/packages/definitions/src/docs ./packages/definitions/src/docs
|
||||
|
||||
# Uses the lockfile to install the dependencies
|
||||
RUN corepack enable pnpm && pnpm install --recursive --frozen-lockfile
|
||||
|
||||
# Install sharp for image optimization
|
||||
RUN corepack enable pnpm && pnpm install sharp -w
|
||||
|
||||
# Build the project
|
||||
COPY --from=builder /app/tasks-out/full/ .
|
||||
COPY --from=builder /app/websocket-out/full/ .
|
||||
COPY --from=builder /app/next-out/full/ .
|
||||
COPY --from=builder /app/migration-out/full/ .
|
||||
COPY --from=builder /app/cli-out/full/ .
|
||||
|
||||
# Copy static data as it is not part of the build
|
||||
COPY static-data ./static-data
|
||||
ARG SKIP_ENV_VALIDATION='true'
|
||||
@@ -56,53 +25,41 @@ RUN corepack enable pnpm && pnpm build
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# gettext is required for envsubst
|
||||
RUN apk add --no-cache redis nginx bash gettext
|
||||
# gettext is required for envsubst, openssl for generating AUTH_SECRET, su-exec for running application as non-root
|
||||
RUN apk add --no-cache redis nginx bash gettext su-exec openssl
|
||||
RUN mkdir /appdata
|
||||
RUN mkdir /appdata/db
|
||||
RUN mkdir /appdata/redis
|
||||
VOLUME /appdata
|
||||
RUN mkdir /secrets
|
||||
VOLUME /secrets
|
||||
|
||||
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# Enable homarr cli
|
||||
COPY --from=installer --chown=nextjs:nodejs /app/packages/cli/cli.cjs /app/apps/cli/cli.cjs
|
||||
COPY --from=builder /app/packages/cli/cli.cjs /app/apps/cli/cli.cjs
|
||||
RUN echo $'#!/bin/bash\ncd /app/apps/cli && node ./cli.cjs "$@"' > /usr/bin/homarr
|
||||
RUN chmod +x /usr/bin/homarr
|
||||
|
||||
# Don't run production as root
|
||||
RUN chown -R nextjs:nodejs /appdata
|
||||
RUN chown -R nextjs:nodejs /secrets
|
||||
RUN mkdir -p /var/cache/nginx && chown -R nextjs:nodejs /var/cache/nginx && \
|
||||
mkdir -p /var/log/nginx && chown -R nextjs:nodejs /var/log/nginx && \
|
||||
mkdir -p /var/lib/nginx && chown -R nextjs:nodejs /var/lib/nginx && \
|
||||
touch /run/nginx/nginx.pid && chown -R nextjs:nodejs /run/nginx/nginx.pid && \
|
||||
mkdir -p /etc/nginx/templates /etc/nginx/ssl/certs && chown -R nextjs:nodejs /etc/nginx
|
||||
USER nextjs
|
||||
RUN mkdir -p /var/cache/nginx && \
|
||||
mkdir -p /var/log/nginx && \
|
||||
mkdir -p /var/lib/nginx && \
|
||||
touch /run/nginx/nginx.pid && \
|
||||
mkdir -p /etc/nginx/templates /etc/nginx/ssl/certs
|
||||
|
||||
COPY --from=installer /app/apps/nextjs/next.config.mjs .
|
||||
COPY --from=installer /app/apps/nextjs/package.json .
|
||||
COPY --from=builder /app/apps/nextjs/next.config.mjs .
|
||||
COPY --from=builder /app/apps/nextjs/package.json .
|
||||
|
||||
COPY --from=installer --chown=nextjs:nodejs /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
|
||||
COPY --from=installer --chown=nextjs:nodejs /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
|
||||
COPY --from=installer --chown=nextjs:nodejs /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
|
||||
COPY --from=builder /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
|
||||
COPY --from=builder /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
|
||||
COPY --from=builder /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=builder /app/packages/db/migrations ./db/migrations
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=installer --chown=nextjs:nodejs /app/apps/nextjs/.next/standalone ./
|
||||
COPY --from=installer --chown=nextjs:nodejs /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
|
||||
COPY --from=installer --chown=nextjs:nodejs /app/apps/nextjs/public ./apps/nextjs/public
|
||||
COPY --chown=nextjs:nodejs scripts/run.sh ./run.sh
|
||||
COPY --chown=nextjs:nodejs scripts/generateEncryptionKey.js ./generateEncryptionKey.js
|
||||
COPY --chown=nextjs:nodejs packages/redis/redis.conf /app/redis.conf
|
||||
COPY --chown=nextjs:nodejs nginx.conf /etc/nginx/templates/nginx.conf
|
||||
COPY --from=builder /app/apps/nextjs/.next/standalone ./
|
||||
COPY --from=builder /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
|
||||
COPY --from=builder /app/apps/nextjs/public ./apps/nextjs/public
|
||||
COPY scripts/run.sh ./run.sh
|
||||
COPY --chmod=777 scripts/entrypoint.sh ./entrypoint.sh
|
||||
COPY packages/redis/redis.conf /app/redis.conf
|
||||
COPY nginx.conf /etc/nginx/templates/nginx.conf
|
||||
|
||||
|
||||
ENV DB_URL='/appdata/db/db.sqlite'
|
||||
@@ -110,4 +67,5 @@ ENV DB_DIALECT='sqlite'
|
||||
ENV DB_DRIVER='better-sqlite3'
|
||||
ENV AUTH_PROVIDERS='credentials'
|
||||
|
||||
CMD ["sh", "run.sh"]
|
||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||
CMD ["sh", "run.sh"]
|
||||
@@ -1,21 +1,201 @@
|
||||
MIT License
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
Copyright (c) 2023 Julius Marminge
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
1. Definitions.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (c) 2024 Meier Lukas, Thomas Camlong and Homarr Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,7 +0,0 @@
|
||||
# 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
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# Security Policy
|
||||
This policy is relevant if you found potential vulnerabilities in an audit.
|
||||
We consider something as a vulnerability if it...
|
||||
1. puts users or user data at risk
|
||||
2. enables third parties to gain control or access (e.g. [RATs](https://en.wikipedia.org/wiki/Remote_desktop_software#RAT), [privilege escalation](https://en.wikipedia.org/wiki/Privilege_escalation), ...)
|
||||
3. abuses the system in an unintended way (e.g. crypto mining, proxy, ...)
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| >1.0.0 | :white_check_mark: |
|
||||
| <1.0.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
We use [GitHub's system for reporting vulnerabilities](https://docs.github.com/en/enterprise-cloud@latest/code-security/security-advisories/working-with-repository-security-advisories/creating-a-repository-security-advisory).
|
||||
Click [**here to report an advisory**](https://github.com/homarr-labs/homarr/security/advisories/new). Our team will get notified and will get back to you within 1-6 business days.
|
||||
|
||||
As a general guideline; please provide as much detail as possible and provide reproduction steps / documentation regarding the re-creation.
|
||||
You may also provide a fork with a fix for the vulnerability.
|
||||
See https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html for guidelines regarding disclosure.
|
||||
|
||||
If you're unable / unwilling (or it's not safe) to disclose vulnerabilites via GitHub, please report them with the subject "Security advisory - CVEXXX" to our email homarr-labs@proton.me.
|
||||
Please never disclose security vulnerabilits on your own publicly - we'd like to search for a dimplomatic solution that is also safe for our users.
|
||||
|
||||
In your initial contact with us, please provide details according to the [OWASP guidelines for initial reports](https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html#initial-report).
|
||||
|
||||
Thank you!
|
||||
We're looking forward to your report
|
||||
@@ -1,12 +1,18 @@
|
||||
// Importing env files here to validate on build
|
||||
import "@homarr/auth/env.mjs";
|
||||
import "@homarr/db/env.mjs";
|
||||
import "@homarr/common/env.mjs";
|
||||
|
||||
import MillionLint from "@million/lint";
|
||||
import createNextIntlPlugin from "next-intl/plugin";
|
||||
|
||||
import "./src/env.mjs";
|
||||
|
||||
// Package path does not work... so we need to use relative path
|
||||
const withNextIntl = createNextIntlPlugin("../../packages/translation/src/request.ts");
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const config = {
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
reactStrictMode: true,
|
||||
/** We already do linting and typechecking as separate tasks in CI */
|
||||
@@ -34,4 +40,4 @@ const config = {
|
||||
// Skip transform is used because of webpack loader, without it for example 'Tooltip.Floating' will not work and show an error
|
||||
const withMillionLint = MillionLint.next({ rsc: true, skipTransform: true, telemetry: false });
|
||||
|
||||
export default config;
|
||||
export default withNextIntl(nextConfig);
|
||||
+31
-29
@@ -23,12 +23,13 @@
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
"@homarr/form": "workspace:^0.1.0",
|
||||
"@homarr/gridstack": "^1.0.3",
|
||||
"@homarr/gridstack": "^1.11.2",
|
||||
"@homarr/integrations": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^",
|
||||
"@homarr/modals": "workspace:^0.1.0",
|
||||
"@homarr/modals-collection": "workspace:^0.1.0",
|
||||
"@homarr/notifications": "workspace:^0.1.0",
|
||||
"@homarr/old-import": "workspace:^0.1.0",
|
||||
"@homarr/old-schema": "workspace:^0.1.0",
|
||||
"@homarr/redis": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
@@ -37,17 +38,18 @@
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@homarr/widgets": "workspace:^0.1.0",
|
||||
"@mantine/colors-generator": "^7.13.4",
|
||||
"@mantine/core": "^7.13.4",
|
||||
"@mantine/hooks": "^7.13.4",
|
||||
"@mantine/modals": "^7.13.4",
|
||||
"@mantine/tiptap": "^7.13.4",
|
||||
"@million/lint": "1.0.11",
|
||||
"@mantine/colors-generator": "^7.15.2",
|
||||
"@mantine/core": "^7.15.2",
|
||||
"@mantine/dropzone": "^7.15.2",
|
||||
"@mantine/hooks": "^7.15.2",
|
||||
"@mantine/modals": "^7.15.2",
|
||||
"@mantine/tiptap": "^7.15.2",
|
||||
"@million/lint": "1.0.14",
|
||||
"@t3-oss/env-nextjs": "^0.11.1",
|
||||
"@tabler/icons-react": "^3.20.0",
|
||||
"@tanstack/react-query": "^5.59.16",
|
||||
"@tanstack/react-query-devtools": "^5.59.16",
|
||||
"@tanstack/react-query-next-experimental": "5.59.16",
|
||||
"@tabler/icons-react": "^3.26.0",
|
||||
"@tanstack/react-query": "^5.62.12",
|
||||
"@tanstack/react-query-devtools": "^5.62.12",
|
||||
"@tanstack/react-query-next-experimental": "5.62.12",
|
||||
"@trpc/client": "next",
|
||||
"@trpc/next": "next",
|
||||
"@trpc/react-query": "next",
|
||||
@@ -58,37 +60,37 @@
|
||||
"chroma-js": "^3.1.2",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"flag-icons": "^7.2.3",
|
||||
"glob": "^11.0.0",
|
||||
"jotai": "^2.10.1",
|
||||
"jotai": "^2.11.0",
|
||||
"mantine-react-table": "2.0.0-beta.7",
|
||||
"next": "^14.2.16",
|
||||
"next": "^14.2.22",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"sass": "^1.80.4",
|
||||
"superjson": "2.2.1",
|
||||
"swagger-ui-react": "^5.17.14",
|
||||
"sass": "^1.83.0",
|
||||
"superjson": "2.2.2",
|
||||
"swagger-ui-react": "^5.18.2",
|
||||
"use-deep-compare-effect": "^1.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/chroma-js": "2.4.4",
|
||||
"@types/node": "^20.17.1",
|
||||
"@types/chroma-js": "2.4.5",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"concurrently": "^9.0.1",
|
||||
"eslint": "^9.13.0",
|
||||
"node-loader": "^2.0.0",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "^5.6.3"
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.17.0",
|
||||
"node-loader": "^2.1.0",
|
||||
"prettier": "^3.4.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 497 B |
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="0 0 575 289.83" width="575" height="289.83"><defs><path d="M575 24.91C573.44 12.15 563.97 1.98 551.91 0C499.05 0 76.18 0 23.32 0C10.11 2.17 0 14.16 0 28.61C0 51.84 0 237.64 0 260.86C0 276.86 12.37 289.83 27.64 289.83C79.63 289.83 495.6 289.83 547.59 289.83C561.65 289.83 573.26 278.82 575 264.57C575 216.64 575 48.87 575 24.91Z" id="d1pwhf9wy2"></path><path d="M69.35 58.24L114.98 58.24L114.98 233.89L69.35 233.89L69.35 58.24Z" id="g5jjnq26yS"></path><path d="M201.2 139.15C197.28 112.38 195.1 97.5 194.67 94.53C192.76 80.2 190.94 67.73 189.2 57.09C185.25 57.09 165.54 57.09 130.04 57.09L130.04 232.74L170.01 232.74L170.15 116.76L186.97 232.74L215.44 232.74L231.39 114.18L231.54 232.74L271.38 232.74L271.38 57.09L211.77 57.09L201.2 139.15Z" id="i3Prh1JpXt"></path><path d="M346.71 93.63C347.21 95.87 347.47 100.95 347.47 108.89C347.47 115.7 347.47 170.18 347.47 176.99C347.47 188.68 346.71 195.84 345.2 198.48C343.68 201.12 339.64 202.43 333.09 202.43C333.09 190.9 333.09 98.66 333.09 87.13C338.06 87.13 341.45 87.66 343.25 88.7C345.05 89.75 346.21 91.39 346.71 93.63ZM367.32 230.95C372.75 229.76 377.31 227.66 381.01 224.67C384.7 221.67 387.29 217.52 388.77 212.21C390.26 206.91 391.14 196.38 391.14 180.63C391.14 174.47 391.14 125.12 391.14 118.95C391.14 102.33 390.49 91.19 389.48 85.53C388.46 79.86 385.93 74.71 381.88 70.09C377.82 65.47 371.9 62.15 364.12 60.13C356.33 58.11 343.63 57.09 321.54 57.09C319.27 57.09 307.93 57.09 287.5 57.09L287.5 232.74L342.78 232.74C355.52 232.34 363.7 231.75 367.32 230.95Z" id="a4ov9rRGQm"></path><path d="M464.76 204.7C463.92 206.93 460.24 208.06 457.46 208.06C454.74 208.06 452.93 206.98 452.01 204.81C451.09 202.65 450.64 197.72 450.64 190C450.64 185.36 450.64 148.22 450.64 143.58C450.64 135.58 451.04 130.59 451.85 128.6C452.65 126.63 454.41 125.63 457.13 125.63C459.91 125.63 463.64 126.76 464.6 129.03C465.55 131.3 466.03 136.15 466.03 143.58C466.03 146.58 466.03 161.58 466.03 188.59C465.74 197.84 465.32 203.21 464.76 204.7ZM406.68 231.21L447.76 231.21C449.47 224.5 450.41 220.77 450.6 220.02C454.32 224.52 458.41 227.9 462.9 230.14C467.37 232.39 474.06 233.51 479.24 233.51C486.45 233.51 492.67 231.62 497.92 227.83C503.16 224.05 506.5 219.57 507.92 214.42C509.34 209.26 510.05 201.42 510.05 190.88C510.05 185.95 510.05 146.53 510.05 141.6C510.05 131 509.81 124.08 509.34 120.83C508.87 117.58 507.47 114.27 505.14 110.88C502.81 107.49 499.42 104.86 494.98 102.98C490.54 101.1 485.3 100.16 479.26 100.16C474.01 100.16 467.29 101.21 462.81 103.28C458.34 105.35 454.28 108.49 450.64 112.7C450.64 108.89 450.64 89.85 450.64 55.56L406.68 55.56L406.68 231.21Z" id="fk968BpsX"></path></defs><g><g><g><use xlink:href="#d1pwhf9wy2" opacity="1" fill="#f6c700" fill-opacity="1"></use><g><use xlink:href="#d1pwhf9wy2" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#g5jjnq26yS" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#g5jjnq26yS" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#i3Prh1JpXt" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#i3Prh1JpXt" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#a4ov9rRGQm" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#a4ov9rRGQm" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#fk968BpsX" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#fk968BpsX" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="708.767" height="179.332">
|
||||
<path fill="#d51007" d="m158.431 165.498-8.354-22.708s-13.575 15.14-33.932 15.14c-18.013 0-30.802-15.662-30.802-40.721 0-32.106 16.182-43.591 32.107-43.591 22.969 0 30.277 14.878 36.543 33.934l8.354 26.103c8.351 25.318 24.013 45.678 69.17 45.678 32.37 0 54.295-9.918 54.295-36.02 0-21.143-12.009-32.107-34.458-37.328l-16.705-3.654c-11.484-2.61-14.877-7.309-14.877-15.14 0-8.875 7.046-14.096 18.533-14.096 12.529 0 19.315 4.699 20.36 15.923l26.102-3.133c-2.088-23.492-18.271-33.15-44.896-33.15-23.491 0-46.462 8.875-46.462 37.327 0 17.75 8.614 28.975 30.277 34.195l17.752 4.175c13.312 3.133 17.748 8.614 17.748 16.185 0 9.656-9.396 13.572-27.146 13.572-26.364 0-37.325-13.834-43.591-32.89l-8.614-26.101c-10.961-33.934-28.452-46.463-63.169-46.463-38.37 0-58.731 24.275-58.731 65.517 0 39.677 20.361 61.08 56.906 61.08 29.492 0 43.59-13.834 43.59-13.834zM46.726 153.229c-2.61.784-5.221 1.306-8.614 1.306-6.265 0-10.703-2.87-10.703-10.442V1.827H0v148.792c0 19.577 13.575 27.672 29.497 27.672 5.221 0 10.181-.785 16.446-2.349l.783-22.713zm330.185-4.176c-6.787 4.701-12.529 7.051-20.36 7.051-9.92 0-15.401-5.221-15.401-18.012V77.006h36.023V55.603H341.41V26.625l-27.669 3.394v25.583h-17.49v21.403h17.49v66.826c0 24.02 13.834 35.5 36.284 35.5 12.269 0 23.232-2.346 31.847-7.305l-4.961-22.973zm23.807 9.396c0 10.705 8.354 19.318 19.056 19.318 11.226 0 19.578-8.613 19.578-19.318 0-10.963-8.353-19.313-19.578-19.313-10.702 0-19.056 8.35-19.056 19.313zm67.009-81.443v99.195h27.409V77.006h30.803V55.603h-30.803V44.638c0-16.444 7.049-21.665 18.534-21.665 8.092 0 13.574 1.825 19.839 5.221l4.437-22.974C530.638 1.827 522.023 0 511.582 0c-22.973 0-43.855 10.963-43.855 43.593v12.01h-17.489v21.403h17.489zm167.427 2.352c-3.133-19.578-15.923-26.629-32.63-26.629-16.706 0-31.062 7.571-37.329 26.104l-3.393-23.23h-22.188v120.598h27.409v-68.129c0-23.235 12.008-32.11 24.799-32.11 13.312 0 18.795 8.875 18.795 23.232V176.2h27.147v-68.39c0-22.974 12.269-31.849 25.061-31.849 13.052 0 18.532 8.875 18.532 23.232v77.006h27.409v-86.66c0-25.843-15.14-36.81-35.24-36.81-16.965 0-32.107 7.571-38.372 26.629z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.4 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 190.24 81.52"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" y1="40.76" x2="190.24" y2="40.76" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset="0.56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><title>Asset 2</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M105.67,36.06h66.9A17.67,17.67,0,0,0,190.24,18.4h0A17.67,17.67,0,0,0,172.57.73h-66.9A17.67,17.67,0,0,0,88,18.4h0A17.67,17.67,0,0,0,105.67,36.06Zm-88,45h76.9A17.67,17.67,0,0,0,112.24,63.4h0A17.67,17.67,0,0,0,94.57,45.73H17.67A17.67,17.67,0,0,0,0,63.4H0A17.67,17.67,0,0,0,17.67,81.06ZM10.41,35.42h7.8V6.92h10.1V0H.31v6.9h10.1Zm28.1,0h7.8V8.25h.1l9,27.15h6l9.3-27.15h.1V35.4h7.8V0H66.76l-8.2,23.1h-.1L50.31,0H38.51ZM152.43,55.67a15.07,15.07,0,0,0-4.52-5.52,18.57,18.57,0,0,0-6.68-3.08,33.54,33.54,0,0,0-8.07-1h-11.7v35.4h12.75a24.58,24.58,0,0,0,7.55-1.15A19.34,19.34,0,0,0,148.11,77a16.27,16.27,0,0,0,4.37-5.5,16.91,16.91,0,0,0,1.63-7.58A18.5,18.5,0,0,0,152.43,55.67ZM145,68.6A8.8,8.8,0,0,1,142.36,72a10.7,10.7,0,0,1-4,1.82,21.57,21.57,0,0,1-5,.55h-4.05v-21h4.6a17,17,0,0,1,4.67.63,11.66,11.66,0,0,1,3.88,1.87A9.14,9.14,0,0,1,145,59a9.87,9.87,0,0,1,1,4.52A11.89,11.89,0,0,1,145,68.6Zm44.63-.13a8,8,0,0,0-1.58-2.62A8.38,8.38,0,0,0,185.63,64a10.31,10.31,0,0,0-3.17-1v-.1a9.22,9.22,0,0,0,4.42-2.82,7.43,7.43,0,0,0,1.68-5,8.42,8.42,0,0,0-1.15-4.65,8.09,8.09,0,0,0-3-2.72,12.56,12.56,0,0,0-4.18-1.3,32.84,32.84,0,0,0-4.62-.33h-13.2v35.4h14.5a22.41,22.41,0,0,0,4.72-.5,13.53,13.53,0,0,0,4.28-1.65,9.42,9.42,0,0,0,3.1-3,8.52,8.52,0,0,0,1.2-4.68A9.39,9.39,0,0,0,189.66,68.47ZM170.21,52.72h5.3a10,10,0,0,1,1.85.18,6.18,6.18,0,0,1,1.7.57,3.39,3.39,0,0,1,1.22,1.13,3.22,3.22,0,0,1,.48,1.82,3.63,3.63,0,0,1-.43,1.8,3.4,3.4,0,0,1-1.12,1.2,4.92,4.92,0,0,1-1.58.65,7.51,7.51,0,0,1-1.77.2h-5.65Zm11.72,20a3.9,3.9,0,0,1-1.22,1.3,4.64,4.64,0,0,1-1.68.7,8.18,8.18,0,0,1-1.82.2h-7v-8h5.9a15.35,15.35,0,0,1,2,.15,8.47,8.47,0,0,1,2.05.55,4,4,0,0,1,1.57,1.18,3.11,3.11,0,0,1,.63,2A3.71,3.71,0,0,1,181.93,72.72Z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="64mm"
|
||||
height="64mm"
|
||||
viewBox="0 0 64 64"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.2-alpha1 (b6a15bbbed, 2022-02-23)"
|
||||
sodipodi:docname="vgmdb.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.1089995"
|
||||
inkscape:cx="96.017091"
|
||||
inkscape:cy="132.29021"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1017"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="456"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
style="fill:#000000;stroke-width:0.21854"
|
||||
d="m 24.915135,62.650271 c 0.887879,-1.778597 2.041345,-3.66055 2.243584,-3.66055 0.373389,0 3.112623,2.34553 4.058216,3.474939 l 0.933036,1.114407 h -3.849247 -3.849246 z m 12.769897,0.133716 c 0.188983,-1.699691 0.540579,-9.438559 0.541952,-11.928786 l 0.0013,-2.561742 1.233546,-0.358603 c 1.858993,-0.540424 4.864719,-0.351614 6.306093,0.396126 2.102595,1.090761 3.373658,3.018391 3.382575,5.129841 0.0167,3.956206 -2.25508,7.722455 -5.608397,9.297806 -1.304436,0.612809 -1.766356,0.710219 -3.720191,0.784508 l -2.230831,0.08482 0.09384,-0.843972 z M 15.28167,58.692354 C 13.855849,58.372721 13.307133,58.098082 12.027965,57.063833 10.767623,56.044804 9.571224,54.160633 8.9251418,52.177293 8.4791686,50.808245 8.4074221,50.178438 8.4306037,47.836119 8.5286648,37.927793 11.975024,28.061437 18.601259,18.719217 c 1.935825,-2.729288 2.971061,-3.715223 4.909347,-4.675547 2.754154,-1.364545 6.425257,-1.245307 7.097695,0.230534 0.528157,1.159175 -0.351225,2.928281 -5.658254,11.38306 -3.70676,5.905347 -6.114927,9.18009 -8.52131,11.587702 l -1.747431,1.748322 0.748093,2.185403 c 1.052426,3.074452 2.737855,6.406887 3.730724,7.376398 l 0.831803,0.812233 1.792285,-1.012255 c 0.985756,-0.556742 3.87521,-2.58299 6.421008,-4.502779 l 4.628722,-3.490523 -0.152738,4.29269 c -0.08401,2.36098 -0.222472,4.480622 -0.3077,4.710314 -0.332428,0.895893 -8.050465,7.1666 -10.09734,8.203831 -2.185833,1.107645 -5.028822,1.564407 -6.994493,1.123754 z m 12.832756,-0.03877 c -0.369353,-0.206871 -0.546974,-0.44325 -0.453135,-0.603036 0.708434,-1.206309 4.008342,-5.172791 4.307161,-5.177197 0.225634,-0.0033 -0.01427,3.110825 -0.312679,4.058824 -0.487124,1.547514 -2.288476,2.42313 -3.541347,1.721409 z M 54.400377,47.270961 c -2.064993,-1.073365 -4.152108,-1.442529 -8.74161,-1.546198 -3.114822,-0.07036 -4.685696,-0.01346 -5.837847,0.211437 l -1.576311,0.307699 -0.101497,-0.893553 C 38.087288,44.858893 37.933571,42.73579 37.801516,40.63234 l -0.240099,-3.824455 0.497396,-0.409308 c 3.74237,-3.079615 11.651909,-7.818064 15.417757,-9.236475 2.486857,-0.936679 2.235049,-2.072647 2.235049,10.082864 0,5.829561 -0.07376,10.585246 -0.163906,10.568187 -0.09015,-0.01705 -0.606449,-0.261044 -1.147336,-0.542192 z M 23.409747,36.75325 c 0.981959,-3.570355 4.756318,-11.031164 6.826933,-13.49486 0.464503,-0.552684 0.586271,-0.604153 0.775553,-0.327811 0.553602,0.808232 1.251919,4.110924 1.621318,7.668016 l 0.126807,1.221076 -3.079512,1.795215 c -1.693731,0.987367 -3.814306,2.281815 -4.71239,2.876551 -0.898084,0.594738 -1.666906,1.081339 -1.708492,1.081339 -0.04159,0 0.02582,-0.368786 0.149783,-0.819526 z m 13.184647,-7.110817 c -0.04629,-0.145705 -0.341909,-1.543377 -0.656926,-3.10594 -0.315019,-1.562562 -0.864147,-3.967401 -1.220285,-5.344086 l -0.647526,-2.503063 1.150575,-1.176622 c 0.632814,-0.647143 2.643448,-2.301196 4.468071,-3.675673 3.438183,-2.589963 4.301443,-3.419286 4.80161,-4.6128516 0.714642,-1.7053778 -0.03157,-3.7643498 -1.67217,-4.613896 -0.898369,-0.4651992 -1.181912,-0.4997701 -3.278104,-0.3996832 -2.190425,0.1045861 -4.050576,0.523224 -8.85088,1.9919437 l -1.092702,0.334327 0.546351,-0.4412242 c 6.292255,-5.0815261 14.525967,-6.98424479 19.646997,-4.540202 2.438424,1.163752 4.050578,3.1575295 5.198539,6.4291158 l 0.605044,1.7243152 0.0047,7.3023343 0.0047,7.302333 -2.622483,0.481857 c -4.702423,0.864027 -9.663403,2.365904 -14.706862,4.452327 -1.118146,0.462565 -1.619586,0.580467 -1.67861,0.394688 z"
|
||||
id="path6625" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,5 @@
|
||||
import definition from "../../boards/(content)/(home)/_definition";
|
||||
|
||||
const { layout } = definition;
|
||||
|
||||
export default layout;
|
||||
@@ -0,0 +1,7 @@
|
||||
import definition from "../../boards/(content)/(home)/_definition";
|
||||
|
||||
const { generateMetadataAsync: generateMetadata, page } = definition;
|
||||
|
||||
export default page;
|
||||
|
||||
export { generateMetadata };
|
||||
@@ -0,0 +1,3 @@
|
||||
import HomeBoardNotFoundPage from "../boards/(content)/not-found";
|
||||
|
||||
export default HomeBoardNotFoundPage;
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { AppShellMain } from "@mantine/core";
|
||||
|
||||
import { MainHeader } from "~/components/layout/header";
|
||||
import { ClientShell } from "~/components/layout/shell";
|
||||
|
||||
export default function MainLayout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<ClientShell hasNavigation={false}>
|
||||
<MainHeader />
|
||||
<AppShellMain>{children}</AppShellMain>
|
||||
</ClientShell>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Stack, Title } from "@mantine/core";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<Stack>
|
||||
<Title>Home</Title>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
import { useSuspenseDayJsLocalization } from "@homarr/translation/dayjs";
|
||||
|
||||
export const DayJsLoader = ({ children }: PropsWithChildren) => {
|
||||
// Load the dayjs localization for the current locale with suspense
|
||||
useSuspenseDayJsLocalization();
|
||||
|
||||
return children;
|
||||
};
|
||||
@@ -8,14 +8,18 @@ import dayjs from "dayjs";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { parseCookies, setClientCookie } from "@homarr/common";
|
||||
import type { ColorScheme } from "@homarr/definitions";
|
||||
import { colorSchemeCookieKey } from "@homarr/definitions";
|
||||
|
||||
export const CustomMantineProvider = ({ children }: PropsWithChildren) => {
|
||||
export const CustomMantineProvider = ({
|
||||
children,
|
||||
defaultColorScheme,
|
||||
}: PropsWithChildren<{ defaultColorScheme: ColorScheme }>) => {
|
||||
const manager = useColorSchemeManager();
|
||||
|
||||
return (
|
||||
<DirectionProvider>
|
||||
<MantineProvider
|
||||
defaultColorScheme="dark"
|
||||
defaultColorScheme={defaultColorScheme}
|
||||
colorSchemeManager={manager}
|
||||
theme={createTheme({
|
||||
primaryColor: "red",
|
||||
@@ -28,12 +32,11 @@ export const CustomMantineProvider = ({ children }: PropsWithChildren) => {
|
||||
);
|
||||
};
|
||||
|
||||
function useColorSchemeManager(): MantineColorSchemeManager {
|
||||
const key = "homarr-color-scheme";
|
||||
export function useColorSchemeManager(): MantineColorSchemeManager {
|
||||
const { data: session } = useSession();
|
||||
|
||||
const updateCookieValue = (value: Exclude<MantineColorScheme, "auto">) => {
|
||||
setClientCookie(key, value, { expires: dayjs().add(1, "year").toDate(), path: "/" });
|
||||
setClientCookie(colorSchemeCookieKey, value, { expires: dayjs().add(1, "year").toDate(), path: "/" });
|
||||
};
|
||||
|
||||
const { mutate: mutateColorScheme } = clientApi.user.changeColorScheme.useMutation({
|
||||
@@ -50,7 +53,7 @@ function useColorSchemeManager(): MantineColorSchemeManager {
|
||||
|
||||
try {
|
||||
const cookies = parseCookies(document.cookie);
|
||||
return (cookies[key] as MantineColorScheme | undefined) ?? defaultValue;
|
||||
return (cookies[colorSchemeCookieKey] as MantineColorScheme | undefined) ?? defaultValue;
|
||||
} catch {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
import { defaultLocale } from "@homarr/translation";
|
||||
import { I18nProviderClient } from "@homarr/translation/client";
|
||||
|
||||
export const NextInternationalProvider = ({ children, locale }: PropsWithChildren<{ locale: string }>) => {
|
||||
return (
|
||||
<I18nProviderClient locale={locale} fallback={defaultLocale}>
|
||||
{children}
|
||||
</I18nProviderClient>
|
||||
);
|
||||
};
|
||||
@@ -15,14 +15,24 @@ import {
|
||||
wsLink,
|
||||
} from "@trpc/client";
|
||||
import superjson from "superjson";
|
||||
import type { SuperJSONResult } from "superjson";
|
||||
|
||||
import type { AppRouter } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { clientApi, createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/client";
|
||||
|
||||
import { env } from "~/env.mjs";
|
||||
|
||||
const getWebSocketProtocol = () => {
|
||||
// window is not defined on server side
|
||||
if (typeof window === "undefined") {
|
||||
return "ws";
|
||||
}
|
||||
|
||||
return window.location.protocol === "https:" ? "wss" : "ws";
|
||||
};
|
||||
|
||||
const constructWebsocketUrl = () => {
|
||||
const fallback = "ws://localhost:3001/websockets";
|
||||
const fallback = `${getWebSocketProtocol()}://localhost:3001/websockets`;
|
||||
if (typeof window === "undefined") {
|
||||
return fallback;
|
||||
}
|
||||
@@ -31,7 +41,7 @@ const constructWebsocketUrl = () => {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return `ws://${window.location.hostname}:${window.location.port}/websockets`;
|
||||
return `${getWebSocketProtocol()}://${window.location.hostname}:${window.location.port}/websockets`;
|
||||
};
|
||||
|
||||
const wsClient = createWSClient({
|
||||
@@ -73,20 +83,17 @@ export function TRPCReactProvider(props: PropsWithChildren) {
|
||||
serialize(object: unknown) {
|
||||
return object;
|
||||
},
|
||||
deserialize(data: unknown) {
|
||||
return data;
|
||||
deserialize(data: SuperJSONResult) {
|
||||
return superjson.deserialize<unknown>(data);
|
||||
},
|
||||
},
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
url: getTrpcUrl(),
|
||||
headers: createHeadersCallbackForSource("nextjs-react (form-data)"),
|
||||
}),
|
||||
false: unstable_httpBatchStreamLink({
|
||||
transformer: superjson,
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
headers() {
|
||||
const headers = new Headers();
|
||||
headers.set("x-trpc-source", "nextjs-react");
|
||||
return headers;
|
||||
},
|
||||
url: getTrpcUrl(),
|
||||
headers: createHeadersCallbackForSource("nextjs-react (json)"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
@@ -103,9 +110,3 @@ export function TRPCReactProvider(props: PropsWithChildren) {
|
||||
</clientApi.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function getBaseUrl() {
|
||||
if (typeof window !== "undefined") return window.location.origin;
|
||||
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
|
||||
return `http://localhost:${process.env.PORT ?? 3000}`;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { Card, Center, Stack, Text, Title } from "@mantine/core";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { isProviderEnabled } from "@homarr/auth/server";
|
||||
import { and, db, eq } from "@homarr/db";
|
||||
import { invites } from "@homarr/db/schema/sqlite";
|
||||
import { invites } from "@homarr/db/schema";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo";
|
||||
@@ -57,7 +57,7 @@ export default async function InviteUsagePage({ params, searchParams }: InviteUs
|
||||
{t("subtitle")}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Card bg="dark.8" w={64 * 6} maw="90vw">
|
||||
<Card withBorder w={64 * 6} maw="90vw">
|
||||
<RegistrationForm invite={invite} />
|
||||
</Card>
|
||||
<Text size="xs" c="gray.5" ta="center">
|
||||
|
||||
@@ -35,7 +35,7 @@ export default async function Login({ searchParams }: LoginProps) {
|
||||
{t("subtitle")}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Card bg="dark.8" w={64 * 6} maw="90vw">
|
||||
<Card withBorder w={64 * 6} maw="90vw">
|
||||
<LoginForm
|
||||
providers={env.AUTH_PROVIDERS}
|
||||
oidcClientName={env.AUTH_OIDC_CLIENT_NAME}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { api } from "@homarr/api/server";
|
||||
|
||||
import { createBoardContentPage } from "../_creator";
|
||||
import { createBoardContentPage } from "../../_creator";
|
||||
|
||||
export default createBoardContentPage<{ locale: string; name: string }>({
|
||||
async getInitialBoardAsync({ name }) {
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,18 @@
|
||||
import { IconLayoutOff } from "@tabler/icons-react";
|
||||
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { BoardNotFound } from "~/components/board/not-found";
|
||||
|
||||
export default async function BoardNotFoundPage() {
|
||||
const tNotFound = await getScopedI18n("board.error.notFound");
|
||||
return (
|
||||
<BoardNotFound
|
||||
icon={IconLayoutOff}
|
||||
title={tNotFound("title")}
|
||||
description={tNotFound("description")}
|
||||
link={{ label: tNotFound("link"), href: "/manage/boards" }}
|
||||
notice={tNotFound("notice")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import type { MouseEvent } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Group, Menu } from "@mantine/core";
|
||||
import { useHotkeys } from "@mantine/hooks";
|
||||
import {
|
||||
@@ -16,7 +18,7 @@ import {
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { useConfirmModal, useModalAction } from "@homarr/modals";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
@@ -26,6 +28,7 @@ import { useCategoryActions } from "~/components/board/sections/category/categor
|
||||
import { CategoryEditModal } from "~/components/board/sections/category/category-edit-modal";
|
||||
import { useDynamicSectionActions } from "~/components/board/sections/dynamic/dynamic-actions";
|
||||
import { HeaderButton } from "~/components/layout/header/button";
|
||||
import { env } from "~/env.mjs";
|
||||
import { useEditMode, useRequiredBoard } from "./_context";
|
||||
|
||||
export const BoardContentHeaderActions = () => {
|
||||
@@ -139,6 +142,7 @@ const EditModeMenu = () => {
|
||||
}, [board, isEditMode, saveBoard, setEditMode]);
|
||||
|
||||
useHotkeys([["mod+e", toggle]]);
|
||||
usePreventLeaveWithDirty(isEditMode);
|
||||
|
||||
return (
|
||||
<HeaderButton onClick={toggle} loading={isPending}>
|
||||
@@ -146,3 +150,63 @@ const EditModeMenu = () => {
|
||||
</HeaderButton>
|
||||
);
|
||||
};
|
||||
|
||||
const usePreventLeaveWithDirty = (isDirty: boolean) => {
|
||||
const t = useI18n();
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const handleClick = (event: MouseEvent<HTMLElement>) => {
|
||||
const target = (event.target as HTMLElement).closest("a");
|
||||
|
||||
if (!target) return;
|
||||
if (!isDirty) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
openConfirmModal({
|
||||
title: t("board.action.edit.confirmLeave.title"),
|
||||
children: t("board.action.edit.confirmLeave.message"),
|
||||
onConfirm() {
|
||||
router.push(target.href);
|
||||
},
|
||||
confirmProps: {
|
||||
children: t("common.action.discard"),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handlePopState = (event: Event) => {
|
||||
if (isDirty) {
|
||||
window.history.pushState(null, document.title, window.location.href);
|
||||
event.preventDefault();
|
||||
} else {
|
||||
window.history.back();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||
if (!isDirty) return;
|
||||
if (env.NODE_ENV === "development") return; // Allow to reload in development
|
||||
|
||||
event.preventDefault();
|
||||
event.returnValue = true;
|
||||
};
|
||||
|
||||
document.querySelectorAll("a").forEach((link) => {
|
||||
link.addEventListener("click", handleClick as never);
|
||||
});
|
||||
window.addEventListener("popstate", handlePopState);
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
|
||||
return () => {
|
||||
document.querySelectorAll("a").forEach((link) => {
|
||||
link.removeEventListener("click", handleClick as never);
|
||||
window.removeEventListener("popstate", handlePopState);
|
||||
});
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isDirty]);
|
||||
};
|
||||
@@ -4,10 +4,17 @@ import type { PropsWithChildren } from "react";
|
||||
import type { MantineColorsTuple } from "@mantine/core";
|
||||
import { createTheme, darken, lighten, MantineProvider } from "@mantine/core";
|
||||
|
||||
import type { ColorScheme } from "@homarr/definitions";
|
||||
|
||||
import { useColorSchemeManager } from "../../_client-providers/mantine";
|
||||
import { useRequiredBoard } from "./_context";
|
||||
|
||||
export const BoardMantineProvider = ({ children }: PropsWithChildren) => {
|
||||
export const BoardMantineProvider = ({
|
||||
children,
|
||||
defaultColorScheme,
|
||||
}: PropsWithChildren<{ defaultColorScheme: ColorScheme }>) => {
|
||||
const board = useRequiredBoard();
|
||||
const colorSchemeManager = useColorSchemeManager();
|
||||
|
||||
const theme = createTheme({
|
||||
colors: {
|
||||
@@ -18,7 +25,11 @@ export const BoardMantineProvider = ({ children }: PropsWithChildren) => {
|
||||
autoContrast: true,
|
||||
});
|
||||
|
||||
return <MantineProvider theme={theme}>{children}</MantineProvider>;
|
||||
return (
|
||||
<MantineProvider defaultColorScheme={defaultColorScheme} theme={theme} colorSchemeManager={colorSchemeManager}>
|
||||
{children}
|
||||
</MantineProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const generateColors = (hex: string) => {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { IconHomeOff } from "@tabler/icons-react";
|
||||
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { db } from "@homarr/db";
|
||||
import { boards } from "@homarr/db/schema";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
|
||||
import type { BoardNotFoundProps } from "~/components/board/not-found";
|
||||
import { BoardNotFound } from "~/components/board/not-found";
|
||||
|
||||
export default async function NotFoundBoardHomePage() {
|
||||
const boardNotFoundProps = await getPropsAsync();
|
||||
|
||||
return <BoardNotFound {...boardNotFoundProps} />;
|
||||
}
|
||||
|
||||
const getPropsAsync = async (): Promise<BoardNotFoundProps> => {
|
||||
const boardCount = await db.$count(boards);
|
||||
const t = await getI18n();
|
||||
|
||||
if (boardCount === 0) {
|
||||
return {
|
||||
icon: { src: "/favicon.ico", alt: "Homarr logo" },
|
||||
title: t("board.error.noBoard.title"),
|
||||
description: t("board.error.noBoard.description"),
|
||||
link: { label: t("board.error.noBoard.link"), href: "/manage/boards" },
|
||||
notice: t("board.error.noBoard.notice"),
|
||||
};
|
||||
}
|
||||
|
||||
const session = await auth();
|
||||
const isAdmin = session?.user.permissions.includes("admin");
|
||||
const type = isAdmin ? "admin" : session !== null ? "user" : "anonymous";
|
||||
const href = {
|
||||
admin: "/manage/settings",
|
||||
user: `/manage/users/${session?.user.id}/general`,
|
||||
anonymous: "/manage/boards",
|
||||
}[type];
|
||||
|
||||
return {
|
||||
icon: IconHomeOff,
|
||||
title: t(`board.error.homeBoard.title`),
|
||||
description: t(`board.error.homeBoard.${type}.description`),
|
||||
link: { label: t(`board.error.homeBoard.${type}.link`), href },
|
||||
notice: t(`board.error.homeBoard.${type}.notice`),
|
||||
};
|
||||
};
|
||||
@@ -12,7 +12,7 @@ import { BoardRenameModal } from "~/components/board/modals/board-rename-modal";
|
||||
import { useRequiredBoard } from "../../(content)/_context";
|
||||
import classes from "./danger.module.css";
|
||||
|
||||
export const DangerZoneSettingsContent = () => {
|
||||
export const DangerZoneSettingsContent = ({ hideVisibility }: { hideVisibility: boolean }) => {
|
||||
const board = useRequiredBoard();
|
||||
const t = useScopedI18n("board.setting");
|
||||
const router = useRouter();
|
||||
@@ -90,14 +90,18 @@ export const DangerZoneSettingsContent = () => {
|
||||
buttonText={t("section.dangerZone.action.rename.button")}
|
||||
onClick={onRenameClick}
|
||||
/>
|
||||
<Divider />
|
||||
<DangerZoneRow
|
||||
label={t("section.dangerZone.action.visibility.label")}
|
||||
description={t(`section.dangerZone.action.visibility.description.${visibility}`)}
|
||||
buttonText={t(`section.dangerZone.action.visibility.button.${visibility}`)}
|
||||
onClick={onVisibilityClick}
|
||||
isPending={isChangeVisibilityPending}
|
||||
/>
|
||||
{hideVisibility ? null : (
|
||||
<>
|
||||
<Divider />
|
||||
<DangerZoneRow
|
||||
label={t("section.dangerZone.action.visibility.label")}
|
||||
description={t(`section.dangerZone.action.visibility.description.${visibility}`)}
|
||||
buttonText={t(`section.dangerZone.action.visibility.button.${visibility}`)}
|
||||
onClick={onVisibilityClick}
|
||||
isPending={isChangeVisibilityPending}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Divider />
|
||||
<DangerZoneRow
|
||||
label={t("section.dangerZone.action.delete.label")}
|
||||
|
||||
@@ -14,6 +14,8 @@ import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { capitalize } from "@homarr/common";
|
||||
import { db } from "@homarr/db";
|
||||
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||
import type { TranslationObject } from "@homarr/translation";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
@@ -63,9 +65,14 @@ const getBoardAndPermissionsAsync = async (params: Props["params"]) => {
|
||||
|
||||
export default async function BoardSettingsPage({ params, searchParams }: Props) {
|
||||
const { board, permissions } = await getBoardAndPermissionsAsync(params);
|
||||
const { hasFullAccess } = await getBoardPermissionsAsync(board);
|
||||
const boardSettings = await getServerSettingByKeyAsync(db, "board");
|
||||
const { hasFullAccess, hasChangeAccess } = await getBoardPermissionsAsync(board);
|
||||
const t = await getScopedI18n("board.setting");
|
||||
|
||||
if (!hasChangeAccess) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Stack>
|
||||
@@ -92,7 +99,7 @@ export default async function BoardSettingsPage({ params, searchParams }: Props)
|
||||
<BoardAccessSettings board={board} initialPermissions={permissions} />
|
||||
</AccordionItemFor>
|
||||
<AccordionItemFor value="dangerZone" icon={IconAlertTriangle} danger noPadding>
|
||||
<DangerZoneSettingsContent />
|
||||
<DangerZoneSettingsContent hideVisibility={boardSettings.homeBoardId === board.id} />
|
||||
</AccordionItemFor>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
import type { JSX, PropsWithChildren } from "react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { AppShellMain } from "@mantine/core";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -8,6 +8,7 @@ import { logger } from "@homarr/log";
|
||||
import { MainHeader } from "~/components/layout/header";
|
||||
import { BoardLogoWithTitle } from "~/components/layout/logo/board-logo";
|
||||
import { ClientShell } from "~/components/layout/shell";
|
||||
import { getCurrentColorSchemeAsync } from "~/theme/color-scheme";
|
||||
import type { Board } from "./_types";
|
||||
import { BoardProvider } from "./(content)/_context";
|
||||
import type { Params } from "./(content)/_creator";
|
||||
@@ -37,10 +38,11 @@ export const createBoardLayout = <TParams extends Params>({
|
||||
|
||||
throw error;
|
||||
});
|
||||
const colorScheme = await getCurrentColorSchemeAsync();
|
||||
|
||||
return (
|
||||
<BoardProvider initialBoard={initialBoard}>
|
||||
<BoardMantineProvider>
|
||||
<BoardMantineProvider defaultColorScheme={colorScheme}>
|
||||
<CustomCss />
|
||||
<ClientShell hasNavigation={false}>
|
||||
<MainHeader
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
export const BackToStart = () => {
|
||||
const t = useI18n();
|
||||
const { mutateAsync, isPending } = clientApi.onboard.previousStep.useMutation();
|
||||
|
||||
const handleBackToStartAsync = async () => {
|
||||
await mutateAsync();
|
||||
await revalidatePathActionAsync("/init");
|
||||
};
|
||||
|
||||
return (
|
||||
<Button loading={isPending} variant="subtle" color="gray" fullWidth onClick={handleBackToStartAsync}>
|
||||
{t("init.backToStart")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import Link from "next/link";
|
||||
import type { MantineColor } from "@mantine/core";
|
||||
import { Button, Card, Stack, Text } from "@mantine/core";
|
||||
import { IconBook2, IconCategoryPlus, IconLayoutDashboard, IconMailForward } from "@tabler/icons-react";
|
||||
|
||||
import { isProviderEnabled } from "@homarr/auth/server";
|
||||
import { getMantineColor } from "@homarr/common";
|
||||
import { db } from "@homarr/db";
|
||||
import { createDocumentationLink } from "@homarr/definitions";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
|
||||
export const InitFinish = async () => {
|
||||
const firstBoard = await db.query.boards.findFirst({ columns: { name: true } });
|
||||
const tFinish = await getScopedI18n("init.step.finish");
|
||||
|
||||
return (
|
||||
<Card w={64 * 6} maw="90vw" withBorder>
|
||||
<Stack>
|
||||
<Text>{tFinish("description")}</Text>
|
||||
|
||||
{firstBoard ? (
|
||||
<InternalLinkButton
|
||||
href={`/auth/login?callbackUrl=/boards/${firstBoard.name}`}
|
||||
iconProps={{ icon: IconLayoutDashboard, color: "blue" }}
|
||||
>
|
||||
{tFinish("action.goToBoard", { name: firstBoard.name })}
|
||||
</InternalLinkButton>
|
||||
) : (
|
||||
<InternalLinkButton
|
||||
href="/auth/login?callbackUrl=/manage/boards"
|
||||
iconProps={{ icon: IconCategoryPlus, color: "blue" }}
|
||||
>
|
||||
{tFinish("action.createBoard")}
|
||||
</InternalLinkButton>
|
||||
)}
|
||||
|
||||
{isProviderEnabled("credentials") && (
|
||||
<InternalLinkButton
|
||||
href="/auth/login?callbackUrl=/manage/users/invites"
|
||||
iconProps={{ icon: IconMailForward, color: "pink" }}
|
||||
>
|
||||
{tFinish("action.inviteUser")}
|
||||
</InternalLinkButton>
|
||||
)}
|
||||
|
||||
<ExternalLinkButton
|
||||
href={createDocumentationLink("/docs/getting-started/after-the-installation")}
|
||||
iconProps={{ icon: IconBook2, color: "yellow" }}
|
||||
>
|
||||
{tFinish("action.docs")}
|
||||
</ExternalLinkButton>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
interface LinkButtonProps {
|
||||
href: string;
|
||||
children: string;
|
||||
iconProps: IconProps;
|
||||
}
|
||||
|
||||
interface IconProps {
|
||||
icon: TablerIcon;
|
||||
color: MantineColor;
|
||||
}
|
||||
|
||||
const Icon = ({ icon: IcomComponent, color }: IconProps) => {
|
||||
return <IcomComponent color={getMantineColor(color, 6)} size={16} stroke={1.5} />;
|
||||
};
|
||||
|
||||
const InternalLinkButton = ({ href, children, iconProps }: LinkButtonProps) => {
|
||||
return (
|
||||
<Button variant="default" component={Link} href={href} leftSection={<Icon {...iconProps} />}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const ExternalLinkButton = ({ href, children, iconProps }: LinkButtonProps) => {
|
||||
return (
|
||||
<Button variant="default" component="a" href={href} leftSection={<Icon {...iconProps} />}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Card, Stack, TextInput } from "@mantine/core";
|
||||
import { IconArrowRight } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { z } from "@homarr/validation";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
export const InitGroup = () => {
|
||||
const t = useI18n();
|
||||
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation();
|
||||
const form = useZodForm(validation.group.create, {
|
||||
initialValues: {
|
||||
name: "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmitAsync = async (values: z.infer<typeof validation.group.create>) => {
|
||||
await mutateAsync(values, {
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/init");
|
||||
},
|
||||
onError(error) {
|
||||
if (error.data?.code === "CONFLICT") {
|
||||
form.setErrors({ name: t("common.zod.errors.custom.groupNameTaken") });
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card w={64 * 6} maw="90vw" withBorder>
|
||||
<form onSubmit={form.onSubmit(handleSubmitAsync)}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label={t("init.step.group.form.name.label")}
|
||||
description={t("init.step.group.form.name.description")}
|
||||
withAsterisk
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<Button type="submit" loading={form.submitting} rightSection={<IconArrowRight size={16} stroke={1.5} />}>
|
||||
{t("common.action.continue")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ActionIcon, Button, Card, Group, Text } from "@mantine/core";
|
||||
import type { FileWithPath } from "@mantine/dropzone";
|
||||
import { IconPencil } from "@tabler/icons-react";
|
||||
|
||||
import { humanFileSize } from "@homarr/common";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
interface FileInfoCardProps {
|
||||
file: FileWithPath;
|
||||
onRemove: () => void;
|
||||
}
|
||||
|
||||
export const FileInfoCard = ({ file, onRemove }: FileInfoCardProps) => {
|
||||
const tFileInfo = useScopedI18n("init.step.import.fileInfo");
|
||||
return (
|
||||
<Card w={64 * 12 + 8} maw="90vw">
|
||||
<Group justify="space-between" align="center" wrap="nowrap">
|
||||
<Group>
|
||||
<Text fw={500} lineClamp={1} style={{ wordBreak: "break-all" }}>
|
||||
{file.name}
|
||||
</Text>
|
||||
<Text visibleFrom="md" c="gray.6" size="sm">
|
||||
{humanFileSize(file.size)}
|
||||
</Text>
|
||||
</Group>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
rightSection={<IconPencil size={16} stroke={1.5} />}
|
||||
onClick={onRemove}
|
||||
visibleFrom="md"
|
||||
>
|
||||
{tFileInfo("action.change")}
|
||||
</Button>
|
||||
<ActionIcon size="sm" variant="subtle" color="gray" hiddenFrom="md" onClick={onRemove}>
|
||||
<IconPencil size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Group, rem, Text } from "@mantine/core";
|
||||
import type { FileWithPath } from "@mantine/dropzone";
|
||||
import { Dropzone, MIME_TYPES } from "@mantine/dropzone";
|
||||
import { IconFileZip, IconUpload, IconX } from "@tabler/icons-react";
|
||||
|
||||
import "@mantine/dropzone/styles.css";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
interface ImportDropZoneProps {
|
||||
loading: boolean;
|
||||
updateFile: (file: FileWithPath) => void;
|
||||
}
|
||||
|
||||
export const ImportDropZone = ({ loading, updateFile }: ImportDropZoneProps) => {
|
||||
const tDropzone = useScopedI18n("init.step.import.dropzone");
|
||||
return (
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const firstFile = files[0];
|
||||
if (!firstFile) return;
|
||||
|
||||
updateFile(firstFile);
|
||||
}}
|
||||
acceptColor="blue.6"
|
||||
rejectColor="red.6"
|
||||
accept={[MIME_TYPES.zip, "application/x-zip-compressed"]}
|
||||
loading={loading}
|
||||
multiple={false}
|
||||
maxSize={1024 * 1024 * 1024 * 64} // 64 MB
|
||||
onReject={(rejections) => {
|
||||
console.error(
|
||||
"Rejected files",
|
||||
rejections.map(
|
||||
(rejection) =>
|
||||
`File: ${rejection.file.name} size=${rejection.file.size} fileType=${rejection.file.type}\n - ${rejection.errors.map((error) => error.message).join("\n - ")}`,
|
||||
),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: "none" }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload style={{ width: rem(52), height: rem(52), color: "var(--mantine-color-blue-6)" }} stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX style={{ width: rem(52), height: rem(52), color: "var(--mantine-color-red-6)" }} stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconFileZip style={{ width: rem(52), height: rem(52), color: "var(--mantine-color-dimmed)" }} stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
{tDropzone("title")}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
{tDropzone("description")}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { startTransition, useState } from "react";
|
||||
import { Card, Stack } from "@mantine/core";
|
||||
import type { FileWithPath } from "@mantine/dropzone";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { InitialOldmarrImport } from "@homarr/old-import/components";
|
||||
|
||||
import { FileInfoCard } from "./file-info-card";
|
||||
import { ImportDropZone } from "./import-dropzone";
|
||||
|
||||
export const InitImport = () => {
|
||||
const [file, setFile] = useState<FileWithPath | null>(null);
|
||||
const { isPending, mutate } = clientApi.import.analyseInitialOldmarrImport.useMutation();
|
||||
const [analyseResult, setAnalyseResult] = useState<RouterOutputs["import"]["analyseInitialOldmarrImport"] | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
if (!file) {
|
||||
return (
|
||||
<Card w={64 * 12 + 8} maw="90vw" withBorder>
|
||||
<ImportDropZone
|
||||
loading={isPending}
|
||||
updateFile={(file) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
mutate(formData, {
|
||||
onSuccess: (result) => {
|
||||
startTransition(() => {
|
||||
setAnalyseResult(result);
|
||||
setFile(file);
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack mb="sm">
|
||||
<FileInfoCard file={file} onRemove={() => setFile(null)} />
|
||||
{analyseResult !== null && <InitialOldmarrImport file={file} analyseResult={analyseResult} />}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
|
||||
import { startTransition } from "react";
|
||||
import { Button, Card, Group, Stack, Switch, Text } from "@mantine/core";
|
||||
import { IconArrowRight } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import type { CheckboxProps } from "@homarr/form/types";
|
||||
import { defaultServerSettings } from "@homarr/server-settings";
|
||||
import type { TranslationObject } from "@homarr/translation";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import type { z } from "@homarr/validation";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
export const InitSettings = () => {
|
||||
const tSection = useScopedI18n("management.page.settings.section");
|
||||
const t = useI18n();
|
||||
const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation();
|
||||
const form = useZodForm(validation.settings.init, { initialValues: defaultServerSettings });
|
||||
|
||||
form.watch("analytics.enableGeneral", ({ value }) => {
|
||||
if (!value) {
|
||||
startTransition(() => {
|
||||
form.setFieldValue("analytics.enableWidgetData", false);
|
||||
form.setFieldValue("analytics.enableIntegrationData", false);
|
||||
form.setFieldValue("analytics.enableUserData", false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmitAsync = async (values: z.infer<typeof validation.settings.init>) => {
|
||||
await mutateAsync(values, {
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/init");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmitAsync)}>
|
||||
<Stack>
|
||||
<Card w={64 * 12 + 8} maw="90vw" withBorder>
|
||||
<Stack gap="sm">
|
||||
<Text fw={500}>{tSection("analytics.title")}</Text>
|
||||
|
||||
<Stack gap="xs">
|
||||
<AnalyticsRow kind="general" {...form.getInputProps("analytics.enableGeneral", { type: "checkbox" })} />
|
||||
|
||||
<Stack gap="xs" ps="md" w="100%">
|
||||
<AnalyticsRow
|
||||
kind="integrationData"
|
||||
disabled={!form.values.analytics.enableGeneral}
|
||||
{...form.getInputProps("analytics.enableWidgetData", { type: "checkbox" })}
|
||||
/>
|
||||
<AnalyticsRow
|
||||
kind="widgetData"
|
||||
disabled={!form.values.analytics.enableGeneral}
|
||||
{...form.getInputProps("analytics.enableIntegrationData", { type: "checkbox" })}
|
||||
/>
|
||||
<AnalyticsRow
|
||||
kind="usersData"
|
||||
disabled={!form.values.analytics.enableGeneral}
|
||||
{...form.getInputProps("analytics.enableUserData", { type: "checkbox" })}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
<Card w={64 * 12 + 8} maw="90vw" withBorder>
|
||||
<Stack gap="sm">
|
||||
<Text fw={500}>{tSection("crawlingAndIndexing.title")}</Text>
|
||||
|
||||
<Stack gap="xs">
|
||||
<CrawlingRow
|
||||
kind="noIndex"
|
||||
{...form.getInputProps("crawlingAndIndexing.noIndex", { type: "checkbox" })}
|
||||
/>
|
||||
<CrawlingRow
|
||||
kind="noFollow"
|
||||
{...form.getInputProps("crawlingAndIndexing.noFollow", { type: "checkbox" })}
|
||||
/>
|
||||
<CrawlingRow
|
||||
kind="noTranslate"
|
||||
{...form.getInputProps("crawlingAndIndexing.noTranslate", { type: "checkbox" })}
|
||||
/>
|
||||
<CrawlingRow
|
||||
kind="noSiteLinksSearchBox"
|
||||
{...form.getInputProps("crawlingAndIndexing.noSiteLinksSearchBox", { type: "checkbox" })}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Button type="submit" loading={form.submitting} rightSection={<IconArrowRight size={16} stroke={1.5} />}>
|
||||
{t("common.action.continue")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
interface AnalyticsRowProps {
|
||||
kind: Exclude<keyof TranslationObject["management"]["page"]["settings"]["section"]["analytics"], "title">;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const AnalyticsRow = ({ kind, ...props }: AnalyticsRowProps & CheckboxProps) => {
|
||||
const tSection = useI18n("management.page.settings.section");
|
||||
|
||||
return (
|
||||
<SettingRow title={tSection(`analytics.${kind}.title`)} text={tSection(`analytics.${kind}.text`)} {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
interface CrawlingRowProps {
|
||||
kind: Exclude<
|
||||
keyof TranslationObject["management"]["page"]["settings"]["section"]["crawlingAndIndexing"],
|
||||
"title" | "warning"
|
||||
>;
|
||||
}
|
||||
|
||||
const CrawlingRow = ({ kind, ...inputProps }: CrawlingRowProps & CheckboxProps) => {
|
||||
const tSection = useI18n("management.page.settings.section");
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
title={tSection(`crawlingAndIndexing.${kind}.title`)}
|
||||
text={tSection(`crawlingAndIndexing.${kind}.text`)}
|
||||
{...inputProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const SettingRow = ({
|
||||
title,
|
||||
text,
|
||||
disabled,
|
||||
...inputProps
|
||||
}: { title: string; text: string; disabled?: boolean } & CheckboxProps) => {
|
||||
return (
|
||||
<Group wrap="nowrap" align="center">
|
||||
<Stack gap={0} style={{ flex: 1 }}>
|
||||
<Text size="sm" fw={500}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text size="xs" c="gray.5">
|
||||
{text}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Switch disabled={disabled} {...inputProps} />
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Card, Stack, Text } from "@mantine/core";
|
||||
import { IconFileImport, IconPlayerPlay } from "@tabler/icons-react";
|
||||
|
||||
import { getMantineColor } from "@homarr/common";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { InitStartButton } from "./next-button";
|
||||
|
||||
export const InitStart = async () => {
|
||||
const tStart = await getScopedI18n("init.step.start");
|
||||
|
||||
return (
|
||||
<Card w={64 * 6} maw="90vw" withBorder>
|
||||
<Stack>
|
||||
<Text>{tStart("description")}</Text>
|
||||
|
||||
<InitStartButton
|
||||
preferredStep={undefined}
|
||||
icon={<IconPlayerPlay color={getMantineColor("green", 6)} size={16} stroke={1.5} />}
|
||||
>
|
||||
{tStart("action.scratch")}
|
||||
</InitStartButton>
|
||||
<InitStartButton
|
||||
preferredStep="import"
|
||||
icon={<IconFileImport color={getMantineColor("cyan", 6)} size={16} stroke={1.5} />}
|
||||
>
|
||||
{tStart("action.importOldmarr")}
|
||||
</InitStartButton>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import type { PropsWithChildren, ReactNode } from "react";
|
||||
import { Button } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import type { OnboardingStep } from "@homarr/definitions";
|
||||
|
||||
interface InitStartButtonProps {
|
||||
icon: ReactNode;
|
||||
preferredStep: OnboardingStep | undefined;
|
||||
}
|
||||
|
||||
export const InitStartButton = ({ preferredStep, icon, children }: PropsWithChildren<InitStartButtonProps>) => {
|
||||
const { mutateAsync } = clientApi.onboard.nextStep.useMutation();
|
||||
|
||||
const handleClickAsync = async () => {
|
||||
await mutateAsync({ preferredStep });
|
||||
await revalidatePathActionAsync("/init");
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleClickAsync} variant="default" leftSection={icon}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
+10
-10
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
@@ -12,9 +12,9 @@ import type { z } from "@homarr/validation";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
export const InitUserForm = () => {
|
||||
const router = useRouter();
|
||||
const t = useScopedI18n("user");
|
||||
const { mutateAsync, error, isPending } = clientApi.user.initUser.useMutation();
|
||||
const tUser = useScopedI18n("init.step.user");
|
||||
const { mutateAsync, isPending } = clientApi.user.initUser.useMutation();
|
||||
const form = useZodForm(validation.user.init, {
|
||||
initialValues: {
|
||||
username: "",
|
||||
@@ -25,17 +25,17 @@ export const InitUserForm = () => {
|
||||
|
||||
const handleSubmitAsync = async (values: FormType) => {
|
||||
await mutateAsync(values, {
|
||||
onSuccess: () => {
|
||||
async onSuccess() {
|
||||
showSuccessNotification({
|
||||
title: "User created",
|
||||
message: "You can now log in",
|
||||
title: tUser("notification.success.title"),
|
||||
message: tUser("notification.success.message"),
|
||||
});
|
||||
router.push("/auth/login");
|
||||
await revalidatePathActionAsync("/init");
|
||||
},
|
||||
onError: () => {
|
||||
onError: (error) => {
|
||||
showErrorNotification({
|
||||
title: "User creation failed",
|
||||
message: error?.message ?? "Unknown error",
|
||||
title: tUser("notification.error.title"),
|
||||
message: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Card } from "@mantine/core";
|
||||
|
||||
import { InitUserForm } from "./init-user-form";
|
||||
|
||||
export const InitUser = () => {
|
||||
return (
|
||||
<Card w={64 * 6} maw="90vw" withBorder>
|
||||
<InitUserForm />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { JSX } from "react";
|
||||
import { Box, Center, Stack, Text, Title } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import type { MaybePromise } from "@homarr/common/types";
|
||||
import type { OnboardingStep } from "@homarr/definitions";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { CurrentColorSchemeCombobox } from "~/components/color-scheme/current-color-scheme-combobox";
|
||||
import { CurrentLanguageCombobox } from "~/components/language/current-language-combobox";
|
||||
import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo";
|
||||
import { BackToStart } from "./_steps/back";
|
||||
import { InitFinish } from "./_steps/finish/init-finish";
|
||||
import { InitGroup } from "./_steps/group/init-group";
|
||||
import { InitImport } from "./_steps/import/init-import";
|
||||
import { InitSettings } from "./_steps/settings/init-settings";
|
||||
import { InitStart } from "./_steps/start/init-start";
|
||||
import { InitUser } from "./_steps/user/init-user";
|
||||
|
||||
const stepComponents: Record<OnboardingStep, null | (() => MaybePromise<JSX.Element>)> = {
|
||||
start: InitStart,
|
||||
import: InitImport,
|
||||
user: InitUser,
|
||||
group: InitGroup,
|
||||
settings: InitSettings,
|
||||
finish: InitFinish,
|
||||
};
|
||||
|
||||
export default async function InitPage() {
|
||||
const t = await getScopedI18n("init.step");
|
||||
const currentStep = await api.onboard.currentStep();
|
||||
|
||||
const CurrentComponent = stepComponents[currentStep.current];
|
||||
|
||||
return (
|
||||
<Box mih="100dvh">
|
||||
<Center>
|
||||
<Stack align="center" mt="xl">
|
||||
<HomarrLogoWithTitle size="lg" />
|
||||
<Stack gap={6} align="center">
|
||||
<Title order={3} fw={400} ta="center">
|
||||
{t(`${currentStep.current}.title`)}
|
||||
</Title>
|
||||
<Text size="sm" c="gray.5" ta="center">
|
||||
{t(`${currentStep.current}.subtitle`)}
|
||||
</Text>
|
||||
</Stack>
|
||||
<CurrentLanguageCombobox width="100%" />
|
||||
<CurrentColorSchemeCombobox w="100%" />
|
||||
{CurrentComponent && <CurrentComponent />}
|
||||
{currentStep.previous === "start" && <BackToStart />}
|
||||
</Stack>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Card, Center, Stack, Text, Title } from "@mantine/core";
|
||||
|
||||
import { db } from "@homarr/db";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo";
|
||||
import { InitUserForm } from "./_init-user-form";
|
||||
|
||||
export default async function InitUser() {
|
||||
const firstUser = await db.query.users.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (firstUser) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const t = await getScopedI18n("user.page.init");
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<Stack align="center" mt="xl">
|
||||
<HomarrLogoWithTitle size="lg" />
|
||||
<Stack gap={6} align="center">
|
||||
<Title order={3} fw={400} ta="center">
|
||||
{t("title")}
|
||||
</Title>
|
||||
<Text size="sm" c="gray.5" ta="center">
|
||||
{t("subtitle")}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Card bg="dark.8" w={64 * 6} maw="90vw">
|
||||
<InitUserForm />
|
||||
</Card>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
@@ -6,19 +6,23 @@ import "@homarr/spotlight/styles.css";
|
||||
import "@homarr/ui/styles.css";
|
||||
import "~/styles/scroll-area.scss";
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
import { notFound } from "next/navigation";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
|
||||
import { env } from "@homarr/auth/env.mjs";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { ModalProvider } from "@homarr/modals";
|
||||
import { Notifications } from "@homarr/notifications";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
import { SpotlightProvider } from "@homarr/spotlight";
|
||||
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
|
||||
import { getI18nMessages } from "@homarr/translation/server";
|
||||
|
||||
import { Analytics } from "~/components/layout/analytics";
|
||||
import { SearchEngineOptimization } from "~/components/layout/search-engine-optimization";
|
||||
import { getCurrentColorSchemeAsync } from "~/theme/color-scheme";
|
||||
import { DayJsLoader } from "./_client-providers/dayjs-loader";
|
||||
import { JotaiProvider } from "./_client-providers/jotai";
|
||||
import { CustomMantineProvider } from "./_client-providers/mantine";
|
||||
import { NextInternationalProvider } from "./_client-providers/next-international";
|
||||
import { AuthProvider } from "./_client-providers/session";
|
||||
import { TRPCReactProvider } from "./_client-providers/trpc";
|
||||
import { composeWrappers } from "./compose";
|
||||
@@ -28,7 +32,8 @@ const fontSans = Inter({
|
||||
variable: "--font-sans",
|
||||
});
|
||||
|
||||
export const generateMetadata = (): Metadata => ({
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
export const generateMetadata = async (): Promise<Metadata> => ({
|
||||
title: "Homarr",
|
||||
description:
|
||||
"Simplify the management of your server with Homarr - a sleek, modern dashboard that puts all of your apps and services at your fingertips.",
|
||||
@@ -47,7 +52,7 @@ export const generateMetadata = (): Metadata => ({
|
||||
title: "Homarr",
|
||||
capable: true,
|
||||
startupImage: { url: "/logo/logo.png" },
|
||||
statusBarStyle: getColorScheme() === "dark" ? "black-translucent" : "default",
|
||||
statusBarStyle: (await getCurrentColorSchemeAsync()) === "dark" ? "black-translucent" : "default",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -59,10 +64,14 @@ export const viewport: Viewport = {
|
||||
};
|
||||
|
||||
export default async function Layout(props: { children: React.ReactNode; params: { locale: string } }) {
|
||||
if (!isLocaleSupported(props.params.locale)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const session = await auth();
|
||||
const colorScheme = getColorScheme();
|
||||
const tCommon = await getScopedI18n("common");
|
||||
const direction = tCommon("direction");
|
||||
const colorScheme = await getCurrentColorSchemeAsync();
|
||||
const direction = isLocaleRTL(props.params.locale) ? "rtl" : "ltr";
|
||||
const i18nMessages = await getI18nMessages();
|
||||
|
||||
const StackedProvider = composeWrappers([
|
||||
(innerProps) => {
|
||||
@@ -70,15 +79,17 @@ export default async function Layout(props: { children: React.ReactNode; params:
|
||||
},
|
||||
(innerProps) => <JotaiProvider {...innerProps} />,
|
||||
(innerProps) => <TRPCReactProvider {...innerProps} />,
|
||||
(innerProps) => <NextInternationalProvider {...innerProps} locale={props.params.locale} />,
|
||||
(innerProps) => <CustomMantineProvider {...innerProps} />,
|
||||
(innerProps) => <DayJsLoader {...innerProps} />,
|
||||
(innerProps) => <NextIntlClientProvider {...innerProps} messages={i18nMessages} />,
|
||||
(innerProps) => <CustomMantineProvider {...innerProps} defaultColorScheme={colorScheme} />,
|
||||
(innerProps) => <ModalProvider {...innerProps} />,
|
||||
(innerProps) => <SpotlightProvider {...innerProps} />,
|
||||
]);
|
||||
|
||||
return (
|
||||
// Instead of ColorSchemScript we use data-mantine-color-scheme to prevent flickering
|
||||
<html
|
||||
lang="en"
|
||||
lang={props.params.locale}
|
||||
dir={direction}
|
||||
data-mantine-color-scheme={colorScheme}
|
||||
style={{
|
||||
@@ -99,7 +110,3 @@ export default async function Layout(props: { children: React.ReactNode; params:
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
const getColorScheme = () => {
|
||||
return cookies().get("homarr-color-scheme")?.value ?? "dark";
|
||||
};
|
||||
@@ -1,12 +1,22 @@
|
||||
.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;
|
||||
@mixin dark {
|
||||
background: linear-gradient(
|
||||
130deg,
|
||||
#fa52521f 0%,
|
||||
var(--mantine-color-dark-6) 35%,
|
||||
var(--mantine-color-dark-6) 100%
|
||||
) !important;
|
||||
}
|
||||
@mixin light {
|
||||
background: linear-gradient(
|
||||
130deg,
|
||||
#fa52521f 0%,
|
||||
var(--mantine-color-gray-3) 35%,
|
||||
var(--mantine-color-gray-3) 100%
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollContainer {
|
||||
|
||||
@@ -32,9 +32,9 @@ const icons = [
|
||||
|
||||
const countIconGroups = 3;
|
||||
const animationDurationInSeconds = 12;
|
||||
const arrayInChunks = splitToNChunks(icons, countIconGroups);
|
||||
|
||||
export const HeroBanner = () => {
|
||||
const arrayInChunks = splitToNChunks(icons, countIconGroups);
|
||||
const gridSpan = 12 / countIconGroups;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Container, Stack, Title } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
@@ -11,6 +13,11 @@ interface AppEditPageProps {
|
||||
}
|
||||
|
||||
export default async function AppEditPage({ params }: AppEditPageProps) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user.permissions.includes("app-modify-all")) {
|
||||
notFound();
|
||||
}
|
||||
const app = await api.app.byId({ id: params.id });
|
||||
const t = await getI18n();
|
||||
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Container, Stack, Title } from "@mantine/core";
|
||||
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { AppNewForm } from "./_app-new-form";
|
||||
|
||||
export default async function AppNewPage() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user.permissions.includes("app-create")) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const t = await getI18n();
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconApps, IconPencil } from "@tabler/icons-react";
|
||||
import { IconBox, IconPencil } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { api } from "@homarr/api/server";
|
||||
import { parseAppHrefWithVariablesServer } from "@homarr/common/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
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 { NoResults } from "~/components/no-results";
|
||||
import { AppDeleteButton } from "./_app-delete-button";
|
||||
|
||||
export default async function AppsPage() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session) {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
||||
const apps = await api.app.all();
|
||||
const t = await getScopedI18n("app");
|
||||
|
||||
@@ -22,9 +30,11 @@ export default async function AppsPage() {
|
||||
<Stack>
|
||||
<Group justify="space-between" align="center">
|
||||
<Title>{t("page.list.title")}</Title>
|
||||
<MobileAffixButton component={Link} href="/manage/apps/new">
|
||||
{t("page.create.title")}
|
||||
</MobileAffixButton>
|
||||
{session.user.permissions.includes("app-create") && (
|
||||
<MobileAffixButton component={Link} href="/manage/apps/new">
|
||||
{t("page.create.title")}
|
||||
</MobileAffixButton>
|
||||
)}
|
||||
</Group>
|
||||
{apps.length === 0 && <AppNoResults />}
|
||||
{apps.length > 0 && (
|
||||
@@ -45,9 +55,10 @@ interface AppCardProps {
|
||||
|
||||
const AppCard = async ({ app }: AppCardProps) => {
|
||||
const t = await getScopedI18n("app");
|
||||
const session = await auth();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card withBorder>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group align="top" justify="start" wrap="nowrap">
|
||||
<Avatar
|
||||
@@ -70,24 +81,26 @@ const AppCard = async ({ app }: AppCardProps) => {
|
||||
</Text>
|
||||
)}
|
||||
{app.href && (
|
||||
<Anchor href={parseAppHrefWithVariablesServer(app.href)} lineClamp={1} size="sm" w="min-content">
|
||||
{parseAppHrefWithVariablesServer(app.href)}
|
||||
<Anchor href={app.href} lineClamp={1} size="sm" w="min-content">
|
||||
{app.href}
|
||||
</Anchor>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
<Group>
|
||||
<ActionIconGroup>
|
||||
<ActionIcon
|
||||
component={Link}
|
||||
href={`/manage/apps/edit/${app.id}`}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label={t("page.edit.title")}
|
||||
>
|
||||
<IconPencil size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
<AppDeleteButton app={app} />
|
||||
{session?.user.permissions.includes("app-modify-all") && (
|
||||
<ActionIcon
|
||||
component={Link}
|
||||
href={`/manage/apps/edit/${app.id}`}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label={t("page.edit.title")}
|
||||
>
|
||||
<IconPencil size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
{session?.user.permissions.includes("app-full-all") && <AppDeleteButton app={app} />}
|
||||
</ActionIconGroup>
|
||||
</Group>
|
||||
</Group>
|
||||
@@ -97,16 +110,17 @@ const AppCard = async ({ app }: AppCardProps) => {
|
||||
|
||||
const AppNoResults = async () => {
|
||||
const t = await getI18n();
|
||||
const session = await auth();
|
||||
|
||||
return (
|
||||
<Card withBorder bg="transparent">
|
||||
<Stack align="center" gap="sm">
|
||||
<IconApps size="2rem" />
|
||||
<Text fw={500} size="lg">
|
||||
{t("app.page.list.noResults.title")}
|
||||
</Text>
|
||||
<Anchor href="/manage/apps/new">{t("app.page.list.noResults.action")}</Anchor>
|
||||
</Stack>
|
||||
</Card>
|
||||
<NoResults
|
||||
icon={IconBox}
|
||||
title={t("app.page.list.noResults.title")}
|
||||
action={{
|
||||
label: t("app.page.list.noResults.action"),
|
||||
href: "/manage/apps/new",
|
||||
hidden: !session?.user.permissions.includes("app-create"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,12 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { Affix, Button, Group, Menu } from "@mantine/core";
|
||||
import { Affix, Button, Menu } from "@mantine/core";
|
||||
import { IconCategoryPlus, IconChevronDown, IconFileImport } from "@tabler/icons-react";
|
||||
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { AddBoardModal, ImportBoardModal } from "@homarr/modals-collection";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { BetaBadge } from "@homarr/ui";
|
||||
|
||||
export const CreateBoardButton = () => {
|
||||
const t = useI18n();
|
||||
@@ -26,10 +25,7 @@ export const CreateBoardButton = () => {
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item onClick={openImportModal} leftSection={<IconFileImport size="1rem" />}>
|
||||
<Group>
|
||||
{t("board.action.oldImport.label")}
|
||||
<BetaBadge size="xs" />
|
||||
</Group>
|
||||
{t("board.action.oldImport.label")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@@ -67,7 +67,7 @@ const BoardCard = async ({ board }: BoardCardProps) => {
|
||||
const VisibilityIcon = board.isPublic ? IconWorld : IconLock;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card withBorder>
|
||||
<CardSection p="sm" withBorder>
|
||||
<Group justify="space-between" align="center">
|
||||
<Group gap="sm">
|
||||
|
||||
+19
-10
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { ActionIcon, Avatar, Button, Card, Collapse, Group, Kbd, Stack, Text } from "@mantine/core";
|
||||
import { ActionIcon, Avatar, Badge, Button, Card, Collapse, Group, Kbd, Stack, Text, Tooltip } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { IconEye, IconEyeOff } from "@tabler/icons-react";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||
import { integrationSecretKindObject } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
@@ -17,13 +17,14 @@ import { integrationSecretIcons } from "./integration-secret-icons";
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
interface SecretCardProps {
|
||||
secret: RouterOutputs["integration"]["byId"]["secrets"][number];
|
||||
secret:
|
||||
| RouterOutputs["integration"]["byId"]["secrets"][number]
|
||||
| { kind: IntegrationSecretKind; value: null; updatedAt: null };
|
||||
children: React.ReactNode;
|
||||
onCancel: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => {
|
||||
const params = useParams<{ locale: string }>();
|
||||
const t = useI18n();
|
||||
const { isPublic } = integrationSecretKindObject[secret.kind];
|
||||
const [publicSecretDisplayOpened, { toggle: togglePublicSecretDisplay }] = useDisclosure(false);
|
||||
@@ -32,7 +33,7 @@ export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => {
|
||||
const KindIcon = integrationSecretIcons[secret.kind];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card withBorder>
|
||||
<Stack>
|
||||
<Group justify="space-between">
|
||||
<Group>
|
||||
@@ -43,11 +44,19 @@ export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => {
|
||||
{publicSecretDisplayOpened ? <Kbd>{secret.value}</Kbd> : null}
|
||||
</Group>
|
||||
<Group>
|
||||
<Text c="gray.6" size="sm">
|
||||
{t("integration.secrets.lastUpdated", {
|
||||
date: dayjs().locale(params.locale).to(dayjs(secret.updatedAt)),
|
||||
})}
|
||||
</Text>
|
||||
{secret.updatedAt ? (
|
||||
<Text c="gray.6" size="sm">
|
||||
{t("integration.secrets.lastUpdated", {
|
||||
date: dayjs().to(dayjs(secret.updatedAt)),
|
||||
})}
|
||||
</Text>
|
||||
) : (
|
||||
<Tooltip label={t("integration.secrets.notSet.tooltip")} position="left">
|
||||
<Badge color="orange" variant="light" size="sm">
|
||||
{t("integration.secrets.notSet.label")}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isPublic ? (
|
||||
<ActionIcon color="gray" variant="subtle" onClick={togglePublicSecretDisplay}>
|
||||
<DisplayIcon size={16} stroke={1.5} />
|
||||
|
||||
+1
-2
@@ -98,8 +98,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
{secretsKinds.map((kind, index) => (
|
||||
<SecretCard
|
||||
key={kind}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
secret={secretsMap.get(kind)!}
|
||||
secret={secretsMap.get(kind) ?? { kind, value: null, updatedAt: null }}
|
||||
onCancel={() =>
|
||||
new Promise((resolve) => {
|
||||
// When nothing changed, just close the secret card
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
||||
import { IntegrationAvatar } from "@homarr/ui";
|
||||
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { catchTrpcNotFound } from "~/errors/trpc-not-found";
|
||||
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
|
||||
import { IntegrationAccessSettings } from "../../_components/integration-access-settings";
|
||||
import { EditIntegrationForm } from "./_integration-edit-form";
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import { useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Alert, Button, Fieldset, Group, SegmentedControl, Stack, Text, TextInput } from "@mantine/core";
|
||||
import { Alert, Button, Checkbox, Fieldset, Group, SegmentedControl, Stack, Text, TextInput } from "@mantine/core";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||
import { getAllSecretKindOptions, getIntegrationName } from "@homarr/definitions";
|
||||
import { getAllSecretKindOptions, getIntegrationName, integrationDefs } from "@homarr/definitions";
|
||||
import type { UseFormReturnType } from "@homarr/form";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
|
||||
@@ -38,6 +38,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
kind,
|
||||
value: "",
|
||||
})),
|
||||
attemptSearchEngineCreation: true,
|
||||
},
|
||||
});
|
||||
const { mutateAsync, isPending } = clientApi.integration.create.useMutation();
|
||||
@@ -78,6 +79,8 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
);
|
||||
};
|
||||
|
||||
const supportsSearchEngine = integrationDefs[searchParams.kind].category.flat().includes("search");
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit((value) => void handleSubmitAsync(value))}>
|
||||
<Stack>
|
||||
@@ -104,6 +107,16 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
|
||||
{supportsSearchEngine && (
|
||||
<Checkbox
|
||||
label={t("integration.field.attemptSearchEngineCreation.label")}
|
||||
description={t("integration.field.attemptSearchEngineCreation.description", {
|
||||
kind: getIntegrationName(searchParams.kind),
|
||||
})}
|
||||
{...form.getInputProps("attemptSearchEngineCreation", { type: "checkbox" })}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Group justify="end" align="center">
|
||||
<Button variant="default" component={Link} href="/manage/integrations">
|
||||
{t("common.action.backToOverview")}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Fragment } from "react";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import {
|
||||
AccordionControl,
|
||||
AccordionItem,
|
||||
@@ -26,7 +27,7 @@ import {
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { IconChevronDown, IconChevronUp, IconPencil } from "@tabler/icons-react";
|
||||
import { IconChevronDown, IconChevronUp, IconPencil, IconPlugX } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { api } from "@homarr/api/server";
|
||||
@@ -39,6 +40,7 @@ import { CountBadge, IntegrationAvatar } from "@homarr/ui";
|
||||
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { NoResults } from "~/components/no-results";
|
||||
import { ActiveTabAccordion } from "../../../../components/active-tab-accordion";
|
||||
import { DeleteIntegrationActionButton } from "./_integration-buttons";
|
||||
import { IntegrationCreateDropdownContent } from "./new/_integration-new-dropdown";
|
||||
@@ -50,11 +52,16 @@ interface IntegrationsPageProps {
|
||||
}
|
||||
|
||||
export default async function IntegrationsPage({ searchParams }: IntegrationsPageProps) {
|
||||
const integrations = await api.integration.all();
|
||||
const session = await auth();
|
||||
|
||||
if (!session) {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
||||
const integrations = await api.integration.all();
|
||||
const t = await getScopedI18n("integration");
|
||||
|
||||
const canCreateIntegrations = session?.user.permissions.includes("integration-create") ?? false;
|
||||
const canCreateIntegrations = session.user.permissions.includes("integration-create");
|
||||
|
||||
return (
|
||||
<ManageContainer>
|
||||
@@ -114,7 +121,7 @@ const IntegrationList = async ({ integrations, activeTab }: IntegrationListProps
|
||||
const hasFullAccess = session?.user.permissions.includes("integration-full-all") ?? false;
|
||||
|
||||
if (integrations.length === 0) {
|
||||
return <div>{t("page.list.empty")}</div>;
|
||||
return <NoResults icon={IconPlugX} title={t("page.list.noResults.title")} />;
|
||||
}
|
||||
|
||||
const grouppedIntegrations = integrations.reduce(
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
IconLayoutDashboard,
|
||||
IconLogs,
|
||||
IconMailForward,
|
||||
IconPhoto,
|
||||
IconPlug,
|
||||
IconQuestionMark,
|
||||
IconReport,
|
||||
@@ -51,16 +52,25 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
|
||||
icon: IconBox,
|
||||
href: "/manage/apps",
|
||||
label: t("items.apps"),
|
||||
hidden: !session,
|
||||
},
|
||||
{
|
||||
icon: IconPlug,
|
||||
href: "/manage/integrations",
|
||||
label: t("items.integrations"),
|
||||
hidden: !session,
|
||||
},
|
||||
{
|
||||
icon: IconSearch,
|
||||
href: "/manage/search-engines",
|
||||
label: t("items.searchEngies"),
|
||||
hidden: !session,
|
||||
},
|
||||
{
|
||||
icon: IconPhoto,
|
||||
href: "/manage/medias",
|
||||
label: t("items.medias"),
|
||||
hidden: !session,
|
||||
},
|
||||
{
|
||||
icon: IconUser,
|
||||
@@ -88,27 +98,32 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
|
||||
{
|
||||
label: t("items.tools.label"),
|
||||
icon: IconTool,
|
||||
hidden: !session?.user.permissions.includes("admin"),
|
||||
// As permissions always include there children permissions, we can check other-view-logs as admin includes it
|
||||
hidden: !session?.user.permissions.includes("other-view-logs"),
|
||||
items: [
|
||||
{
|
||||
label: t("items.tools.items.docker"),
|
||||
icon: IconBrandDocker,
|
||||
href: "/manage/tools/docker",
|
||||
hidden: !session?.user.permissions.includes("admin"),
|
||||
},
|
||||
{
|
||||
label: t("items.tools.items.api"),
|
||||
icon: IconPlug,
|
||||
href: "/manage/tools/api",
|
||||
hidden: !session?.user.permissions.includes("admin"),
|
||||
},
|
||||
{
|
||||
label: t("items.tools.items.logs"),
|
||||
icon: IconLogs,
|
||||
href: "/manage/tools/logs",
|
||||
hidden: !session?.user.permissions.includes("other-view-logs"),
|
||||
},
|
||||
{
|
||||
label: t("items.tools.items.tasks"),
|
||||
icon: IconReport,
|
||||
href: "/manage/tools/tasks",
|
||||
hidden: !session?.user.permissions.includes("admin"),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { ActionIcon, CopyButton, Tooltip } from "@mantine/core";
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
interface CopyMediaProps {
|
||||
media: RouterOutputs["media"]["getPaginated"]["items"][number];
|
||||
}
|
||||
|
||||
export const CopyMedia = ({ media }: CopyMediaProps) => {
|
||||
const t = useI18n();
|
||||
|
||||
const url = typeof window !== "undefined" ? `${window.location.origin}/api/user-medias/${media.id}` : "";
|
||||
|
||||
return (
|
||||
<CopyButton value={url}>
|
||||
{({ copy, copied }) => (
|
||||
<Tooltip label={t("media.action.copy.label")} openDelay={500}>
|
||||
<ActionIcon onClick={copy} color={copied ? "teal" : "gray"} variant="subtle">
|
||||
{copied ? <IconCheck size={16} stroke={1.5} /> : <IconCopy size={16} stroke={1.5} />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</CopyButton>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { ActionIcon, Tooltip } from "@mantine/core";
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
interface DeleteMediaProps {
|
||||
media: RouterOutputs["media"]["getPaginated"]["items"][number];
|
||||
}
|
||||
|
||||
export const DeleteMedia = ({ media }: DeleteMediaProps) => {
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const t = useI18n();
|
||||
const { mutateAsync, isPending } = clientApi.media.deleteMedia.useMutation();
|
||||
|
||||
const onClick = () => {
|
||||
openConfirmModal({
|
||||
title: t("media.action.delete.label"),
|
||||
children: t.rich("media.action.delete.description", { bName: () => <b>{media.name}</b> }),
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
onConfirm: async () => {
|
||||
await mutateAsync({ id: media.id });
|
||||
await revalidatePathActionAsync("/manage/medias");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip label={t("media.action.delete.label")} openDelay={500}>
|
||||
<ActionIcon color="red" variant="subtle" onClick={onClick} loading={isPending}>
|
||||
<IconTrash color="red" size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { ChangeEvent } from "react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { Switch } from "@mantine/core";
|
||||
import type { SwitchProps } from "@mantine/core";
|
||||
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
type ShowAllSwitchProps = Pick<SwitchProps, "defaultChecked">;
|
||||
|
||||
export const IncludeFromAllUsersSwitch = ({ defaultChecked }: ShowAllSwitchProps) => {
|
||||
const router = useRouter();
|
||||
const pathName = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const [checked, setChecked] = useState(defaultChecked);
|
||||
const t = useI18n();
|
||||
|
||||
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setChecked(event.target.checked);
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set("includeFromAllUsers", event.target.checked.toString());
|
||||
if (params.has("page")) params.set("page", "1"); // Reset page to 1
|
||||
router.replace(`${pathName}?${params.toString()}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch
|
||||
defaultChecked={defaultChecked}
|
||||
checked={checked}
|
||||
label={t("management.page.media.includeFromAllUsers")}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { Button, FileButton } from "@mantine/core";
|
||||
import { IconUpload } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { supportedMediaUploadFormats } from "@homarr/validation";
|
||||
|
||||
export const UploadMedia = () => {
|
||||
const t = useI18n();
|
||||
const { mutateAsync, isPending } = clientApi.media.uploadMedia.useMutation();
|
||||
|
||||
const handleFileUploadAsync = async (file: File | null) => {
|
||||
if (!file) return;
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
await mutateAsync(formData, {
|
||||
onSuccess() {
|
||||
showSuccessNotification({
|
||||
message: t("media.action.upload.notification.success.message"),
|
||||
});
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
message: t("media.action.upload.notification.error.message"),
|
||||
});
|
||||
},
|
||||
async onSettled() {
|
||||
await revalidatePathActionAsync("/manage/medias");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<FileButton onChange={handleFileUploadAsync} accept={supportedMediaUploadFormats.join(",")}>
|
||||
{({ onClick }) => (
|
||||
<Button onClick={onClick} loading={isPending} rightSection={<IconUpload size={16} stroke={1.5} />}>
|
||||
{t("media.action.upload.label")}
|
||||
</Button>
|
||||
)}
|
||||
</FileButton>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Anchor, Group, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from "@mantine/core";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { humanFileSize } from "@homarr/common";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
import { SearchInput, TablePagination, UserAvatar } from "@homarr/ui";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { CopyMedia } from "./_actions/copy-media";
|
||||
import { DeleteMedia } from "./_actions/delete-media";
|
||||
import { IncludeFromAllUsersSwitch } from "./_actions/show-all";
|
||||
import { UploadMedia } from "./_actions/upload-media";
|
||||
|
||||
const searchParamsSchema = z.object({
|
||||
search: z.string().optional(),
|
||||
includeFromAllUsers: z
|
||||
.string()
|
||||
.regex(/true|false/)
|
||||
.catch("false")
|
||||
.transform((value) => value === "true"),
|
||||
pageSize: z.string().regex(/\d+/).transform(Number).catch(10),
|
||||
page: z.string().regex(/\d+/).transform(Number).catch(1),
|
||||
});
|
||||
|
||||
type SearchParamsSchemaInputFromSchema<TSchema extends Record<string, unknown>> = Partial<{
|
||||
[K in keyof TSchema]: Exclude<TSchema[K], undefined> extends unknown[] ? string[] : string;
|
||||
}>;
|
||||
|
||||
interface MediaListPageProps {
|
||||
searchParams: SearchParamsSchemaInputFromSchema<z.infer<typeof searchParamsSchema>>;
|
||||
}
|
||||
|
||||
export default async function GroupsListPage(props: MediaListPageProps) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const t = await getI18n();
|
||||
const searchParams = searchParamsSchema.parse(props.searchParams);
|
||||
const { items: medias, totalCount } = await api.media.getPaginated(searchParams);
|
||||
|
||||
return (
|
||||
<ManageContainer size="xl">
|
||||
<DynamicBreadcrumb />
|
||||
<Stack>
|
||||
<Title>{t("media.plural")}</Title>
|
||||
<Group justify="space-between">
|
||||
<Group>
|
||||
<SearchInput placeholder={`${t("media.search")}...`} defaultValue={searchParams.search} />
|
||||
{session.user.permissions.includes("media-view-all") && (
|
||||
<IncludeFromAllUsersSwitch defaultChecked={searchParams.includeFromAllUsers} />
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{session.user.permissions.includes("media-upload") && <UploadMedia />}
|
||||
</Group>
|
||||
<Table striped highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh></TableTh>
|
||||
<TableTh>{t("media.field.name")}</TableTh>
|
||||
<TableTh>{t("media.field.size")}</TableTh>
|
||||
<TableTh>{t("media.field.creator")}</TableTh>
|
||||
<TableTh></TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{medias.map((media) => (
|
||||
<Row key={media.id} media={media} />
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
|
||||
<Group justify="end">
|
||||
<TablePagination total={Math.ceil(totalCount / searchParams.pageSize)} />
|
||||
</Group>
|
||||
</Stack>
|
||||
</ManageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface RowProps {
|
||||
media: RouterOutputs["media"]["getPaginated"]["items"][number];
|
||||
}
|
||||
|
||||
const Row = async ({ media }: RowProps) => {
|
||||
const session = await auth();
|
||||
const canDelete = media.creatorId === session?.user.id || session?.user.permissions.includes("media-full-all");
|
||||
|
||||
return (
|
||||
<TableTr>
|
||||
<TableTd w={64}>
|
||||
<Image
|
||||
src={`/api/user-medias/${media.id}`}
|
||||
alt={media.name}
|
||||
width={64}
|
||||
height={64}
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
</TableTd>
|
||||
<TableTd>{media.name}</TableTd>
|
||||
<TableTd>{humanFileSize(media.size)}</TableTd>
|
||||
<TableTd>
|
||||
{media.creator ? (
|
||||
<Group gap="sm">
|
||||
<UserAvatar user={media.creator} size="sm" />
|
||||
<Anchor component={Link} href={`/manage/users/${media.creator.id}/general`} size="sm">
|
||||
{media.creator.name}
|
||||
</Anchor>
|
||||
</Group>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableTd>
|
||||
<TableTd w={64}>
|
||||
<Group wrap="nowrap" gap="xs">
|
||||
<CopyMedia media={media} />
|
||||
{canDelete && <DeleteMedia media={media} />}
|
||||
</Group>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
);
|
||||
};
|
||||
@@ -64,6 +64,7 @@ export default async function ManagementPage() {
|
||||
href: "/manage/apps",
|
||||
subtitle: t("statisticLabel.resources"),
|
||||
title: t("statistic.app"),
|
||||
hidden: !session?.user,
|
||||
},
|
||||
{
|
||||
count: statistics.countGroups,
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Button, Grid, Group, Stack, Textarea, TextInput } from "@mantine/core";
|
||||
import type { SegmentedControlItem } from "@mantine/core";
|
||||
import { Button, Fieldset, Grid, Group, SegmentedControl, Stack, Textarea, TextInput } from "@mantine/core";
|
||||
import { WidgetIntegrationSelect } from "node_modules/@homarr/widgets/src/widget-integration-select";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { searchEngineTypes } from "@homarr/definitions";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
@@ -25,6 +29,8 @@ export const SearchEngineForm = (props: SearchEngineFormProps) => {
|
||||
const { submitButtonTranslation, handleSubmit, initialValues, isPending, disableShort } = props;
|
||||
const t = useI18n();
|
||||
|
||||
const [integrationData] = clientApi.integration.allThatSupportSearch.useSuspenseQuery();
|
||||
|
||||
const form = useZodForm(validation.searchEngine.manage, {
|
||||
initialValues: initialValues ?? {
|
||||
name: "",
|
||||
@@ -32,6 +38,7 @@ export const SearchEngineForm = (props: SearchEngineFormProps) => {
|
||||
iconUrl: "",
|
||||
urlTemplate: "",
|
||||
description: "",
|
||||
type: "generic",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -52,11 +59,40 @@ export const SearchEngineForm = (props: SearchEngineFormProps) => {
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<IconPicker initialValue={initialValues?.iconUrl} {...form.getInputProps("iconUrl")} />
|
||||
<TextInput
|
||||
{...form.getInputProps("urlTemplate")}
|
||||
withAsterisk
|
||||
label={t("search.engine.field.urlTemplate.label")}
|
||||
/>
|
||||
|
||||
<Fieldset legend={t("search.engine.page.edit.configControl")}>
|
||||
<SegmentedControl
|
||||
data={searchEngineTypes.map(
|
||||
(type) =>
|
||||
({
|
||||
label: t(`search.engine.page.edit.searchEngineType.${type}`),
|
||||
value: type,
|
||||
}) satisfies SegmentedControlItem,
|
||||
)}
|
||||
{...form.getInputProps("type")}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
{form.values.type === "generic" && (
|
||||
<TextInput
|
||||
{...form.getInputProps("urlTemplate")}
|
||||
withAsterisk
|
||||
label={t("search.engine.field.urlTemplate.label")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{form.values.type === "fromIntegration" && (
|
||||
<WidgetIntegrationSelect
|
||||
label="Integration"
|
||||
data={integrationData}
|
||||
canSelectMultiple={false}
|
||||
onChange={(value) => form.setFieldValue("integrationId", value[0])}
|
||||
value={form.values.integrationId !== undefined ? [form.values.integrationId] : []}
|
||||
withAsterisk
|
||||
/>
|
||||
)}
|
||||
</Fieldset>
|
||||
|
||||
<Textarea {...form.getInputProps("description")} label={t("search.engine.field.description.label")} />
|
||||
|
||||
<Group justify="end">
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Stack, Title } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
@@ -12,6 +14,12 @@ interface SearchEngineEditPageProps {
|
||||
}
|
||||
|
||||
export default async function SearchEngineEditPage({ params }: SearchEngineEditPageProps) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user.permissions.includes("search-engine-modify-all")) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const searchEngine = await api.searchEngine.byId({ id: params.id });
|
||||
const t = await getI18n();
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Stack, Title } from "@mantine/core";
|
||||
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
@@ -7,6 +9,12 @@ import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { SearchEngineNewForm } from "./_search-engine-new-form";
|
||||
|
||||
export default async function SearchEngineNewPage() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user.permissions.includes("search-engine-create")) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const t = await getI18n();
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconPencil, IconSearch } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
||||
import { SearchInput, TablePagination } from "@homarr/ui";
|
||||
import { z } from "@homarr/validation";
|
||||
@@ -11,6 +13,7 @@ import { z } from "@homarr/validation";
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { NoResults } from "~/components/no-results";
|
||||
import { SearchEngineDeleteButton } from "./_search-engine-delete-button";
|
||||
|
||||
const searchParamsSchema = z.object({
|
||||
@@ -28,6 +31,12 @@ interface SearchEnginesPageProps {
|
||||
}
|
||||
|
||||
export default async function SearchEnginesPage(props: SearchEnginesPageProps) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session) {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
||||
const searchParams = searchParamsSchema.parse(props.searchParams);
|
||||
const { items: searchEngines, totalCount } = await api.searchEngine.getPaginated(searchParams);
|
||||
|
||||
@@ -40,9 +49,11 @@ export default async function SearchEnginesPage(props: SearchEnginesPageProps) {
|
||||
<Title>{tEngine("page.list.title")}</Title>
|
||||
<Group justify="space-between" align="center">
|
||||
<SearchInput placeholder={`${tEngine("search")}...`} defaultValue={searchParams.search} />
|
||||
<MobileAffixButton component={Link} href="/manage/search-engines/new">
|
||||
{tEngine("page.create.title")}
|
||||
</MobileAffixButton>
|
||||
{session.user.permissions.includes("search-engine-create") && (
|
||||
<MobileAffixButton component={Link} href="/manage/search-engines/new">
|
||||
{tEngine("page.create.title")}
|
||||
</MobileAffixButton>
|
||||
)}
|
||||
</Group>
|
||||
{searchEngines.length === 0 && <SearchEngineNoResults />}
|
||||
{searchEngines.length > 0 && (
|
||||
@@ -67,9 +78,10 @@ interface SearchEngineCardProps {
|
||||
|
||||
const SearchEngineCard = async ({ searchEngine }: SearchEngineCardProps) => {
|
||||
const t = await getScopedI18n("search.engine");
|
||||
const session = await auth();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card withBorder>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group align="top" justify="start" wrap="nowrap" style={{ flex: 1 }}>
|
||||
<Avatar
|
||||
@@ -91,23 +103,34 @@ const SearchEngineCard = async ({ searchEngine }: SearchEngineCardProps) => {
|
||||
{searchEngine.description}
|
||||
</Text>
|
||||
)}
|
||||
<Anchor href={searchEngine.urlTemplate.replace("%s", "test")} lineClamp={1} size="sm">
|
||||
{searchEngine.urlTemplate}
|
||||
</Anchor>
|
||||
{searchEngine.type === "generic" && searchEngine.urlTemplate !== null && (
|
||||
<Anchor href={searchEngine.urlTemplate.replace("%s", "test")} lineClamp={1} size="sm">
|
||||
{searchEngine.urlTemplate}
|
||||
</Anchor>
|
||||
)}
|
||||
{searchEngine.type === "fromIntegration" && searchEngine.integrationId !== null && (
|
||||
<Text c="dimmed" size="sm">
|
||||
{t("page.list.interactive")}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
<Group>
|
||||
<ActionIconGroup>
|
||||
<ActionIcon
|
||||
component={Link}
|
||||
href={`/manage/search-engines/edit/${searchEngine.id}`}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label={t("page.edit.title")}
|
||||
>
|
||||
<IconPencil size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
<SearchEngineDeleteButton searchEngine={searchEngine} />
|
||||
{session?.user.permissions.includes("search-engine-modify-all") && (
|
||||
<ActionIcon
|
||||
component={Link}
|
||||
href={`/manage/search-engines/edit/${searchEngine.id}`}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label={t("page.edit.title")}
|
||||
>
|
||||
<IconPencil size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
{session?.user.permissions.includes("search-engine-full-all") && (
|
||||
<SearchEngineDeleteButton searchEngine={searchEngine} />
|
||||
)}
|
||||
</ActionIconGroup>
|
||||
</Group>
|
||||
</Group>
|
||||
@@ -117,16 +140,17 @@ const SearchEngineCard = async ({ searchEngine }: SearchEngineCardProps) => {
|
||||
|
||||
const SearchEngineNoResults = async () => {
|
||||
const t = await getI18n();
|
||||
const session = await auth();
|
||||
|
||||
return (
|
||||
<Card withBorder bg="transparent">
|
||||
<Stack align="center" gap="sm">
|
||||
<IconSearch size="2rem" />
|
||||
<Text fw={500} size="lg">
|
||||
{t("search.engine.page.list.noResults.title")}
|
||||
</Text>
|
||||
<Anchor href="/manage/search-engines/new">{t("search.engine.page.list.noResults.action")}</Anchor>
|
||||
</Stack>
|
||||
</Card>
|
||||
<NoResults
|
||||
icon={IconSearch}
|
||||
title={t("search.engine.page.list.noResults.title")}
|
||||
action={{
|
||||
label: t("search.engine.page.list.noResults.action"),
|
||||
href: "/manage/search-engines/new",
|
||||
hidden: !session?.user.permissions.includes("search-engine-create"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { Group, Text } from "@mantine/core";
|
||||
import { IconMoon, IconSun } from "@tabler/icons-react";
|
||||
|
||||
import type { ColorScheme } from "@homarr/definitions";
|
||||
import { colorSchemes } from "@homarr/definitions";
|
||||
import type { ServerSettings } from "@homarr/server-settings";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { SelectWithCustomItems } from "@homarr/ui";
|
||||
|
||||
import { CommonSettingsForm } from "./common-form";
|
||||
|
||||
export const AppearanceSettingsForm = ({ defaultValues }: { defaultValues: ServerSettings["appearance"] }) => {
|
||||
const tApperance = useScopedI18n("management.page.settings.section.appearance");
|
||||
|
||||
return (
|
||||
<CommonSettingsForm settingKey="appearance" defaultValues={defaultValues}>
|
||||
{(form) => (
|
||||
<>
|
||||
<SelectWithCustomItems
|
||||
label={tApperance("defaultColorScheme.label")}
|
||||
data={colorSchemes.map((scheme) => ({
|
||||
value: scheme,
|
||||
label: tApperance(`defaultColorScheme.options.${scheme}`),
|
||||
}))}
|
||||
{...form.getInputProps("defaultColorScheme")}
|
||||
SelectOption={ApperanceCustomOption}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</CommonSettingsForm>
|
||||
);
|
||||
};
|
||||
|
||||
const appearanceIcons = {
|
||||
light: IconSun,
|
||||
dark: IconMoon,
|
||||
};
|
||||
|
||||
const ApperanceCustomOption = ({ value, label }: { value: ColorScheme; label: string }) => {
|
||||
const Icon = appearanceIcons[value];
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<Icon size={16} stroke={1.5} />
|
||||
<Text fz="sm" fw={500}>
|
||||
{label}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { Group, Text } from "@mantine/core";
|
||||
import { IconLayoutDashboard } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { ServerSettings } from "@homarr/server-settings";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { SelectWithCustomItems } from "@homarr/ui";
|
||||
|
||||
import { CommonSettingsForm } from "./common-form";
|
||||
|
||||
export const BoardSettingsForm = ({ defaultValues }: { defaultValues: ServerSettings["board"] }) => {
|
||||
const tBoard = useScopedI18n("management.page.settings.section.board");
|
||||
const [selectableBoards] = clientApi.board.getPublicBoards.useSuspenseQuery();
|
||||
|
||||
return (
|
||||
<CommonSettingsForm settingKey="board" defaultValues={defaultValues}>
|
||||
{(form) => (
|
||||
<>
|
||||
<SelectWithCustomItems
|
||||
label={tBoard("homeBoard.label")}
|
||||
description={tBoard("homeBoard.description")}
|
||||
data={selectableBoards.map((board) => ({
|
||||
value: board.id,
|
||||
label: board.name,
|
||||
image: board.logoImageUrl,
|
||||
}))}
|
||||
SelectOption={({ label, image }: { value: string; label: string; image: string | null }) => (
|
||||
<Group>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
{image ? <img width={16} height={16} src={image} alt={label} /> : <IconLayoutDashboard size={16} />}
|
||||
<Text fz="sm" fw={500}>
|
||||
{label}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
{...form.getInputProps("homeBoardId")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</CommonSettingsForm>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Group, Stack } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import type { ServerSettings } from "@homarr/server-settings";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
export const CommonSettingsForm = <TKey extends keyof ServerSettings>({
|
||||
settingKey,
|
||||
defaultValues,
|
||||
children,
|
||||
}: {
|
||||
settingKey: TKey;
|
||||
defaultValues: ServerSettings[TKey];
|
||||
children: (form: ReturnType<typeof useForm<ServerSettings[TKey]>>) => React.ReactNode;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const tSettings = useScopedI18n("management.page.settings");
|
||||
const { mutateAsync, isPending } = clientApi.serverSettings.saveSettings.useMutation({
|
||||
onSuccess() {
|
||||
showSuccessNotification({
|
||||
message: tSettings("notification.success.message"),
|
||||
});
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
message: tSettings("notification.error.message"),
|
||||
});
|
||||
},
|
||||
});
|
||||
const form = useForm({
|
||||
initialValues: defaultValues,
|
||||
});
|
||||
|
||||
const handleSubmitAsync = async (values: ServerSettings[TKey]) => {
|
||||
await mutateAsync({
|
||||
settingsKey: settingKey,
|
||||
value: values,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
|
||||
<Stack gap="sm">
|
||||
{children(form)}
|
||||
<Group justify="end">
|
||||
<Button type="submit" loading={isPending}>
|
||||
{t("common.action.save")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import type { ServerSettings } from "@homarr/server-settings";
|
||||
import type { SupportedLanguage } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { LanguageCombobox } from "~/components/language/language-combobox";
|
||||
import { CommonSettingsForm } from "./common-form";
|
||||
|
||||
export const CultureSettingsForm = ({ defaultValues }: { defaultValues: ServerSettings["culture"] }) => {
|
||||
const tCulture = useScopedI18n("management.page.settings.section.culture");
|
||||
|
||||
return (
|
||||
<CommonSettingsForm settingKey="culture" defaultValues={defaultValues}>
|
||||
{(form) => (
|
||||
<>
|
||||
<LanguageCombobox
|
||||
label={tCulture("defaultLocale.label")}
|
||||
value={form.getInputProps("defaultLocale").value as SupportedLanguage}
|
||||
{...form.getInputProps("defaultLocale")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</CommonSettingsForm>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,16 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Stack, Title } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { CrawlingAndIndexingSettings } from "~/app/[locale]/manage/settings/_components/crawling-and-indexing.settings";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { AnalyticsSettings } from "./_components/analytics.settings";
|
||||
import { AppearanceSettingsForm } from "./_components/appearance-settings-form";
|
||||
import { BoardSettingsForm } from "./_components/board-settings-form";
|
||||
import { CultureSettingsForm } from "./_components/culture-settings-form";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const t = await getScopedI18n("management");
|
||||
@@ -17,15 +22,33 @@ export async function generateMetadata() {
|
||||
}
|
||||
|
||||
export default async function SettingsPage() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user.permissions.includes("admin")) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const serverSettings = await api.serverSettings.getAll();
|
||||
const t = await getScopedI18n("management.page.settings");
|
||||
const tSettings = await getScopedI18n("management.page.settings");
|
||||
return (
|
||||
<>
|
||||
<DynamicBreadcrumb />
|
||||
<Stack>
|
||||
<Title order={1}>{t("title")}</Title>
|
||||
<Title order={1}>{tSettings("title")}</Title>
|
||||
<AnalyticsSettings initialData={serverSettings.analytics} />
|
||||
<CrawlingAndIndexingSettings initialData={serverSettings.crawlingAndIndexing} />
|
||||
<Stack>
|
||||
<Title order={2}>{tSettings("section.board.title")}</Title>
|
||||
<BoardSettingsForm defaultValues={serverSettings.board} />
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Title order={2}>{tSettings("section.appearance.title")}</Title>
|
||||
<AppearanceSettingsForm defaultValues={serverSettings.appearance} />
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Title order={2}>{tSettings("section.culture.title")}</Title>
|
||||
<CultureSettingsForm defaultValues={serverSettings.culture} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ export const ApiKeysManagement = ({ apiKeys }: ApiKeysManagementProps) => {
|
||||
const { mutate, isPending } = clientApi.apiKeys.create.useMutation({
|
||||
async onSuccess(data) {
|
||||
openModal({
|
||||
apiKey: data.randomToken,
|
||||
apiKey: data.apiKey,
|
||||
});
|
||||
await revalidatePathActionAsync("/manage/tools/api");
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import type { OpenAPIV3 } from "openapi-types";
|
||||
import type { OpenAPIObject } from "openapi3-ts/oas31";
|
||||
import SwaggerUI from "swagger-ui-react";
|
||||
|
||||
// workaround for CSS that cannot be processed by next.js, https://github.com/swagger-api/swagger-ui/issues/10045
|
||||
@@ -9,7 +9,7 @@ import "../swagger-ui-overrides.css";
|
||||
import "../swagger-ui.css";
|
||||
|
||||
interface SwaggerUIClientProps {
|
||||
document: OpenAPIV3.Document;
|
||||
document: OpenAPIObject;
|
||||
}
|
||||
|
||||
export const SwaggerUIClient = ({ document }: SwaggerUIClientProps) => {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Anchor, Center, Stack, Text } from "@mantine/core";
|
||||
import { IconShipOff } from "@tabler/icons-react";
|
||||
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
export default function DockerErrorPage() {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<Stack align="center">
|
||||
<IconShipOff size={48} stroke={1.5} />
|
||||
<Stack align="center" gap="xs">
|
||||
<Text size="lg" fw={500}>
|
||||
{t("docker.error.internalServerError")}
|
||||
</Text>
|
||||
<Anchor size="sm" component={Link} href="/manage/tools/logs">
|
||||
{t("common.action.checkLogs")}
|
||||
</Anchor>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export async function generateMetadata() {
|
||||
|
||||
export default async function LogsManagementPage() {
|
||||
const session = await auth();
|
||||
if (!session?.user || !session.user.permissions.includes("admin")) {
|
||||
if (!session?.user || !session.user.permissions.includes("other-view-logs")) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import classes from "./terminal.module.css";
|
||||
export const TerminalComponent = () => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const terminalRef = useRef<Terminal>();
|
||||
const terminalRef = useRef<Terminal>(null);
|
||||
clientApi.log.subscribe.useSubscription(undefined, {
|
||||
onData(data) {
|
||||
terminalRef.current?.writeln(`${data.timestamp} ${data.level} ${data.message}`);
|
||||
|
||||
Loaded 100 of 637 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user