Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19029f40b8 | ||
|
|
b6bc257726 | ||
|
|
fef50a53b1 | ||
|
|
94ff401d7d | ||
|
|
63328d125a | ||
|
|
457b970d8f | ||
|
|
8d69970562 | ||
|
|
8a071f9ab6 | ||
|
|
76b67ac8e1 | ||
|
|
eba738e5d6 | ||
|
|
996ec548c4 | ||
|
|
40e00987cb | ||
|
|
93c64baec2 | ||
|
|
b5b0b069d2 | ||
|
|
62c7955e48 | ||
|
|
217818573e | ||
|
|
70879ccb90 | ||
|
|
34eb9b366f | ||
|
|
ac77bdd84c | ||
|
|
70ea093eb6 | ||
|
|
08e0dd76ae | ||
|
|
0ed4c741c5 | ||
|
|
569ec6fe5c | ||
|
|
e7e80dbc7d | ||
|
|
b054bdd987 | ||
|
|
84f73d33a0 | ||
|
|
78b55202e7 | ||
|
|
7706f97228 | ||
|
|
a0ee558582 | ||
|
|
94d3dad909 | ||
|
|
1a3a55934d | ||
|
|
8bf893b392 | ||
|
|
43b8cfa4dd | ||
|
|
a05b5965b6 | ||
|
|
68407eaa19 | ||
|
|
6854e4729d | ||
|
|
f03e381aea | ||
|
|
151ccb7086 | ||
|
|
8f7f2110ec | ||
|
|
579dd5763d | ||
|
|
3e1c000d51 | ||
|
|
f624611540 | ||
|
|
07f0abf28c | ||
|
|
c3264b2fe1 | ||
|
|
d0aa48fab3 | ||
|
|
d634fa2c95 | ||
|
|
5a8405a9a8 | ||
|
|
a6f388d076 | ||
|
|
1dd7b1a6b9 | ||
|
|
b7a6d950db | ||
|
|
00f5a877a6 | ||
|
|
75a949eecf | ||
|
|
ce0705e9d4 | ||
|
|
0f4a9af57d | ||
|
|
703a362356 | ||
|
|
826d54a0ca | ||
|
|
46c95e0da3 | ||
|
|
0aacbe95c2 | ||
|
|
18f845b4c4 | ||
|
|
b5ae5f876b | ||
|
|
ef359b6997 | ||
|
|
1d3dc5b9e7 | ||
|
|
ff0b0c659c | ||
|
|
8dee8f8680 | ||
|
|
25e4c814ac | ||
|
|
3edfe6bfed | ||
|
|
22d32ccb8d | ||
|
|
7aa5d2f8ed | ||
|
|
91a69c162a | ||
|
|
e3fcfbe916 | ||
|
|
3e4d24b28e | ||
|
|
425d5edab5 | ||
|
|
b0381cb883 | ||
|
|
a04cb25dd0 | ||
|
|
b4692b62ab | ||
|
|
bb8cb6dded | ||
|
|
4cc37397c2 | ||
|
|
2d2ae5a419 | ||
|
|
15197897ac | ||
|
|
289c7f1a60 | ||
|
|
495432073f | ||
|
|
c9fecaabb6 | ||
|
|
5b49fe1039 | ||
|
|
7ba2067689 | ||
|
|
e0ae5e6705 | ||
|
|
305d259e98 | ||
|
|
fed3d1ce23 | ||
|
|
8f083adf39 | ||
|
|
3e9c8172ef | ||
|
|
f743c76b17 | ||
|
|
7d8650babc | ||
|
|
612f0e4fbf | ||
|
|
75fb9dbf1e | ||
|
|
7f7156a798 | ||
|
|
85d2d61184 | ||
|
|
25f7406283 | ||
|
|
1e50ac411c | ||
|
|
04c17d1c41 | ||
|
|
22f9166690 | ||
|
|
8a02c65648 | ||
|
|
bb20999bf3 | ||
|
|
b607c5702f | ||
|
|
83eacb23b9 | ||
|
|
d8247f2dc9 | ||
|
|
56fe8dd9e9 | ||
|
|
c94159388d | ||
|
|
f55d8a9c2e | ||
|
|
47ebc66c9e | ||
|
|
d669f99ac1 | ||
|
|
3d4d32f377 | ||
|
|
268daee4a6 | ||
|
|
a53e7aaee5 | ||
|
|
fbbfe9af29 | ||
|
|
e774b06fc2 | ||
|
|
6c9fbd6c09 | ||
|
|
2135f9980b | ||
|
|
ba3f1e9eaa | ||
|
|
8486073f27 | ||
|
|
afcde8afe5 | ||
|
|
a102f39518 | ||
|
|
225ed4881b | ||
|
|
d714e53cfa | ||
|
|
483ce9c28d | ||
|
|
ff6ca86a78 | ||
|
|
55ad0b0a65 | ||
|
|
8cb8eaf9cc | ||
|
|
9c582d906b | ||
|
|
d6cfd23068 | ||
|
|
f19aa29607 | ||
|
|
f6f0d7c72b | ||
|
|
2f80c096a7 | ||
|
|
8f8d7884a9 | ||
|
|
d584ade8f4 | ||
|
|
82528dbd96 | ||
|
|
d9720ac596 | ||
|
|
9e48f438b4 | ||
|
|
4472c04967 | ||
|
|
2e62a61f4d | ||
|
|
0b07f227ee | ||
|
|
fd5b1b0c08 | ||
|
|
1db9c3c692 | ||
|
|
fb800a0008 | ||
|
|
c0e1355d7f | ||
|
|
7ec6fbc376 | ||
|
|
df249a0173 | ||
|
|
ac5fc38ec0 | ||
|
|
75bee0be8b | ||
|
|
b3464b3340 | ||
|
|
4a7ccda362 | ||
|
|
d410af3402 | ||
|
|
65f00f19de | ||
|
|
e3ad569cc2 | ||
|
|
f25837f9f8 | ||
|
|
44007b2b4b | ||
|
|
37d471457a | ||
|
|
9881577f47 | ||
|
|
a572aae887 | ||
|
|
4721a97094 | ||
|
|
15026afa46 | ||
|
|
3403579925 | ||
|
|
da4aaba9f0 | ||
|
|
98dd96f37c | ||
|
|
67c739c35b | ||
|
|
6f8b3de903 | ||
|
|
e4a39223a3 |
No files matched your search
@@ -34,4 +34,7 @@ DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
|
||||
# If it is used, please use the full path to the directory where the certificates are stored.
|
||||
# LOCAL_CERTIFICATE_PATH='FULL_PATH_TO_CERTIFICATES'
|
||||
|
||||
TURBO_TELEMETRY_DISABLED=1
|
||||
TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
# Enable kubernetes tool
|
||||
# ENABLE_KUBERNETES=true
|
||||
@@ -31,6 +31,13 @@ body:
|
||||
label: Version
|
||||
description: What version of Homarr are you running?
|
||||
options:
|
||||
- 1.13.0
|
||||
- 1.12.0
|
||||
- 1.11.0
|
||||
- 1.10.0
|
||||
- 1.9.0
|
||||
- 1.8.0
|
||||
- 1.7.0
|
||||
- 1.6.0
|
||||
- 1.5.0
|
||||
- 1.4.0
|
||||
|
||||
@@ -6,11 +6,6 @@
|
||||
matchPackagePatterns: ["^@homarr/"],
|
||||
enabled: false,
|
||||
},
|
||||
// 15.2.0 crashes with turbopack error (panic)
|
||||
{
|
||||
matchPackagePatterns: ["^next$", "^@next/eslint-plugin-next$"],
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
matchUpdateTypes: ["minor", "patch", "pin", "digest"],
|
||||
automerge: true,
|
||||
|
||||
@@ -65,3 +65,6 @@ e2e/shared/tmp
|
||||
|
||||
#personal backgrounds
|
||||
apps/nextjs/public/images/background.png
|
||||
|
||||
# next-intl
|
||||
en.d.json.ts
|
||||
@@ -0,0 +1,14 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="All Tests" type="JavaScriptTestRunnerVitest" nameIsGenerated="true">
|
||||
<node-interpreter value="project" />
|
||||
<vitest-package value="$PROJECT_DIR$/node_modules/vitest" />
|
||||
<working-dir value="$PROJECT_DIR$" />
|
||||
<vitest-options value="--exclude e2e --coverage.enabled" />
|
||||
<envs>
|
||||
<env name="NODE_ENV" value="development" />
|
||||
<env name="CI" value="true" />
|
||||
</envs>
|
||||
<scope-kind value="ALL" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -12,17 +12,17 @@ languageIds:
|
||||
# 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
|
||||
# For direct t("your.i18n.keys") usage
|
||||
- "[^\\w\\d]t\\(['\"`]({key})['\"`]"
|
||||
# For variable t assigned from getScopedI18n or useScopedI18n
|
||||
- "\\bt\\(['\"`]({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*['\"](.*?)['\"]\\)"
|
||||
scopeRangeRegex: "(?:const|let|var)\\s+t\\s*=\\s*(?:await\\s+)?(?:getScopedI18n|useScopedI18n)\\(\\s*['\"](.*?)['\"]\\)"
|
||||
|
||||
# An array of strings containing refactor templates.
|
||||
# The "$1" will be replaced by the keypath specified.
|
||||
|
||||
@@ -1,3 +1,145 @@
|
||||
## [1.13.1](https://github.com/homarr-labs/homarr/compare/v1.13.0...v1.13.1) (2025-03-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update react monorepo to v19.1.0 ([#2734](https://github.com/homarr-labs/homarr/issues/2734)) ([94ff401](https://github.com/homarr-labs/homarr/commit/94ff401d7dface924f48affccbc59e3a98ed8dc1))
|
||||
|
||||
### Reverts
|
||||
|
||||
* ssg integration create ([#2736](https://github.com/homarr-labs/homarr/issues/2736)) ([fef50a5](https://github.com/homarr-labs/homarr/commit/fef50a53b1fb592668dfa9aade6b110e42aec9a8))
|
||||
|
||||
## [1.13.0](https://github.com/homarr-labs/homarr/compare/v1.12.0...v1.13.0) (2025-03-28)
|
||||
|
||||
### Features
|
||||
|
||||
* add default search engines seeding / set homarr docs as global server search engine ([#2663](https://github.com/homarr-labs/homarr/issues/2663)) ([3e1c000](https://github.com/homarr-labs/homarr/commit/3e1c000d515c0edc6f49241fc381187e1829ca7e))
|
||||
* add german swiss locale ([#2726](https://github.com/homarr-labs/homarr/issues/2726)) ([eba738e](https://github.com/homarr-labs/homarr/commit/eba738e5d6594d46b5b7d1eab875fc8060e0fe7b))
|
||||
* add jetbrains run profile ([#2704](https://github.com/homarr-labs/homarr/issues/2704)) ([07f0abf](https://github.com/homarr-labs/homarr/commit/07f0abf28cb31f81860e623c70970e16d17e2836))
|
||||
* add title to dynamic section ([#2614](https://github.com/homarr-labs/homarr/issues/2614)) ([1dd7b1a](https://github.com/homarr-labs/homarr/commit/1dd7b1a6b97f75bca0d1a5c0dbb4254aa4bbd418))
|
||||
* app dropdown add infinity scroll ([#2696](https://github.com/homarr-labs/homarr/issues/2696)) ([8bf893b](https://github.com/homarr-labs/homarr/commit/8bf893b39292f34f5c697c09ba2c9e53c8a6ea90))
|
||||
* **auth:** add env variable to force user-info usage instead of idtoken ([#2711](https://github.com/homarr-labs/homarr/issues/2711)) ([94d3dad](https://github.com/homarr-labs/homarr/commit/94d3dad909c39cec096ff61c0a2f45e8853b6111))
|
||||
* auto sign-in when creating the initial user ([#2664](https://github.com/homarr-labs/homarr/issues/2664)) ([46c95e0](https://github.com/homarr-labs/homarr/commit/46c95e0da3ad27eb072efc45250abb0baf3e2483))
|
||||
* implement auto-select icon ([#2679](https://github.com/homarr-labs/homarr/issues/2679)) ([1a3a559](https://github.com/homarr-labs/homarr/commit/1a3a55934d8b3e4703e0ce44d9d26ad7ff100aca))
|
||||
* **lang:** add english uk language ([#2715](https://github.com/homarr-labs/homarr/issues/2715)) ([93c64ba](https://github.com/homarr-labs/homarr/commit/93c64baec294c39cd490a85f8b06b57d53fc82b3))
|
||||
* **logs:** improve logs by logging errors with causes and metadata ([#2703](https://github.com/homarr-labs/homarr/issues/2703)) ([579dd57](https://github.com/homarr-labs/homarr/commit/579dd5763d139e1644c2e192620728afa54c96d2))
|
||||
* set home board when creating first board ([#2683](https://github.com/homarr-labs/homarr/issues/2683)) ([8f7f211](https://github.com/homarr-labs/homarr/commit/8f7f2110ec29db12ea2d83cd779ec86851209188))
|
||||
* SSG integration creation page ([#2684](https://github.com/homarr-labs/homarr/issues/2684)) ([ce0705e](https://github.com/homarr-labs/homarr/commit/ce0705e9d4a502273a0d84e4e14d61a061fd21f9))
|
||||
* **widget:** add restriction callback to restrict visibility and modification of widget kinds ([#2658](https://github.com/homarr-labs/homarr/issues/2658)) ([84f73d3](https://github.com/homarr-labs/homarr/commit/84f73d33a061a1d243803078b245a8016a615af9))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow deletion of external users ([#2725](https://github.com/homarr-labs/homarr/issues/2725)) ([40e0098](https://github.com/homarr-labs/homarr/commit/40e00987cb6f4ee58fdce1da9489eedd92b8843d))
|
||||
* **auth:** fusion auth returns www-authenticate token response ([#2719](https://github.com/homarr-labs/homarr/issues/2719)) ([b5b0b06](https://github.com/homarr-labs/homarr/commit/b5b0b069d2029110d5618a29af5ae003030584d9))
|
||||
* bookmarks layout ([#2675](https://github.com/homarr-labs/homarr/issues/2675)) ([8a071f9](https://github.com/homarr-labs/homarr/commit/8a071f9ab6bc4c85073dc52cc2e60da69f30950a))
|
||||
* calendar indicators ([#2712](https://github.com/homarr-labs/homarr/issues/2712)) ([70ea093](https://github.com/homarr-labs/homarr/commit/70ea093eb6530d25d3e60e6d496651e3fc457826))
|
||||
* **deps:** update dependency drizzle-kit to ^0.30.6 ([#2706](https://github.com/homarr-labs/homarr/issues/2706)) ([f03e381](https://github.com/homarr-labs/homarr/commit/f03e381aea414c5fb770c7ba092b31f9c53eee11))
|
||||
* **deps:** update dependency drizzle-zod to ^0.7.1 ([#2717](https://github.com/homarr-labs/homarr/issues/2717)) ([569ec6f](https://github.com/homarr-labs/homarr/commit/569ec6fe5ccec980351162024170fabe06dcc3da))
|
||||
* **deps:** update dependency ldapts to v7.3.3 ([#2691](https://github.com/homarr-labs/homarr/issues/2691)) ([00f5a87](https://github.com/homarr-labs/homarr/commit/00f5a877a6f09037eff4c9934fc273c4d2af0f6c))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.20.2 ([#2708](https://github.com/homarr-labs/homarr/issues/2708)) ([68407ea](https://github.com/homarr-labs/homarr/commit/68407eaa195894db37018cefeeca4ae2da085847))
|
||||
* **deps:** update dependency typescript-eslint to ^8.28.0 ([#2693](https://github.com/homarr-labs/homarr/issues/2693)) ([f624611](https://github.com/homarr-labs/homarr/commit/f6246115405cdc85948059170742b0bae58ab337))
|
||||
* **deps:** update dependency undici to v7.6.0 ([#2710](https://github.com/homarr-labs/homarr/issues/2710)) ([43b8cfa](https://github.com/homarr-labs/homarr/commit/43b8cfa4dd250013fdf15d509a072ba0984f9ab1))
|
||||
* **deps:** update mantine monorepo to ^7.17.3 ([#2707](https://github.com/homarr-labs/homarr/issues/2707)) ([6854e47](https://github.com/homarr-labs/homarr/commit/6854e4729d3ba4a82a318813fec0a20eccdbed18))
|
||||
* **deps:** update nextjs monorepo to v15.2.4 ([#2720](https://github.com/homarr-labs/homarr/issues/2720)) ([ac77bdd](https://github.com/homarr-labs/homarr/commit/ac77bdd84cba47a43d1952725a9c7feccd972766))
|
||||
* **deps:** update tanstack-query monorepo to ^5.69.2 ([#2714](https://github.com/homarr-labs/homarr/issues/2714)) ([78b5520](https://github.com/homarr-labs/homarr/commit/78b55202e7f4739933768d27211556f927b04fed))
|
||||
* **deps:** update tanstack-query monorepo to ^5.69.3 ([#2716](https://github.com/homarr-labs/homarr/issues/2716)) ([e7e80db](https://github.com/homarr-labs/homarr/commit/e7e80dbc7d62830ce1ce150b6e22a67220da8be8))
|
||||
* **deps:** update tanstack-query monorepo to ^5.70.0 ([#2722](https://github.com/homarr-labs/homarr/issues/2722)) ([34eb9b3](https://github.com/homarr-labs/homarr/commit/34eb9b366fdc58c9fc03d18ac7dd68bff1ea83e6))
|
||||
* **deps:** update testcontainers-node monorepo to ^10.22.0 ([#2692](https://github.com/homarr-labs/homarr/issues/2692)) ([b7a6d95](https://github.com/homarr-labs/homarr/commit/b7a6d950db557ea9deadc0be014b5f218611ac83))
|
||||
* **deps:** update testcontainers-node monorepo to ^10.23.0 ([#2699](https://github.com/homarr-labs/homarr/issues/2699)) ([c3264b2](https://github.com/homarr-labs/homarr/commit/c3264b2fe10ad374e92eb16921d43427d7ca866b))
|
||||
* **deps:** update tiptap monorepo to v2.11.6 ([#2718](https://github.com/homarr-labs/homarr/issues/2718)) ([0ed4c74](https://github.com/homarr-labs/homarr/commit/0ed4c741c59c3e498aa247adc292bdbbde1c59d3))
|
||||
* **deps:** update trpc monorepo to ^11.0.1 ([#2724](https://github.com/homarr-labs/homarr/issues/2724)) ([2178185](https://github.com/homarr-labs/homarr/commit/217818573e8bd7d49a821afdf269cd8b3f3d442d))
|
||||
* **import:** autofix missing shapes for sidebars and some sections as well ([#2723](https://github.com/homarr-labs/homarr/issues/2723)) ([62c7955](https://github.com/homarr-labs/homarr/commit/62c7955e48ca62b4bf1baf26ac6a12847795ac18))
|
||||
* Media request via search engine ([#2677](https://github.com/homarr-labs/homarr/issues/2677)) ([5a8405a](https://github.com/homarr-labs/homarr/commit/5a8405a9a81257b023c0265309f7f530b2e23c7a))
|
||||
|
||||
### Reverts
|
||||
|
||||
* add development package and update workspace ([#2700](https://github.com/homarr-labs/homarr/issues/2700))" ([#2728](https://github.com/homarr-labs/homarr/issues/2728)) ([996ec54](https://github.com/homarr-labs/homarr/commit/996ec548c4b9c8561919409b81adc90b0a24fbd6))
|
||||
|
||||
## [1.12.0](https://github.com/homarr-labs/homarr/compare/v1.11.0...v1.12.0) (2025-03-21)
|
||||
|
||||
### Features
|
||||
|
||||
* stock price widget ([#2579](https://github.com/homarr-labs/homarr/issues/2579)) ([91a69c1](https://github.com/homarr-labs/homarr/commit/91a69c162a51cff8899eaa7d5a113f6ec8feafba))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** fix-usernames command only fixes lowercase usernames ([#2650](https://github.com/homarr-labs/homarr/issues/2650)) ([b0381cb](https://github.com/homarr-labs/homarr/commit/b0381cb8836b8477d0990c264aa399cb06b85237))
|
||||
* **deps:** update dependency better-sqlite3 to ^11.9.1 ([#2632](https://github.com/homarr-labs/homarr/issues/2632)) ([289c7f1](https://github.com/homarr-labs/homarr/commit/289c7f1a60477024207261d486c2a88718320df7))
|
||||
* **deps:** update dependency drizzle-orm to ^0.40.1 ([#2624](https://github.com/homarr-labs/homarr/issues/2624)) ([7ba2067](https://github.com/homarr-labs/homarr/commit/7ba2067689ed7fd3ab78e75a6ba385483a97b0a3))
|
||||
* **deps:** update dependency drizzle-orm to ^0.41.0 ([#2666](https://github.com/homarr-labs/homarr/issues/2666)) ([8dee8f8](https://github.com/homarr-labs/homarr/commit/8dee8f8680f2282d76f6e727123220a60e2b596d))
|
||||
* **deps:** update dependency mysql2 to v3.14.0 ([#2652](https://github.com/homarr-labs/homarr/issues/2652)) ([3e4d24b](https://github.com/homarr-labs/homarr/commit/3e4d24b28e998af80c00c8ad141660d4bfe5ef33))
|
||||
* **deps:** update dependency sass to ^1.86.0 ([#2629](https://github.com/homarr-labs/homarr/issues/2629)) ([c9fecaa](https://github.com/homarr-labs/homarr/commit/c9fecaabb6416db5c55d9066ed782712cc96faa0))
|
||||
* **deps:** update dependency typescript-eslint to ^8.27.0 ([#2653](https://github.com/homarr-labs/homarr/issues/2653)) ([e3fcfbe](https://github.com/homarr-labs/homarr/commit/e3fcfbe9166834e9443c5ead21c2f67b2d01ad2e))
|
||||
* **deps:** update tanstack-query monorepo to ^5.69.0 ([#2634](https://github.com/homarr-labs/homarr/issues/2634)) ([2d2ae5a](https://github.com/homarr-labs/homarr/commit/2d2ae5a4191085404f8f7f790a4f0f62b86b408d))
|
||||
* **deps:** update testcontainers-node monorepo to ^10.21.0 ([#2633](https://github.com/homarr-labs/homarr/issues/2633)) ([1519789](https://github.com/homarr-labs/homarr/commit/15197897ac3e2f3ed1dcc24c5f64748eaa52f98f))
|
||||
* **permissions:** unable to edit board with full board permissions ([#2625](https://github.com/homarr-labs/homarr/issues/2625)) ([4cc3739](https://github.com/homarr-labs/homarr/commit/4cc37397c2e56a97e6b1173735da78824c7b15e5))
|
||||
* prohibit dot in board name ([#2643](https://github.com/homarr-labs/homarr/issues/2643)) ([bb8cb6d](https://github.com/homarr-labs/homarr/commit/bb8cb6ddeda6906b3c43270ec9dd8ec66d27f0cd))
|
||||
|
||||
## [1.11.0](https://github.com/homarr-labs/homarr/compare/v1.10.0...v1.11.0) (2025-03-14)
|
||||
|
||||
### Features
|
||||
|
||||
* add nextcloud integration ([#2501](https://github.com/homarr-labs/homarr/issues/2501)) ([2e62a61](https://github.com/homarr-labs/homarr/commit/2e62a61f4d20241fb77065faa00b14d427d4f4e0))
|
||||
* **calendar:** add show-unmonitored option ([#2547](https://github.com/homarr-labs/homarr/issues/2547)) ([f6f0d7c](https://github.com/homarr-labs/homarr/commit/f6f0d7c72bd4b91949f957e5e28ff60e6c82d2a5))
|
||||
* **integrations:** add checkbox to add app ([#2463](https://github.com/homarr-labs/homarr/issues/2463)) ([22f9166](https://github.com/homarr-labs/homarr/commit/22f91666908c6a140fadd89f52f57a57a51cd935))
|
||||
* **kubernetes:** add kubernetes tool ([#1929](https://github.com/homarr-labs/homarr/issues/1929)) ([f19aa29](https://github.com/homarr-labs/homarr/commit/f19aa29607640578f61d1842d4300cc0c0618896))
|
||||
* **logs:** improve error logs with stacked causes ([#2574](https://github.com/homarr-labs/homarr/issues/2574)) ([3d4d32f](https://github.com/homarr-labs/homarr/commit/3d4d32f3773f006edc1d441ce419d7b97c26b526))
|
||||
* **widgets:** add border color option to advanced-options ([#2512](https://github.com/homarr-labs/homarr/issues/2512)) ([b3464b3](https://github.com/homarr-labs/homarr/commit/b3464b3340cf57b4f85da0dd2e1bd8e754f1bec8))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* action icons not being rendered ([#2593](https://github.com/homarr-labs/homarr/issues/2593)) ([85d2d61](https://github.com/homarr-labs/homarr/commit/85d2d6118495f3b466a1f014b90446d03c5ffdcb))
|
||||
* **app:** improve responsive styles ([#2537](https://github.com/homarr-labs/homarr/issues/2537)) ([fb800a0](https://github.com/homarr-labs/homarr/commit/fb800a00084833e785deecdb3b6fde4e35ddf7b8))
|
||||
* **bookmarks:** improve responsive styles ([#2536](https://github.com/homarr-labs/homarr/issues/2536)) ([df249a0](https://github.com/homarr-labs/homarr/commit/df249a0173c3a70711c99df3f7983729a64eaff9))
|
||||
* **calendar:** dropdown spans over whole screen ([#2569](https://github.com/homarr-labs/homarr/issues/2569)) ([fbbfe9a](https://github.com/homarr-labs/homarr/commit/fbbfe9af293961afcb686f3e7a559b67b37f6743))
|
||||
* **calendar:** improve responsive styles ([#2538](https://github.com/homarr-labs/homarr/issues/2538)) ([1db9c3c](https://github.com/homarr-labs/homarr/commit/1db9c3c6925f0baae963c5f079037c2bcd1cee44))
|
||||
* **date-time:** improve responsive styles ([#2539](https://github.com/homarr-labs/homarr/issues/2539)) ([fd5b1b0](https://github.com/homarr-labs/homarr/commit/fd5b1b0c082ee440c6c5137629ef1ec4eead23eb))
|
||||
* **deps:** update dependency @kubernetes/client-node to ^1.1.0 ([#2591](https://github.com/homarr-labs/homarr/issues/2591)) ([8a02c65](https://github.com/homarr-labs/homarr/commit/8a02c656488bf0f239b8f394d73ed63ade8f61ed))
|
||||
* **deps:** update dependency better-sqlite3 to ^11.9.0 ([#2598](https://github.com/homarr-labs/homarr/issues/2598)) ([04c17d1](https://github.com/homarr-labs/homarr/commit/04c17d1c41f0c44e88e39cdfe116972da32624b4))
|
||||
* **deps:** update dependency jotai to ^2.12.2 ([#2560](https://github.com/homarr-labs/homarr/issues/2560)) ([ff6ca86](https://github.com/homarr-labs/homarr/commit/ff6ca86a78ccec4a2dee3d08c1420948c4de28ee))
|
||||
* **deps:** update dependency next-intl to v4 ([#2580](https://github.com/homarr-labs/homarr/issues/2580)) ([f55d8a9](https://github.com/homarr-labs/homarr/commit/f55d8a9c2ebbc612775d66b1f073605fb9e151f3))
|
||||
* **deps:** update dependency next-intl to v4.0.1 ([#2585](https://github.com/homarr-labs/homarr/issues/2585)) ([d8247f2](https://github.com/homarr-labs/homarr/commit/d8247f2dc9a4aaa45d8e193db04295f51c44d4aa))
|
||||
* **deps:** update dependency next-intl to v4.0.2 ([#2589](https://github.com/homarr-labs/homarr/issues/2589)) ([b607c57](https://github.com/homarr-labs/homarr/commit/b607c5702fd2bf86be8b56d7b7b00435363095e0))
|
||||
* **deps:** update dependency prismjs to ^1.30.0 ([#2562](https://github.com/homarr-labs/homarr/issues/2562)) ([8cb8eaf](https://github.com/homarr-labs/homarr/commit/8cb8eaf9cc2aaa842869c6be75e9208b9a19fd77))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.20.1 ([#2558](https://github.com/homarr-labs/homarr/issues/2558)) ([9c582d9](https://github.com/homarr-labs/homarr/commit/9c582d906bb2702de5db282594293e51e94ca98d))
|
||||
* **deps:** update dependency typescript-eslint to ^8.26.1 ([#2561](https://github.com/homarr-labs/homarr/issues/2561)) ([55ad0b0](https://github.com/homarr-labs/homarr/commit/55ad0b0a6533df44e7d1f6eecf33135654cea104))
|
||||
* **deps:** update dependency undici to v7.5.0 ([#2583](https://github.com/homarr-labs/homarr/issues/2583)) ([56fe8dd](https://github.com/homarr-labs/homarr/commit/56fe8dd9e9ff14dc0c10a93f6798df11c2ef5761))
|
||||
* **deps:** update dependency video.js to ^8.22.0 ([#2564](https://github.com/homarr-labs/homarr/issues/2564)) ([a102f39](https://github.com/homarr-labs/homarr/commit/a102f39518722d898d75d978df91ae2a7f8da3e9))
|
||||
* **deps:** update dependency zod-form-data to ^2.0.6 ([#2572](https://github.com/homarr-labs/homarr/issues/2572)) ([2135f99](https://github.com/homarr-labs/homarr/commit/2135f9980b4eb9e682fd19d2a1536f50201a1bee))
|
||||
* **deps:** update dependency zod-form-data to ^2.0.7 ([#2573](https://github.com/homarr-labs/homarr/issues/2573)) ([6c9fbd6](https://github.com/homarr-labs/homarr/commit/6c9fbd6c09b481b24dad849cc8d367d0c5591cdb))
|
||||
* **deps:** update mantine monorepo to ^7.17.2 ([#2599](https://github.com/homarr-labs/homarr/issues/2599)) ([1e50ac4](https://github.com/homarr-labs/homarr/commit/1e50ac411c9e416de4225881b631e4cc497e7b0d))
|
||||
* **deps:** update tanstack-query monorepo to ^5.67.3 ([#2567](https://github.com/homarr-labs/homarr/issues/2567)) ([8486073](https://github.com/homarr-labs/homarr/commit/8486073f271c69f767d9cc76fdd3f27cf1f9db20))
|
||||
* **deps:** update tanstack-query monorepo to ^5.68.0 ([#2600](https://github.com/homarr-labs/homarr/issues/2600)) ([25f7406](https://github.com/homarr-labs/homarr/commit/25f7406283e2999b85fdc21953e0e6c572d8f23b))
|
||||
* **deps:** update testcontainers-node monorepo to ^10.19.0 ([#2576](https://github.com/homarr-labs/homarr/issues/2576)) ([d669f99](https://github.com/homarr-labs/homarr/commit/d669f99ac161697d6c3db81c51b7bf25532fbbfd))
|
||||
* **deps:** update testcontainers-node monorepo to ^10.20.0 ([#2602](https://github.com/homarr-labs/homarr/issues/2602)) ([75fb9db](https://github.com/homarr-labs/homarr/commit/75fb9dbf1e7c2137d1dd98bc2952ddb28fc0375f))
|
||||
* **dns-controls:** improve responsive styles ([#2534](https://github.com/homarr-labs/homarr/issues/2534)) ([c0e1355](https://github.com/homarr-labs/homarr/commit/c0e1355d7f995e79eec23680568f1830e5f03542))
|
||||
* **dns-hole-summary:** remove inconsistency for position of text ([#2568](https://github.com/homarr-labs/homarr/issues/2568)) ([e774b06](https://github.com/homarr-labs/homarr/commit/e774b06fc2173eb094e2b698a93624a54cd37dff))
|
||||
* **dns-summary:** improve responsive styles ([#2535](https://github.com/homarr-labs/homarr/issues/2535)) ([7ec6fbc](https://github.com/homarr-labs/homarr/commit/7ec6fbc3765491a810356948a294309ab606bdce))
|
||||
* **downloads:** improve responsive styles ([#2552](https://github.com/homarr-labs/homarr/issues/2552)) ([d714e53](https://github.com/homarr-labs/homarr/commit/d714e53cfa88da22a6fc75473455d57be7bac497))
|
||||
* **import:** autofix missing sizes of items with grid-algorithm ([#2522](https://github.com/homarr-labs/homarr/issues/2522)) ([d410af3](https://github.com/homarr-labs/homarr/commit/d410af34029a43e6e11cd766f55c634b00eb8fa2))
|
||||
* **indexer-manager:** improve responsive styles ([#2541](https://github.com/homarr-labs/homarr/issues/2541)) ([4472c04](https://github.com/homarr-labs/homarr/commit/4472c04967f0a3799507785b02f96a84c9626465))
|
||||
* **media-request-list:** improve responsive styles ([#2543](https://github.com/homarr-labs/homarr/issues/2543)) ([9e48f43](https://github.com/homarr-labs/homarr/commit/9e48f438b4550f542130651ea7dccccc36b59da8))
|
||||
* **media-request-stats:** improve responsive styles ([#2544](https://github.com/homarr-labs/homarr/issues/2544)) ([d584ade](https://github.com/homarr-labs/homarr/commit/d584ade8f4a788b284071cb9d17b6d8b606098c8))
|
||||
* **media-server:** improve responsive styles ([#2548](https://github.com/homarr-labs/homarr/issues/2548)) ([82528db](https://github.com/homarr-labs/homarr/commit/82528dbd961b21682d71b104b22804391b6c7d08))
|
||||
* **media-transcoding:** improve responsive styles ([#2550](https://github.com/homarr-labs/homarr/issues/2550)) ([8f8d788](https://github.com/homarr-labs/homarr/commit/8f8d7884a9484aa8e73f4ff114e58df78a8bb470))
|
||||
* **minecraft-status:** wrong api path for bedrock ([#2551](https://github.com/homarr-labs/homarr/issues/2551)) ([2f80c09](https://github.com/homarr-labs/homarr/commit/2f80c096a7e25b1a223336644f40a56311e1c7a8))
|
||||
* **notebook:** css for checkboxes not working ([#2570](https://github.com/homarr-labs/homarr/issues/2570)) ([a53e7aa](https://github.com/homarr-labs/homarr/commit/a53e7aaee5207ab563b6e0144f3154db917f7da1))
|
||||
* **smart-home:** improve responsive styles ([#2540](https://github.com/homarr-labs/homarr/issues/2540)) ([0b07f22](https://github.com/homarr-labs/homarr/commit/0b07f227ee72988fe1cbd37a9a6a8751b942a087))
|
||||
* **system-health:** improve responsive styles ([#2566](https://github.com/homarr-labs/homarr/issues/2566)) ([268daee](https://github.com/homarr-labs/homarr/commit/268daee4a6e90052a19b9d2426c2cf8b2c8a0c00))
|
||||
|
||||
## [1.10.0](https://github.com/homarr-labs/homarr/compare/v1.9.0...v1.10.0) (2025-03-07)
|
||||
|
||||
### Features
|
||||
|
||||
* remove cqmin system ([#2407](https://github.com/homarr-labs/homarr/issues/2407)) ([37d4714](https://github.com/homarr-labs/homarr/commit/37d471457a0a2b10112b67c02923f63de479e617))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency eslint-config-prettier to ^10.1.1 ([#2524](https://github.com/homarr-labs/homarr/issues/2524)) ([4721a97](https://github.com/homarr-labs/homarr/commit/4721a97094ae1181216923c7fef7a29c343f882b))
|
||||
* **deps:** update dependency ioredis to v5.6.0 ([#2509](https://github.com/homarr-labs/homarr/issues/2509)) ([e4a3922](https://github.com/homarr-labs/homarr/commit/e4a39223a33d4bfccb1e9b4e2c30de1376879a1d))
|
||||
* **deps:** update dependency mysql2 to v3.13.0 ([#2516](https://github.com/homarr-labs/homarr/issues/2516)) ([67c739c](https://github.com/homarr-labs/homarr/commit/67c739c35bcb2d537a349f0ebff080eac67e93b5))
|
||||
* **deps:** update tanstack-query monorepo to ^5.67.2 ([#2525](https://github.com/homarr-labs/homarr/issues/2525)) ([a572aae](https://github.com/homarr-labs/homarr/commit/a572aae887142a7e601f70dcc601bf6335e37c17))
|
||||
* **pi-hole:** wrong fetch used in factory ([#2527](https://github.com/homarr-labs/homarr/issues/2527)) ([9881577](https://github.com/homarr-labs/homarr/commit/9881577f47fbeacf861dd564d57fc6d7c469d27b))
|
||||
|
||||
## [1.9.0](https://github.com/homarr-labs/homarr/compare/v1.8.0...v1.9.0) (2025-03-05)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -10,7 +10,12 @@ import MillionLint from "@million/lint";
|
||||
import createNextIntlPlugin from "next-intl/plugin";
|
||||
|
||||
// Package path does not work... so we need to use relative path
|
||||
const withNextIntl = createNextIntlPlugin("../../packages/translation/src/request.ts");
|
||||
const withNextIntl = createNextIntlPlugin({
|
||||
experimental: {
|
||||
createMessagesDeclaration: "../../packages/translation/src/lang/en.json",
|
||||
},
|
||||
requestConfig: "../../packages/translation/src/request.ts",
|
||||
});
|
||||
|
||||
interface WebpackConfig {
|
||||
module: {
|
||||
|
||||
@@ -48,21 +48,21 @@
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@homarr/widgets": "workspace:^0.1.0",
|
||||
"@mantine/colors-generator": "^7.17.1",
|
||||
"@mantine/core": "^7.17.1",
|
||||
"@mantine/dropzone": "^7.17.1",
|
||||
"@mantine/hooks": "^7.17.1",
|
||||
"@mantine/modals": "^7.17.1",
|
||||
"@mantine/tiptap": "^7.17.1",
|
||||
"@mantine/colors-generator": "^7.17.3",
|
||||
"@mantine/core": "^7.17.3",
|
||||
"@mantine/dropzone": "^7.17.3",
|
||||
"@mantine/hooks": "^7.17.3",
|
||||
"@mantine/modals": "^7.17.3",
|
||||
"@mantine/tiptap": "^7.17.3",
|
||||
"@million/lint": "1.0.14",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@tanstack/react-query": "^5.67.1",
|
||||
"@tanstack/react-query-devtools": "^5.67.1",
|
||||
"@tanstack/react-query-next-experimental": "^5.67.1",
|
||||
"@trpc/client": "next",
|
||||
"@trpc/next": "next",
|
||||
"@trpc/react-query": "next",
|
||||
"@trpc/server": "next",
|
||||
"@tanstack/react-query": "^5.70.0",
|
||||
"@tanstack/react-query-devtools": "^5.70.0",
|
||||
"@tanstack/react-query-next-experimental": "^5.70.0",
|
||||
"@trpc/client": "^11.0.1",
|
||||
"@trpc/next": "^11.0.1",
|
||||
"@trpc/react-query": "^11.0.1",
|
||||
"@trpc/server": "^11.0.1",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
@@ -72,18 +72,18 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"flag-icons": "^7.3.2",
|
||||
"glob": "^11.0.1",
|
||||
"jotai": "^2.12.1",
|
||||
"jotai": "^2.12.2",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "15.1.7",
|
||||
"next": "15.2.4",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"sass": "^1.85.1",
|
||||
"sass": "^1.86.0",
|
||||
"superjson": "2.2.2",
|
||||
"swagger-ui-react": "^5.20.0",
|
||||
"swagger-ui-react": "^5.20.2",
|
||||
"use-deep-compare-effect": "^1.8.1",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
@@ -92,13 +92,13 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/chroma-js": "3.1.1",
|
||||
"@types/node": "^22.13.9",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react": "19.0.12",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint": "^9.23.0",
|
||||
"node-loader": "^2.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.2"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 139.3 512.2 233.5"><path d="M256 139.3c-53 0-97.9 35.8-112.1 84.4-12.2-25.5-38.2-43.4-68.2-43.4C34.2 180.2 0 214.4 0 256s34.2 75.8 75.8 75.8c30 0 55.9-17.9 68.2-43.4 14.1 48.6 59.1 84.4 112.1 84.4S354 337 368.2 288.4c12.2 25.5 38.2 43.4 68.2 43.4 41.6 0 75.8-34.2 75.8-75.8s-34.2-75.8-75.8-75.8c-30 0-55.9 17.9-68.2 43.4-14.3-48.5-59.2-84.3-112.2-84.3m0 45c39.9 0 71.7 31.8 71.7 71.7s-31.8 71.7-71.7 71.7-71.7-31.8-71.7-71.7 31.8-71.7 71.7-71.7m-180.2 41c17.2 0 30.7 13.5 30.7 30.7S93 286.7 75.8 286.7 45.1 273.2 45.1 256s13.4-30.7 30.7-30.7m360.4 0c17.2 0 30.7 13.5 30.7 30.7s-13.5 30.7-30.7 30.7-30.7-13.5-30.7-30.7 13.5-30.7 30.7-30.7" style="fill:#3784c9"/></svg>
|
||||
|
After Width: | Height: | Size: 739 B |
@@ -0,0 +1,141 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="18.035334mm"
|
||||
height="17.500378mm"
|
||||
viewBox="0 0 18.035334 17.500378"
|
||||
version="1.1"
|
||||
id="svg13826"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="cm.svg">
|
||||
<defs
|
||||
id="defs13820" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="8"
|
||||
inkscape:cx="-2.090004"
|
||||
inkscape:cy="33.752239"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="771"
|
||||
inkscape:window-x="1"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<metadata
|
||||
id="metadata13823">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-0.99262638,-1.174181)">
|
||||
<g
|
||||
id="g70"
|
||||
transform="matrix(1.0148887,0,0,1.0148887,16.902146,-2.698726)">
|
||||
<path
|
||||
inkscape:export-ydpi="250.55"
|
||||
inkscape:export-xdpi="250.55"
|
||||
inkscape:export-filename="new.png"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3055"
|
||||
d="m -6.8492015,4.2724668 a 1.1191255,1.1099671 0 0 0 -0.4288818,0.1085303 l -5.8524037,2.7963394 a 1.1191255,1.1099671 0 0 0 -0.605524,0.7529759 l -1.443828,6.2812846 a 1.1191255,1.1099671 0 0 0 0.151943,0.851028 1.1191255,1.1099671 0 0 0 0.06362,0.08832 l 4.0508,5.036555 a 1.1191255,1.1099671 0 0 0 0.874979,0.417654 l 6.4961011,-0.0015 a 1.1191255,1.1099671 0 0 0 0.8749788,-0.416906 L 1.3818872,15.149453 A 1.1191255,1.1099671 0 0 0 1.5981986,14.210104 L 0.15212657,7.9288154 A 1.1191255,1.1099671 0 0 0 -0.45339794,7.1758396 L -6.3065496,4.3809971 A 1.1191255,1.1099671 0 0 0 -6.8492015,4.2724668 Z"
|
||||
style="fill:#326ce5;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
id="path3054-2-9"
|
||||
d="M -6.8523435,3.8176372 A 1.1814304,1.171762 0 0 0 -7.3044284,3.932904 l -6.1787426,2.9512758 a 1.1814304,1.171762 0 0 0 -0.639206,0.794891 l -1.523915,6.6308282 a 1.1814304,1.171762 0 0 0 0.160175,0.89893 1.1814304,1.171762 0 0 0 0.06736,0.09281 l 4.276094,5.317236 a 1.1814304,1.171762 0 0 0 0.92363,0.440858 l 6.8576188,-0.0015 a 1.1814304,1.171762 0 0 0 0.9236308,-0.44011 l 4.2745966,-5.317985 a 1.1814304,1.171762 0 0 0 0.228288,-0.990993 L 0.53894439,7.6775738 A 1.1814304,1.171762 0 0 0 -0.10026101,6.8834313 L -6.2790037,3.9321555 A 1.1814304,1.171762 0 0 0 -6.8523435,3.8176372 Z m 0.00299,0.4550789 a 1.1191255,1.1099671 0 0 1 0.5426517,0.1085303 l 5.85315169,2.7948425 A 1.1191255,1.1099671 0 0 1 0.15197811,7.9290648 L 1.598051,14.21035 a 1.1191255,1.1099671 0 0 1 -0.2163123,0.939348 l -4.0493032,5.037304 a 1.1191255,1.1099671 0 0 1 -0.8749789,0.416906 l -6.4961006,0.0015 a 1.1191255,1.1099671 0 0 1 -0.874979,-0.417652 l -4.0508,-5.036554 a 1.1191255,1.1099671 0 0 1 -0.06362,-0.08832 1.1191255,1.1099671 0 0 1 -0.151942,-0.851028 l 1.443827,-6.2812853 a 1.1191255,1.1099671 0 0 1 0.605524,-0.7529758 l 5.8524036,-2.7963395 a 1.1191255,1.1099671 0 0 1 0.4288819,-0.1085303 z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;marker:none;enable-background:accumulate"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<g
|
||||
id="g3349"
|
||||
transform="translate(0.11778981,0.45794291)">
|
||||
<path
|
||||
inkscape:export-ydpi="376.57999"
|
||||
inkscape:export-xdpi="376.57999"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.79374999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 8.236948,6.2914262 5.825001,0"
|
||||
id="path876" />
|
||||
<path
|
||||
inkscape:export-ydpi="376.57999"
|
||||
inkscape:export-xdpi="376.57999"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.79374999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 5.723058,6.2914262 1.45834,0"
|
||||
id="path880" />
|
||||
<a
|
||||
id="a3346">
|
||||
<path
|
||||
id="path884"
|
||||
d="m 10.353619,8.4080928 3.70833,0"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.79374999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:export-xdpi="376.57999"
|
||||
inkscape:export-ydpi="376.57999" />
|
||||
</a>
|
||||
<path
|
||||
inkscape:export-ydpi="376.57999"
|
||||
inkscape:export-xdpi="376.57999"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.79374999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 7.839728,8.4080928 1.4583305,0"
|
||||
id="path888" />
|
||||
<path
|
||||
inkscape:export-ydpi="376.57999"
|
||||
inkscape:export-xdpi="376.57999"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.79374999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 10.353619,10.52476 3.70833,0"
|
||||
id="path892" />
|
||||
<path
|
||||
inkscape:export-ydpi="376.57999"
|
||||
inkscape:export-xdpi="376.57999"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.79374999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 7.839728,10.52476 1.4583305,0"
|
||||
id="path896" />
|
||||
<path
|
||||
inkscape:export-ydpi="376.57999"
|
||||
inkscape:export-xdpi="376.57999"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.79374999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 8.236948,12.641428 5.825001,0"
|
||||
id="path900" />
|
||||
<path
|
||||
inkscape:export-ydpi="376.57999"
|
||||
inkscape:export-xdpi="376.57999"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.79374999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 5.723058,12.641428 1.45834,0"
|
||||
id="path904" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="18.035334mm"
|
||||
height="17.500378mm"
|
||||
viewBox="0 0 18.035334 17.500378"
|
||||
version="1.1"
|
||||
id="svg13826"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="ing.svg">
|
||||
<defs
|
||||
id="defs13820" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="8"
|
||||
inkscape:cx="-2.090004"
|
||||
inkscape:cy="28.752239"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="775"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="1"
|
||||
inkscape:window-maximized="1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<metadata
|
||||
id="metadata13823">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-0.99262638,-1.174181)">
|
||||
<g
|
||||
id="g70"
|
||||
transform="matrix(1.0148887,0,0,1.0148887,16.902146,-2.698726)">
|
||||
<path
|
||||
inkscape:export-ydpi="250.55"
|
||||
inkscape:export-xdpi="250.55"
|
||||
inkscape:export-filename="new.png"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3055"
|
||||
d="m -6.8492015,4.2724668 a 1.1191255,1.1099671 0 0 0 -0.4288818,0.1085303 l -5.8524037,2.7963394 a 1.1191255,1.1099671 0 0 0 -0.605524,0.7529759 l -1.443828,6.2812846 a 1.1191255,1.1099671 0 0 0 0.151943,0.851028 1.1191255,1.1099671 0 0 0 0.06362,0.08832 l 4.0508,5.036555 a 1.1191255,1.1099671 0 0 0 0.874979,0.417654 l 6.4961011,-0.0015 a 1.1191255,1.1099671 0 0 0 0.8749788,-0.416906 L 1.3818872,15.149453 A 1.1191255,1.1099671 0 0 0 1.5981986,14.210104 L 0.15212657,7.9288154 A 1.1191255,1.1099671 0 0 0 -0.45339794,7.1758396 L -6.3065496,4.3809971 A 1.1191255,1.1099671 0 0 0 -6.8492015,4.2724668 Z"
|
||||
style="fill:#326ce5;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
id="path3054-2-9"
|
||||
d="M -6.8523435,3.8176372 A 1.1814304,1.171762 0 0 0 -7.3044284,3.932904 l -6.1787426,2.9512758 a 1.1814304,1.171762 0 0 0 -0.639206,0.794891 l -1.523915,6.6308282 a 1.1814304,1.171762 0 0 0 0.160175,0.89893 1.1814304,1.171762 0 0 0 0.06736,0.09281 l 4.276094,5.317236 a 1.1814304,1.171762 0 0 0 0.92363,0.440858 l 6.8576188,-0.0015 a 1.1814304,1.171762 0 0 0 0.9236308,-0.44011 l 4.2745966,-5.317985 a 1.1814304,1.171762 0 0 0 0.228288,-0.990993 L 0.53894439,7.6775738 A 1.1814304,1.171762 0 0 0 -0.10026101,6.8834313 L -6.2790037,3.9321555 A 1.1814304,1.171762 0 0 0 -6.8523435,3.8176372 Z m 0.00299,0.4550789 a 1.1191255,1.1099671 0 0 1 0.5426517,0.1085303 l 5.85315169,2.7948425 A 1.1191255,1.1099671 0 0 1 0.15197811,7.9290648 L 1.598051,14.21035 a 1.1191255,1.1099671 0 0 1 -0.2163123,0.939348 l -4.0493032,5.037304 a 1.1191255,1.1099671 0 0 1 -0.8749789,0.416906 l -6.4961006,0.0015 a 1.1191255,1.1099671 0 0 1 -0.874979,-0.417652 l -4.0508,-5.036554 a 1.1191255,1.1099671 0 0 1 -0.06362,-0.08832 1.1191255,1.1099671 0 0 1 -0.151942,-0.851028 l 1.443827,-6.2812853 a 1.1191255,1.1099671 0 0 1 0.605524,-0.7529758 l 5.8524036,-2.7963395 a 1.1191255,1.1099671 0 0 1 0.4288819,-0.1085303 z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;marker:none;enable-background:accumulate"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<path
|
||||
id="path7709"
|
||||
pointer-events="none"
|
||||
d="m 12.75799,13.997178 -2.270701,0 -4.9209009,-6.1558617 -1.42366,0 0,-2.0149069 2.31473,0 4.9230119,6.1558536 1.37752,0 0,-1.593474 3.119869,2.599882 -3.119869,2.601983 z m -2.47616,-4.7552751 1.09864,-1.3754256 1.37752,0 0,1.593475 3.119869,-2.5998829 -3.119869,-2.601983 0,1.593483 -2.270701,0 -1.4571904,1.8241102 z m -3.5979219,1.3649431 -1.11752,1.400578 -1.42366,0 0,2.014915 2.31473,0 1.4781699,-1.849278 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.20966817" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="18.035334mm"
|
||||
height="17.500378mm"
|
||||
viewBox="0 0 18.035334 17.500378"
|
||||
version="1.1"
|
||||
id="svg13826"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="ns.svg">
|
||||
<defs
|
||||
id="defs13820" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="8"
|
||||
inkscape:cx="-2.090004"
|
||||
inkscape:cy="23.752239"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="775"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="1"
|
||||
inkscape:window-maximized="1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<metadata
|
||||
id="metadata13823">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-0.99262638,-1.174181)">
|
||||
<g
|
||||
id="g70"
|
||||
transform="matrix(1.0148887,0,0,1.0148887,16.902146,-2.698726)">
|
||||
<path
|
||||
inkscape:export-ydpi="250.55"
|
||||
inkscape:export-xdpi="250.55"
|
||||
inkscape:export-filename="new.png"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3055"
|
||||
d="m -6.8492015,4.2724668 a 1.1191255,1.1099671 0 0 0 -0.4288818,0.1085303 l -5.8524037,2.7963394 a 1.1191255,1.1099671 0 0 0 -0.605524,0.7529759 l -1.443828,6.2812846 a 1.1191255,1.1099671 0 0 0 0.151943,0.851028 1.1191255,1.1099671 0 0 0 0.06362,0.08832 l 4.0508,5.036555 a 1.1191255,1.1099671 0 0 0 0.874979,0.417654 l 6.4961011,-0.0015 a 1.1191255,1.1099671 0 0 0 0.8749788,-0.416906 L 1.3818872,15.149453 A 1.1191255,1.1099671 0 0 0 1.5981986,14.210104 L 0.15212657,7.9288154 A 1.1191255,1.1099671 0 0 0 -0.45339794,7.1758396 L -6.3065496,4.3809971 A 1.1191255,1.1099671 0 0 0 -6.8492015,4.2724668 Z"
|
||||
style="fill:#326ce5;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
id="path3054-2-9"
|
||||
d="M -6.8523435,3.8176372 A 1.1814304,1.171762 0 0 0 -7.3044284,3.932904 l -6.1787426,2.9512758 a 1.1814304,1.171762 0 0 0 -0.639206,0.794891 l -1.523915,6.6308282 a 1.1814304,1.171762 0 0 0 0.160175,0.89893 1.1814304,1.171762 0 0 0 0.06736,0.09281 l 4.276094,5.317236 a 1.1814304,1.171762 0 0 0 0.92363,0.440858 l 6.8576188,-0.0015 a 1.1814304,1.171762 0 0 0 0.9236308,-0.44011 l 4.2745966,-5.317985 a 1.1814304,1.171762 0 0 0 0.228288,-0.990993 L 0.53894439,7.6775738 A 1.1814304,1.171762 0 0 0 -0.10026101,6.8834313 L -6.2790037,3.9321555 A 1.1814304,1.171762 0 0 0 -6.8523435,3.8176372 Z m 0.00299,0.4550789 a 1.1191255,1.1099671 0 0 1 0.5426517,0.1085303 l 5.85315169,2.7948425 A 1.1191255,1.1099671 0 0 1 0.15197811,7.9290648 L 1.598051,14.21035 a 1.1191255,1.1099671 0 0 1 -0.2163123,0.939348 l -4.0493032,5.037304 a 1.1191255,1.1099671 0 0 1 -0.8749789,0.416906 l -6.4961006,0.0015 a 1.1191255,1.1099671 0 0 1 -0.874979,-0.417652 l -4.0508,-5.036554 a 1.1191255,1.1099671 0 0 1 -0.06362,-0.08832 1.1191255,1.1099671 0 0 1 -0.151942,-0.851028 l 1.443827,-6.2812853 a 1.1191255,1.1099671 0 0 1 0.605524,-0.7529758 l 5.8524036,-2.7963395 a 1.1191255,1.1099671 0 0 1 0.4288819,-0.1085303 z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;marker:none;enable-background:accumulate"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<rect
|
||||
y="6.5793304"
|
||||
x="6.1734986"
|
||||
height="6.6900792"
|
||||
width="7.6735892"
|
||||
id="rect8790"
|
||||
style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.40000001;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:0.80000001, 0.4;stroke-dashoffset:3.44000006;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="18.035334mm"
|
||||
height="17.500378mm"
|
||||
viewBox="0 0 18.035334 17.500378"
|
||||
version="1.1"
|
||||
id="svg13826"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="node.svg">
|
||||
<defs
|
||||
id="defs13820" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="8"
|
||||
inkscape:cx="16.847496"
|
||||
inkscape:cy="33.752239"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="775"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="1"
|
||||
inkscape:window-maximized="1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<metadata
|
||||
id="metadata13823">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-0.99262638,-1.174181)">
|
||||
<g
|
||||
id="g70"
|
||||
transform="matrix(1.0148887,0,0,1.0148887,16.902146,-2.698726)">
|
||||
<path
|
||||
inkscape:export-ydpi="250.55"
|
||||
inkscape:export-xdpi="250.55"
|
||||
inkscape:export-filename="new.png"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3055"
|
||||
d="m -6.8492015,4.2724668 a 1.1191255,1.1099671 0 0 0 -0.4288818,0.1085303 l -5.8524037,2.7963394 a 1.1191255,1.1099671 0 0 0 -0.605524,0.7529759 l -1.443828,6.2812846 a 1.1191255,1.1099671 0 0 0 0.151943,0.851028 1.1191255,1.1099671 0 0 0 0.06362,0.08832 l 4.0508,5.036555 a 1.1191255,1.1099671 0 0 0 0.874979,0.417654 l 6.4961011,-0.0015 a 1.1191255,1.1099671 0 0 0 0.8749788,-0.416906 L 1.3818872,15.149453 A 1.1191255,1.1099671 0 0 0 1.5981986,14.210104 L 0.15212657,7.9288154 A 1.1191255,1.1099671 0 0 0 -0.45339794,7.1758396 L -6.3065496,4.3809971 A 1.1191255,1.1099671 0 0 0 -6.8492015,4.2724668 Z"
|
||||
style="fill:#326ce5;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
id="path3054-2-9"
|
||||
d="M -6.8523435,3.8176372 A 1.1814304,1.171762 0 0 0 -7.3044284,3.932904 l -6.1787426,2.9512758 a 1.1814304,1.171762 0 0 0 -0.639206,0.794891 l -1.523915,6.6308282 a 1.1814304,1.171762 0 0 0 0.160175,0.89893 1.1814304,1.171762 0 0 0 0.06736,0.09281 l 4.276094,5.317236 a 1.1814304,1.171762 0 0 0 0.92363,0.440858 l 6.8576188,-0.0015 a 1.1814304,1.171762 0 0 0 0.9236308,-0.44011 l 4.2745966,-5.317985 a 1.1814304,1.171762 0 0 0 0.228288,-0.990993 L 0.53894439,7.6775738 A 1.1814304,1.171762 0 0 0 -0.10026101,6.8834313 L -6.2790037,3.9321555 A 1.1814304,1.171762 0 0 0 -6.8523435,3.8176372 Z m 0.00299,0.4550789 a 1.1191255,1.1099671 0 0 1 0.5426517,0.1085303 l 5.85315169,2.7948425 A 1.1191255,1.1099671 0 0 1 0.15197811,7.9290648 L 1.598051,14.21035 a 1.1191255,1.1099671 0 0 1 -0.2163123,0.939348 l -4.0493032,5.037304 a 1.1191255,1.1099671 0 0 1 -0.8749789,0.416906 l -6.4961006,0.0015 a 1.1191255,1.1099671 0 0 1 -0.874979,-0.417652 l -4.0508,-5.036554 a 1.1191255,1.1099671 0 0 1 -0.06362,-0.08832 1.1191255,1.1099671 0 0 1 -0.151942,-0.851028 l 1.443827,-6.2812853 a 1.1191255,1.1099671 0 0 1 0.605524,-0.7529758 l 5.8524036,-2.7963395 a 1.1191255,1.1099671 0 0 1 0.4288819,-0.1085303 z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;marker:none;enable-background:accumulate"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<path
|
||||
sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccscccccccccccccccc"
|
||||
id="path1994"
|
||||
d="M 9.9921803,5.0101759 C 9.8538505,5.0057759 5.9970805,6.902049 5.9466504,6.999295 c -0.12117,0.233682 -0.9989,4.281827 -0.94731,4.369074 0.03,0.05065 0.66219,0.851861 1.40458,1.780327 l 1.3498201,1.688014 2.2211901,9.31e-4 2.2216404,9.31e-4 1.41321,-1.765731 1.41365,-1.765228 -0.49479,-2.1685759 C 14.256441,7.946271 14.012981,6.950423 13.988111,6.926433 13.918611,6.859553 10.067151,5.0126389 9.9921803,5.0102179 Z m 0.1961407,0.947753 0.90893,0.2635771 -0.90893,0.263576 -0.9089209,-0.263576 z m -0.9089209,0.36452 0.8511209,0.2532261 -0.004,1.183289 -0.8468109,-0.469347 z m 1.8178509,0 0,0.9671681 -0.84679,0.469347 -0.004,-1.183289 z M 8.8997705,7.937127 9.8087101,8.2007027 8.8997705,8.4642797 7.9908504,8.2007027 Z m 2.2087005,0 0.90894,0.2635757 -0.90894,0.263577 -0.90893,-0.263577 z m -3.1176206,0.3645197 0.8511202,0.252792 -0.004,1.1832908 -0.8468199,-0.468915 z m 1.8178597,0 0,0.9671678 -0.8468098,0.468915 -0.004,-1.1832908 z m 0.3908309,0 0.85113,0.252792 -0.004,1.1832908 -0.84682,-0.468915 z m 1.81787,0 0,0.9671678 -0.84682,0.468915 -0.004,-1.1832908 z m -3.4496605,1.515028 c 0.2706299,0.0096 0.0611,0.2819093 0.3684101,0.4279353 0.3277495,0.155764 0.3953995,-0.235354 0.6013395,0.06341 0.20599,0.298766 -0.18339,0.223239 -0.15443,0.584957 0.029,0.361718 0.40165,0.224858 0.24589,0.552606 -0.15575,0.327746 -0.28532,-0.04764 -0.5840895,0.158317 -0.2987401,0.205957 0.006,0.460208 -0.35546,0.489192 -0.3617401,0.02898 -0.1015001,-0.270447 -0.42924,-0.426208 -0.32775,-0.155765 -0.3953801,0.234921 -0.6013402,-0.06385 -0.2059599,-0.298767 0.1838299,-0.22281 0.15485,-0.584528 -0.029,-0.361718 -0.4016499,-0.224859 -0.24587,-0.552604 0.1557501,-0.327749 0.2848801,0.04764 0.5836502,-0.15832 0.2987898,-0.205956 -0.006,-0.4602083 0.3559099,-0.4891903 0.022499,-0.0018 0.0424,-0.0023 0.0604,-0.0018 z m 2.3359605,0.3627943 c 0.48335,0.01358 0.0146,0.467218 0.45596,0.664766 0.44144,0.197548 0.46714,-0.454103 0.79937,-0.102669 0.33221,0.351432 -0.31997,0.34064 -0.14753,0.792455 0.17243,0.451813 0.65163,0.0092 0.63802,0.49264 -0.0137,0.483411 -0.46723,0.01456 -0.66477,0.455977 -0.19755,0.441412 0.4541,0.467143 0.10266,0.799357 -0.35141,0.332212 -0.34021,-0.319974 -0.79202,-0.147534 -0.45183,0.172437 -0.009,0.65161 -0.49265,0.638019 -0.48339,-0.01358 -0.0146,-0.467216 -0.45596,-0.664764 -0.4414105,-0.197551 -0.4675805,0.454102 -0.7997909,0.102669 -0.3322097,-0.351431 0.3199804,-0.340209 0.14754,-0.792025 -0.17245,-0.451815 -0.6516296,-0.0092 -0.6380295,-0.492642 0.013699,-0.483408 0.4672095,-0.01499 0.6647795,-0.456405 0.1975204,-0.441414 -0.45411,-0.467143 -0.10269,-0.799357 0.3514505,-0.332213 0.3406505,0.319971 0.7924609,0.147534 0.45184,-0.17244 0.009,-0.651611 0.49265,-0.638021 z m -2.3148207,0.253655 c -0.2936499,9e-5 -0.5316098,0.238249 -0.5314498,0.531898 7.99e-5,0.293481 0.2379701,0.531377 0.5314498,0.531467 0.2936602,1.59e-4 0.5318202,-0.23781 0.5319,-0.531467 1.601e-4,-0.293825 -0.2380699,-0.532057 -0.5319,-0.531898 z m 2.2643607,0.480564 c -0.58689,-1.96e-4 -1.0627109,0.475614 -1.0625209,1.062502 5e-5,0.586719 0.4758009,1.062267 1.0625209,1.062071 0.58654,-5.8e-5 1.06201,-0.475531 1.06206,-1.062071 1.9e-4,-0.586708 -0.47535,-1.062444 -1.06206,-1.062502 z"
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#eeeeee;stroke-width:0;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:11.23642349;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="18.035334mm"
|
||||
height="17.500378mm"
|
||||
viewBox="0 0 18.035334 17.500378"
|
||||
version="1.1"
|
||||
id="svg13826"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="pod.svg">
|
||||
<defs
|
||||
id="defs13820" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="8"
|
||||
inkscape:cx="-2.090004"
|
||||
inkscape:cy="33.752239"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="775"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="1"
|
||||
inkscape:window-maximized="1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<metadata
|
||||
id="metadata13823">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-0.99262638,-1.174181)">
|
||||
<g
|
||||
id="g70"
|
||||
transform="matrix(1.0148887,0,0,1.0148887,16.902146,-2.698726)">
|
||||
<path
|
||||
inkscape:export-ydpi="250.55"
|
||||
inkscape:export-xdpi="250.55"
|
||||
inkscape:export-filename="new.png"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3055"
|
||||
d="m -6.8492015,4.2724668 a 1.1191255,1.1099671 0 0 0 -0.4288818,0.1085303 l -5.8524037,2.7963394 a 1.1191255,1.1099671 0 0 0 -0.605524,0.7529759 l -1.443828,6.2812846 a 1.1191255,1.1099671 0 0 0 0.151943,0.851028 1.1191255,1.1099671 0 0 0 0.06362,0.08832 l 4.0508,5.036555 a 1.1191255,1.1099671 0 0 0 0.874979,0.417654 l 6.4961011,-0.0015 a 1.1191255,1.1099671 0 0 0 0.8749788,-0.416906 L 1.3818872,15.149453 A 1.1191255,1.1099671 0 0 0 1.5981986,14.210104 L 0.15212657,7.9288154 A 1.1191255,1.1099671 0 0 0 -0.45339794,7.1758396 L -6.3065496,4.3809971 A 1.1191255,1.1099671 0 0 0 -6.8492015,4.2724668 Z"
|
||||
style="fill:#326ce5;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
id="path3054-2-9"
|
||||
d="M -6.8523435,3.8176372 A 1.1814304,1.171762 0 0 0 -7.3044284,3.932904 l -6.1787426,2.9512758 a 1.1814304,1.171762 0 0 0 -0.639206,0.794891 l -1.523915,6.6308282 a 1.1814304,1.171762 0 0 0 0.160175,0.89893 1.1814304,1.171762 0 0 0 0.06736,0.09281 l 4.276094,5.317236 a 1.1814304,1.171762 0 0 0 0.92363,0.440858 l 6.8576188,-0.0015 a 1.1814304,1.171762 0 0 0 0.9236308,-0.44011 l 4.2745966,-5.317985 a 1.1814304,1.171762 0 0 0 0.228288,-0.990993 L 0.53894439,7.6775738 A 1.1814304,1.171762 0 0 0 -0.10026101,6.8834313 L -6.2790037,3.9321555 A 1.1814304,1.171762 0 0 0 -6.8523435,3.8176372 Z m 0.00299,0.4550789 a 1.1191255,1.1099671 0 0 1 0.5426517,0.1085303 l 5.85315169,2.7948425 A 1.1191255,1.1099671 0 0 1 0.15197811,7.9290648 L 1.598051,14.21035 a 1.1191255,1.1099671 0 0 1 -0.2163123,0.939348 l -4.0493032,5.037304 a 1.1191255,1.1099671 0 0 1 -0.8749789,0.416906 l -6.4961006,0.0015 a 1.1191255,1.1099671 0 0 1 -0.874979,-0.417652 l -4.0508,-5.036554 a 1.1191255,1.1099671 0 0 1 -0.06362,-0.08832 1.1191255,1.1099671 0 0 1 -0.151942,-0.851028 l 1.443827,-6.2812853 a 1.1191255,1.1099671 0 0 1 0.605524,-0.7529758 l 5.8524036,-2.7963395 a 1.1191255,1.1099671 0 0 1 0.4288819,-0.1085303 z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;marker:none;enable-background:accumulate"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<g
|
||||
id="g3341"
|
||||
transform="translate(0.12766661,0.35147801)">
|
||||
<path
|
||||
inkscape:export-ydpi="376.57999"
|
||||
inkscape:export-xdpi="376.57999"
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:0.26458332;stroke-linecap:square;stroke-miterlimit:10"
|
||||
inkscape:connector-curvature="0"
|
||||
d="M 6.2617914,7.036086 9.8826317,5.986087 13.503462,7.036086 9.8826317,8.086087 Z"
|
||||
id="path910" />
|
||||
<path
|
||||
inkscape:export-ydpi="376.57999"
|
||||
inkscape:export-xdpi="376.57999"
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:0.26458332;stroke-linecap:square;stroke-miterlimit:10"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 6.2617914,7.43817 0,3.852778 3.3736103,1.868749 0.0167,-4.713193 z"
|
||||
id="path912" />
|
||||
<path
|
||||
inkscape:export-ydpi="376.57999"
|
||||
inkscape:export-xdpi="376.57999"
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:0.26458332;stroke-linecap:square;stroke-miterlimit:10"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 13.503462,7.43817 0,3.852778 -3.37361,1.868749 -0.0167,-4.713193 z"
|
||||
id="path914" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
@@ -0,0 +1,128 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="18.035334mm"
|
||||
height="17.500378mm"
|
||||
viewBox="0 0 18.035334 17.500378"
|
||||
version="1.1"
|
||||
id="svg13826"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="secret.svg">
|
||||
<defs
|
||||
id="defs13820" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="8"
|
||||
inkscape:cx="-2.090004"
|
||||
inkscape:cy="28.752239"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="775"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="1"
|
||||
inkscape:window-maximized="1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<metadata
|
||||
id="metadata13823">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-0.99262638,-1.174181)">
|
||||
<g
|
||||
id="g70"
|
||||
transform="matrix(1.0148887,0,0,1.0148887,16.902146,-2.698726)">
|
||||
<path
|
||||
inkscape:export-ydpi="250.55"
|
||||
inkscape:export-xdpi="250.55"
|
||||
inkscape:export-filename="new.png"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3055"
|
||||
d="m -6.8492015,4.2724668 a 1.1191255,1.1099671 0 0 0 -0.4288818,0.1085303 l -5.8524037,2.7963394 a 1.1191255,1.1099671 0 0 0 -0.605524,0.7529759 l -1.443828,6.2812846 a 1.1191255,1.1099671 0 0 0 0.151943,0.851028 1.1191255,1.1099671 0 0 0 0.06362,0.08832 l 4.0508,5.036555 a 1.1191255,1.1099671 0 0 0 0.874979,0.417654 l 6.4961011,-0.0015 a 1.1191255,1.1099671 0 0 0 0.8749788,-0.416906 L 1.3818872,15.149453 A 1.1191255,1.1099671 0 0 0 1.5981986,14.210104 L 0.15212657,7.9288154 A 1.1191255,1.1099671 0 0 0 -0.45339794,7.1758396 L -6.3065496,4.3809971 A 1.1191255,1.1099671 0 0 0 -6.8492015,4.2724668 Z"
|
||||
style="fill:#326ce5;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
id="path3054-2-9"
|
||||
d="M -6.8523435,3.8176372 A 1.1814304,1.171762 0 0 0 -7.3044284,3.932904 l -6.1787426,2.9512758 a 1.1814304,1.171762 0 0 0 -0.639206,0.794891 l -1.523915,6.6308282 a 1.1814304,1.171762 0 0 0 0.160175,0.89893 1.1814304,1.171762 0 0 0 0.06736,0.09281 l 4.276094,5.317236 a 1.1814304,1.171762 0 0 0 0.92363,0.440858 l 6.8576188,-0.0015 a 1.1814304,1.171762 0 0 0 0.9236308,-0.44011 l 4.2745966,-5.317985 a 1.1814304,1.171762 0 0 0 0.228288,-0.990993 L 0.53894439,7.6775738 A 1.1814304,1.171762 0 0 0 -0.10026101,6.8834313 L -6.2790037,3.9321555 A 1.1814304,1.171762 0 0 0 -6.8523435,3.8176372 Z m 0.00299,0.4550789 a 1.1191255,1.1099671 0 0 1 0.5426517,0.1085303 l 5.85315169,2.7948425 A 1.1191255,1.1099671 0 0 1 0.15197811,7.9290648 L 1.598051,14.21035 a 1.1191255,1.1099671 0 0 1 -0.2163123,0.939348 l -4.0493032,5.037304 a 1.1191255,1.1099671 0 0 1 -0.8749789,0.416906 l -6.4961006,0.0015 a 1.1191255,1.1099671 0 0 1 -0.874979,-0.417652 l -4.0508,-5.036554 a 1.1191255,1.1099671 0 0 1 -0.06362,-0.08832 1.1191255,1.1099671 0 0 1 -0.151942,-0.851028 l 1.443827,-6.2812853 a 1.1191255,1.1099671 0 0 1 0.605524,-0.7529758 l 5.8524036,-2.7963395 a 1.1191255,1.1099671 0 0 1 0.4288819,-0.1085303 z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;marker:none;enable-background:accumulate"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<g
|
||||
id="g3347"
|
||||
transform="translate(0.05710921,0.77487342)">
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.79374999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 10.414299,8.0912253 3.708331,0"
|
||||
id="path922" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.79400003;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 8.2976282,5.9745582 5.8250018,0"
|
||||
id="path930" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.79374999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 5.7837382,5.9745582 1.45834,0"
|
||||
id="path934" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:#326ce5;stroke-width:0.26458332;stroke-linecap:square;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 7.7183782,8.4321603 0,0 c 0,-1.1667021 1.0110897,-2.1125011 2.2583305,-2.1125011 1.2472403,0 2.2583303,0.945799 2.2583303,2.1125011 l -1.05624,0 0,0 c 0,-0.583351 -0.5382,-1.0562501 -1.2020903,-1.0562501 -0.6638904,0 -1.2020808,0.4728991 -1.2020808,1.0562501 z"
|
||||
id="path936" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.79374999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 10.414299,10.207893 3.708331,0"
|
||||
id="path942" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.79374999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 8.2976282,12.32456 5.8250018,0"
|
||||
id="path950" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.79374999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 5.7837382,12.32456 1.45834,0"
|
||||
id="path954" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:#326ce5;stroke-width:0.26458332;stroke-linecap:square;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 7.1622182,8.3691063 5.6166608,0 0,3.6583337 -5.6166608,0 z"
|
||||
id="path956" />
|
||||
<circle
|
||||
r="0.55515254"
|
||||
cy="10.198272"
|
||||
cx="9.9705448"
|
||||
id="path7280"
|
||||
style="opacity:1;fill:#326ce5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.26478875;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:0.52957746, 0.26478873;stroke-dashoffset:5.01860619;stroke-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.5 KiB |
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="18.035334mm"
|
||||
height="17.500378mm"
|
||||
viewBox="0 0 18.035334 17.500378"
|
||||
version="1.1"
|
||||
id="svg13826"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="svc.svg">
|
||||
<defs
|
||||
id="defs13820" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="8"
|
||||
inkscape:cx="-2.090004"
|
||||
inkscape:cy="28.752239"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="775"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="1"
|
||||
inkscape:window-maximized="1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<metadata
|
||||
id="metadata13823">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-0.99262638,-1.174181)">
|
||||
<g
|
||||
id="g70"
|
||||
transform="matrix(1.0148887,0,0,1.0148887,16.902146,-2.698726)">
|
||||
<path
|
||||
inkscape:export-ydpi="250.55"
|
||||
inkscape:export-xdpi="250.55"
|
||||
inkscape:export-filename="new.png"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3055"
|
||||
d="m -6.8492015,4.2724668 a 1.1191255,1.1099671 0 0 0 -0.4288818,0.1085303 l -5.8524037,2.7963394 a 1.1191255,1.1099671 0 0 0 -0.605524,0.7529759 l -1.443828,6.2812846 a 1.1191255,1.1099671 0 0 0 0.151943,0.851028 1.1191255,1.1099671 0 0 0 0.06362,0.08832 l 4.0508,5.036555 a 1.1191255,1.1099671 0 0 0 0.874979,0.417654 l 6.4961011,-0.0015 a 1.1191255,1.1099671 0 0 0 0.8749788,-0.416906 L 1.3818872,15.149453 A 1.1191255,1.1099671 0 0 0 1.5981986,14.210104 L 0.15212657,7.9288154 A 1.1191255,1.1099671 0 0 0 -0.45339794,7.1758396 L -6.3065496,4.3809971 A 1.1191255,1.1099671 0 0 0 -6.8492015,4.2724668 Z"
|
||||
style="fill:#326ce5;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
id="path3054-2-9"
|
||||
d="M -6.8523435,3.8176372 A 1.1814304,1.171762 0 0 0 -7.3044284,3.932904 l -6.1787426,2.9512758 a 1.1814304,1.171762 0 0 0 -0.639206,0.794891 l -1.523915,6.6308282 a 1.1814304,1.171762 0 0 0 0.160175,0.89893 1.1814304,1.171762 0 0 0 0.06736,0.09281 l 4.276094,5.317236 a 1.1814304,1.171762 0 0 0 0.92363,0.440858 l 6.8576188,-0.0015 a 1.1814304,1.171762 0 0 0 0.9236308,-0.44011 l 4.2745966,-5.317985 a 1.1814304,1.171762 0 0 0 0.228288,-0.990993 L 0.53894439,7.6775738 A 1.1814304,1.171762 0 0 0 -0.10026101,6.8834313 L -6.2790037,3.9321555 A 1.1814304,1.171762 0 0 0 -6.8523435,3.8176372 Z m 0.00299,0.4550789 a 1.1191255,1.1099671 0 0 1 0.5426517,0.1085303 l 5.85315169,2.7948425 A 1.1191255,1.1099671 0 0 1 0.15197811,7.9290648 L 1.598051,14.21035 a 1.1191255,1.1099671 0 0 1 -0.2163123,0.939348 l -4.0493032,5.037304 a 1.1191255,1.1099671 0 0 1 -0.8749789,0.416906 l -6.4961006,0.0015 a 1.1191255,1.1099671 0 0 1 -0.874979,-0.417652 l -4.0508,-5.036554 a 1.1191255,1.1099671 0 0 1 -0.06362,-0.08832 1.1191255,1.1099671 0 0 1 -0.151942,-0.851028 l 1.443827,-6.2812853 a 1.1191255,1.1099671 0 0 1 0.605524,-0.7529758 l 5.8524036,-2.7963395 a 1.1191255,1.1099671 0 0 1 0.4288819,-0.1085303 z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;marker:none;enable-background:accumulate"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<g
|
||||
id="g3345"
|
||||
transform="translate(0.09238801,0.66897746)">
|
||||
<path
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:0.26458332;stroke-linecap:square;stroke-miterlimit:10"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 4.4949896,11.260826 2.9083311,0 0,2.041667 -2.9083311,0 z"
|
||||
id="path964" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:0.26458332;stroke-linecap:square;stroke-miterlimit:10"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 8.4637407,11.260826 2.9083303,0 0,2.041667 -2.9083303,0 z"
|
||||
id="path966" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:0.26458332;stroke-linecap:square;stroke-miterlimit:10"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 12.432491,11.260826 2.90833,0 0,2.041667 -2.90833,0 z"
|
||||
id="path968" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:0.26458332;stroke-linecap:square;stroke-miterlimit:10"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 7.6137407,5.2082921 4.6083303,0 0,2.041667 -4.6083303,0 z"
|
||||
id="path970" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.52916664;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 9.9179005,7.2499601 0,2.005449 -3.966671,0 0,2.0028859"
|
||||
id="path978" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.52899998;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 9.9179005,7.2499601 0,2.005449 3.9666705,0 0,2.0028859"
|
||||
id="path986" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.52916664;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 9.9095538,7.2512251 0,2.005449 0.0167,0 0,2.0028859"
|
||||
id="path982" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.7 KiB |
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="18.035334mm"
|
||||
height="17.500378mm"
|
||||
viewBox="0 0 18.035334 17.500378"
|
||||
version="1.1"
|
||||
id="svg13826"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="pv.svg">
|
||||
<defs
|
||||
id="defs13820" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="8"
|
||||
inkscape:cx="-12.090004"
|
||||
inkscape:cy="28.752239"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="775"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="1"
|
||||
inkscape:window-maximized="1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<metadata
|
||||
id="metadata13823">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-0.99262638,-1.174181)">
|
||||
<g
|
||||
id="g70"
|
||||
transform="matrix(1.0148887,0,0,1.0148887,16.902146,-2.698726)">
|
||||
<path
|
||||
inkscape:export-ydpi="250.55"
|
||||
inkscape:export-xdpi="250.55"
|
||||
inkscape:export-filename="new.png"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3055"
|
||||
d="m -6.8492015,4.2724668 a 1.1191255,1.1099671 0 0 0 -0.4288818,0.1085303 l -5.8524037,2.7963394 a 1.1191255,1.1099671 0 0 0 -0.605524,0.7529759 l -1.443828,6.2812846 a 1.1191255,1.1099671 0 0 0 0.151943,0.851028 1.1191255,1.1099671 0 0 0 0.06362,0.08832 l 4.0508,5.036555 a 1.1191255,1.1099671 0 0 0 0.874979,0.417654 l 6.4961011,-0.0015 a 1.1191255,1.1099671 0 0 0 0.8749788,-0.416906 L 1.3818872,15.149453 A 1.1191255,1.1099671 0 0 0 1.5981986,14.210104 L 0.15212657,7.9288154 A 1.1191255,1.1099671 0 0 0 -0.45339794,7.1758396 L -6.3065496,4.3809971 A 1.1191255,1.1099671 0 0 0 -6.8492015,4.2724668 Z"
|
||||
style="fill:#326ce5;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
id="path3054-2-9"
|
||||
d="M -6.8523435,3.8176372 A 1.1814304,1.171762 0 0 0 -7.3044284,3.932904 l -6.1787426,2.9512758 a 1.1814304,1.171762 0 0 0 -0.639206,0.794891 l -1.523915,6.6308282 a 1.1814304,1.171762 0 0 0 0.160175,0.89893 1.1814304,1.171762 0 0 0 0.06736,0.09281 l 4.276094,5.317236 a 1.1814304,1.171762 0 0 0 0.92363,0.440858 l 6.8576188,-0.0015 a 1.1814304,1.171762 0 0 0 0.9236308,-0.44011 l 4.2745966,-5.317985 a 1.1814304,1.171762 0 0 0 0.228288,-0.990993 L 0.53894439,7.6775738 A 1.1814304,1.171762 0 0 0 -0.10026101,6.8834313 L -6.2790037,3.9321555 A 1.1814304,1.171762 0 0 0 -6.8523435,3.8176372 Z m 0.00299,0.4550789 a 1.1191255,1.1099671 0 0 1 0.5426517,0.1085303 l 5.85315169,2.7948425 A 1.1191255,1.1099671 0 0 1 0.15197811,7.9290648 L 1.598051,14.21035 a 1.1191255,1.1099671 0 0 1 -0.2163123,0.939348 l -4.0493032,5.037304 a 1.1191255,1.1099671 0 0 1 -0.8749789,0.416906 l -6.4961006,0.0015 a 1.1191255,1.1099671 0 0 1 -0.874979,-0.417652 l -4.0508,-5.036554 a 1.1191255,1.1099671 0 0 1 -0.06362,-0.08832 1.1191255,1.1099671 0 0 1 -0.151942,-0.851028 l 1.443827,-6.2812853 a 1.1191255,1.1099671 0 0 1 0.605524,-0.7529758 l 5.8524036,-2.7963395 a 1.1191255,1.1099671 0 0 1 0.4288819,-0.1085303 z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;marker:none;enable-background:accumulate"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<g
|
||||
id="g3341"
|
||||
transform="translate(-0.18983289,0.49258906)">
|
||||
<path
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:0.26458332;stroke-linecap:square;stroke-miterlimit:10"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 5.5709614,7.9105849 0,0 c 0,0.621121 2.0725401,1.124639 4.6291706,1.124639 2.556609,0 4.629159,-0.503518 4.629159,-1.124639 l 0,3.0423911 c 0,0.62112 -2.07255,1.124638 -4.629159,1.124638 -2.5566305,0 -4.6291706,-0.503518 -4.6291706,-1.124638 z"
|
||||
id="path1114" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332;stroke-linecap:square;stroke-miterlimit:10"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 5.5709614,7.9105849 0,0 c 0,-0.621119 2.0725401,-1.124637 4.6291706,-1.124637 2.556609,0 4.629159,0.503518 4.629159,1.124637 l 0,0 c 0,0.621121 -2.07255,1.124639 -4.629159,1.124639 -2.5566305,0 -4.6291706,-0.503518 -4.6291706,-1.124639 z"
|
||||
id="path1116" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#326ce5;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 14.829291,7.9105849 0,0 c 0,0.621121 -2.07255,1.124639 -4.629159,1.124639 -2.5566205,0 -4.6291706,-0.503518 -4.6291706,-1.124639 l 0,0 c 0,-0.621119 2.0725501,-1.124637 4.6291706,-1.124637 2.556609,0 4.629159,0.503518 4.629159,1.124637 l 0,3.0423911 c 0,0.62112 -2.07255,1.124638 -4.629159,1.124638 -2.5566205,0 -4.6291706,-0.503518 -4.6291706,-1.124638 l 0,-3.0423911"
|
||||
id="path1120" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
@@ -7,11 +7,11 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
|
||||
import {
|
||||
createWSClient,
|
||||
httpBatchStreamLink,
|
||||
httpLink,
|
||||
isNonJsonSerializable,
|
||||
loggerLink,
|
||||
splitLink,
|
||||
unstable_httpBatchStreamLink,
|
||||
wsLink,
|
||||
} from "@trpc/client";
|
||||
import superjson from "superjson";
|
||||
@@ -90,7 +90,7 @@ export function TRPCReactProvider(props: PropsWithChildren) {
|
||||
url: getTrpcUrl(),
|
||||
headers: createHeadersCallbackForSource("nextjs-react (form-data)"),
|
||||
}),
|
||||
false: unstable_httpBatchStreamLink({
|
||||
false: httpBatchStreamLink({
|
||||
transformer: superjson,
|
||||
url: getTrpcUrl(),
|
||||
maxURLLength: 2083, // Suggested by tRPC: https://trpc.io/docs/client/links/httpBatchLink#setting-a-maximum-url-length
|
||||
|
||||
@@ -63,7 +63,8 @@ export default async function InviteUsagePage(props: InviteUsagePageProps) {
|
||||
<RegistrationForm invite={invite} />
|
||||
</Card>
|
||||
<Text size="xs" c="gray.5" ta="center">
|
||||
{t("description", { username: invite.creator.name })}
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||
{t("description", { username: invite.creator.name! })}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { signIn } from "@homarr/auth/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
@@ -30,6 +31,13 @@ export const InitUserForm = () => {
|
||||
title: tUser("notification.success.title"),
|
||||
message: tUser("notification.success.message"),
|
||||
});
|
||||
|
||||
await signIn("credentials", {
|
||||
name: values.username,
|
||||
password: values.password,
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
await revalidatePathActionAsync("/init");
|
||||
},
|
||||
onError: (error) => {
|
||||
|
||||
@@ -20,7 +20,6 @@ import { SettingsProvider } from "@homarr/settings";
|
||||
import { SpotlightProvider } from "@homarr/spotlight";
|
||||
import type { SupportedLanguage } from "@homarr/translation";
|
||||
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";
|
||||
@@ -81,7 +80,6 @@ export default async function Layout(props: {
|
||||
const serverSettings = await getServerSettingsAsync(db);
|
||||
const colorScheme = await getCurrentColorSchemeAsync();
|
||||
const direction = isLocaleRTL((await props.params).locale) ? "rtl" : "ltr";
|
||||
const i18nMessages = await getI18nMessages();
|
||||
|
||||
const StackedProvider = composeWrappers([
|
||||
(innerProps) => {
|
||||
@@ -105,7 +103,7 @@ export default async function Layout(props: {
|
||||
(innerProps) => <JotaiProvider {...innerProps} />,
|
||||
(innerProps) => <TRPCReactProvider {...innerProps} />,
|
||||
(innerProps) => <DayJsLoader {...innerProps} />,
|
||||
(innerProps) => <NextIntlClientProvider {...innerProps} messages={i18nMessages} />,
|
||||
(innerProps) => <NextIntlClientProvider {...innerProps} />,
|
||||
(innerProps) => <CustomMantineProvider {...innerProps} defaultColorScheme={colorScheme} />,
|
||||
(innerProps) => <ModalProvider {...innerProps} />,
|
||||
(innerProps) => <SpotlightProvider {...innerProps} />,
|
||||
|
||||
@@ -80,7 +80,7 @@ export default async function AboutPage() {
|
||||
<Text>{t("accordion.contributors.title")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("accordion.contributors.subtitle", {
|
||||
count: githubContributors.length,
|
||||
count: String(githubContributors.length),
|
||||
})}
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -104,7 +104,7 @@ export default async function AboutPage() {
|
||||
<Text>{t("accordion.translators.title")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("accordion.translators.subtitle", {
|
||||
count: crowdinContributors.length,
|
||||
count: String(crowdinContributors.length),
|
||||
})}
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -128,7 +128,7 @@ export default async function AboutPage() {
|
||||
<Text>{t("accordion.libraries.title")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("accordion.libraries.subtitle", {
|
||||
count: Object.keys(attributes.dependencies).length,
|
||||
count: String(Object.keys(attributes.dependencies).length),
|
||||
})}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -23,7 +23,9 @@ export const AppDeleteButton = ({ app }: AppDeleteButtonProps) => {
|
||||
const onClick = useCallback(() => {
|
||||
openConfirmModal({
|
||||
title: t("title"),
|
||||
children: t("message", app),
|
||||
children: t("message", {
|
||||
name: app.name,
|
||||
}),
|
||||
onConfirm: () => {
|
||||
mutate(
|
||||
{ id: app.id },
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Alert, Button, Checkbox, Fieldset, Group, SegmentedControl, Stack, Text, TextInput } from "@mantine/core";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Checkbox,
|
||||
Collapse,
|
||||
Fieldset,
|
||||
Group,
|
||||
SegmentedControl,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
import type { z } from "zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||
import { getAllSecretKindOptions, getIntegrationName, integrationDefs } from "@homarr/definitions";
|
||||
import { getAllSecretKindOptions, getIconUrl, getIntegrationName, integrationDefs } from "@homarr/definitions";
|
||||
import type { UseFormReturnType } from "@homarr/form";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
|
||||
@@ -26,11 +37,19 @@ interface NewIntegrationFormProps {
|
||||
};
|
||||
}
|
||||
|
||||
const formSchema = validation.integration.create.omit({ kind: true }).and(
|
||||
z.object({
|
||||
createApp: z.boolean(),
|
||||
appHref: validation.app.manage.shape.href,
|
||||
}),
|
||||
);
|
||||
|
||||
export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) => {
|
||||
const t = useI18n();
|
||||
const secretKinds = getAllSecretKindOptions(searchParams.kind);
|
||||
const router = useRouter();
|
||||
const form = useZodForm(validation.integration.create.omit({ kind: true }), {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const form = useZodForm(formSchema, {
|
||||
initialValues: {
|
||||
name: searchParams.name ?? getIntegrationName(searchParams.kind),
|
||||
url: searchParams.url ?? "",
|
||||
@@ -39,23 +58,60 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
value: "",
|
||||
})),
|
||||
attemptSearchEngineCreation: true,
|
||||
createApp: false,
|
||||
appHref: "",
|
||||
},
|
||||
onValuesChange(values, previous) {
|
||||
if (values.createApp !== previous.createApp) {
|
||||
setOpened(values.createApp);
|
||||
}
|
||||
},
|
||||
});
|
||||
const { mutateAsync, isPending } = clientApi.integration.create.useMutation();
|
||||
|
||||
const { mutateAsync: createIntegrationAsync, isPending: isPendingIntegration } =
|
||||
clientApi.integration.create.useMutation();
|
||||
const { mutateAsync: createAppAsync, isPending: isPendingApp } = clientApi.app.create.useMutation();
|
||||
const isPending = isPendingIntegration || isPendingApp;
|
||||
|
||||
const handleSubmitAsync = async (values: FormType) => {
|
||||
await mutateAsync(
|
||||
await createIntegrationAsync(
|
||||
{
|
||||
kind: searchParams.kind,
|
||||
...values,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
async onSuccess() {
|
||||
showSuccessNotification({
|
||||
title: t("integration.page.create.notification.success.title"),
|
||||
message: t("integration.page.create.notification.success.message"),
|
||||
});
|
||||
void revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
|
||||
|
||||
if (!values.createApp) {
|
||||
await revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
|
||||
return;
|
||||
}
|
||||
|
||||
const hasCustomHref = values.appHref !== null && values.appHref.trim().length >= 1;
|
||||
await createAppAsync(
|
||||
{
|
||||
name: values.name,
|
||||
href: hasCustomHref ? values.appHref : values.url,
|
||||
iconUrl: getIconUrl(searchParams.kind),
|
||||
description: null,
|
||||
pingUrl: values.url,
|
||||
},
|
||||
{
|
||||
async onSettled() {
|
||||
await revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
title: t("app.page.create.notification.error.title"),
|
||||
message: t("app.page.create.notification.error.message"),
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
const testConnectionError = convertIntegrationTestConnectionError(error.data?.error);
|
||||
@@ -117,6 +173,16 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
/>
|
||||
)}
|
||||
|
||||
<Checkbox
|
||||
{...form.getInputProps("createApp", { type: "checkbox" })}
|
||||
label={t("integration.field.createApp.label")}
|
||||
description={t("integration.field.createApp.description")}
|
||||
/>
|
||||
|
||||
<Collapse in={opened}>
|
||||
<TextInput placeholder={t("integration.field.appHref.placeholder")} {...form.getInputProps("appHref")} />
|
||||
</Collapse>
|
||||
|
||||
<Group justify="end" align="center">
|
||||
<Button variant="default" component={Link} href="/manage/integrations">
|
||||
{t("common.action.backToOverview")}
|
||||
@@ -166,4 +232,4 @@ const SecretKindsSegmentedControl = ({ secretKinds, form }: SecretKindsSegmented
|
||||
return <SegmentedControl fullWidth data={secretKindGroups} onChange={onChange}></SegmentedControl>;
|
||||
};
|
||||
|
||||
type FormType = Omit<z.infer<typeof validation.integration.create>, "kind">;
|
||||
type FormType = z.infer<typeof formSchema>;
|
||||
@@ -208,21 +208,20 @@ const IntegrationList = async ({ integrations, activeTab }: IntegrationListProps
|
||||
<Stack gap={0}>
|
||||
<Group justify="space-between" align="center" wrap="nowrap">
|
||||
<Text>{integration.name}</Text>
|
||||
{hasFullAccess ||
|
||||
(integration.permissions.hasFullAccess && (
|
||||
<ActionIconGroup>
|
||||
<ActionIcon
|
||||
component={Link}
|
||||
href={`/manage/integrations/edit/${integration.id}`}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label={t("page.edit.title", { name: getIntegrationName(integration.kind) })}
|
||||
>
|
||||
<IconPencil size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
<DeleteIntegrationActionButton integration={integration} count={integrations.length} />
|
||||
</ActionIconGroup>
|
||||
))}
|
||||
{(hasFullAccess || integration.permissions.hasFullAccess) && (
|
||||
<ActionIconGroup>
|
||||
<ActionIcon
|
||||
component={Link}
|
||||
href={`/manage/integrations/edit/${integration.id}`}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label={t("page.edit.title", { name: getIntegrationName(integration.kind) })}
|
||||
>
|
||||
<IconPencil size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
<DeleteIntegrationActionButton integration={integration} count={integrations.length} />
|
||||
</ActionIconGroup>
|
||||
)}
|
||||
</Group>
|
||||
<Anchor href={integration.url} target="_blank" rel="noreferrer" size="sm">
|
||||
{integration.url}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { isProviderEnabled } from "@homarr/auth/server";
|
||||
import { createDocumentationLink } from "@homarr/definitions";
|
||||
import { env } from "@homarr/docker/env";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { MainHeader } from "~/components/layout/header";
|
||||
@@ -113,7 +114,13 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
|
||||
label: t("items.tools.items.docker"),
|
||||
icon: IconBrandDocker,
|
||||
href: "/manage/tools/docker",
|
||||
hidden: !session?.user.permissions.includes("admin"),
|
||||
hidden: !(session?.user.permissions.includes("admin") && env.ENABLE_DOCKER),
|
||||
},
|
||||
{
|
||||
label: t("items.tools.items.kubernetes"),
|
||||
icon: IconBox,
|
||||
href: "/manage/tools/kubernetes",
|
||||
hidden: !(session?.user.permissions.includes("admin") && env.ENABLE_KUBERNETES),
|
||||
},
|
||||
{
|
||||
label: t("items.tools.items.api"),
|
||||
|
||||
@@ -23,7 +23,9 @@ export const SearchEngineDeleteButton = ({ searchEngine }: SearchEngineDeleteBut
|
||||
const onClick = useCallback(() => {
|
||||
openConfirmModal({
|
||||
title: t("title"),
|
||||
children: t("message", searchEngine),
|
||||
children: t("message", {
|
||||
name: searchEngine.name,
|
||||
}),
|
||||
onConfirm: () => {
|
||||
mutate(
|
||||
{ id: searchEngine.id },
|
||||
|
||||
@@ -104,7 +104,7 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers"
|
||||
enableBottomToolbar: false,
|
||||
positionGlobalFilter: "right",
|
||||
mantineSearchTextInputProps: {
|
||||
placeholder: tDocker("table.search", { count: data.containers.length }),
|
||||
placeholder: tDocker("table.search", { count: String(data.containers.length) }),
|
||||
style: { minWidth: 300 },
|
||||
autoFocus: true,
|
||||
},
|
||||
@@ -146,8 +146,8 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers"
|
||||
{groupedAlert}
|
||||
<Text fw={500}>
|
||||
{tDocker("table.selected", {
|
||||
selectCount: table.getSelectedRowModel().rows.length,
|
||||
totalCount: table.getRowCount(),
|
||||
selectCount: String(table.getSelectedRowModel().rows.length),
|
||||
totalCount: String(table.getRowCount()),
|
||||
})}
|
||||
</Text>
|
||||
<ContainerActionBar selectedContainers={dockerContainers} />
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Stack, Title } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { env } from "@homarr/docker/env";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
@@ -10,7 +11,7 @@ import { DockerTable } from "./docker-table";
|
||||
|
||||
export default async function DockerPage() {
|
||||
const session = await auth();
|
||||
if (!session?.user || !session.user.permissions.includes("admin")) {
|
||||
if (!(session?.user.permissions.includes("admin") && env.ENABLE_DOCKER)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { SimpleGrid, Skeleton, Stack, Title } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { createId } from "@homarr/db/client";
|
||||
import type { KubernetesLabelResourceType } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import KubernetesErrorPage from "~/app/[locale]/manage/tools/kubernetes/cluster-dashboard/error";
|
||||
import { HeaderCard } from "~/app/[locale]/manage/tools/kubernetes/cluster-dashboard/header-card/header-card";
|
||||
import { ResourceGauge } from "~/app/[locale]/manage/tools/kubernetes/cluster-dashboard/resource-gauge/resource-gauge";
|
||||
import { ResourceTile } from "~/app/[locale]/manage/tools/kubernetes/cluster-dashboard/resource-tile/resource-tile";
|
||||
|
||||
export function ClusterDashboard() {
|
||||
const t = useI18n();
|
||||
|
||||
const {
|
||||
data: clusterData,
|
||||
isLoading: isClusterLoading,
|
||||
isError: isClusterError,
|
||||
} = clientApi.kubernetes.cluster.getCluster.useQuery();
|
||||
|
||||
const {
|
||||
data: resourceCountsData,
|
||||
isLoading: isResourceCountsLoading,
|
||||
isError: isResourceCountsError,
|
||||
} = clientApi.kubernetes.cluster.getClusterResourceCounts.useQuery();
|
||||
|
||||
return (
|
||||
<Stack bg="var(--mantine-color-body)">
|
||||
<Title>{t("kubernetes.cluster.title")}</Title>
|
||||
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3 }}>
|
||||
{isClusterError ? (
|
||||
Array.from({ length: 3 }).map(() => <KubernetesErrorPage key={createId()} />)
|
||||
) : isClusterLoading ? (
|
||||
Array.from({ length: 3 }).map(() => <Skeleton key={createId()} height={65} />)
|
||||
) : (
|
||||
<>
|
||||
<HeaderCard headerType={"providers"} value={clusterData ? clusterData.providers : ""} />
|
||||
<HeaderCard headerType={"version"} value={clusterData ? clusterData.kubernetesVersion : ""} />
|
||||
<HeaderCard headerType={"architecture"} value={clusterData ? clusterData.architecture : ""} />
|
||||
</>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
|
||||
<Title>{t("kubernetes.cluster.capacity.title")}</Title>
|
||||
|
||||
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3 }}>
|
||||
{isClusterError
|
||||
? Array.from({ length: 3 }).map(() => <KubernetesErrorPage key={createId()} />)
|
||||
: isClusterLoading
|
||||
? Array.from({ length: 3 }).map(() => <Skeleton key={createId()} height={200} />)
|
||||
: clusterData?.capacity.map((capacity) => (
|
||||
<ResourceGauge kubernetesCapacity={capacity} key={capacity.type} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
<Title>{t("kubernetes.cluster.resources.title")}</Title>
|
||||
|
||||
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3 }}>
|
||||
{isResourceCountsError
|
||||
? Array.from({ length: 8 }).map(() => <KubernetesErrorPage key={createId()} />)
|
||||
: isResourceCountsLoading
|
||||
? Array.from({ length: 8 }).map(() => <Skeleton key={createId()} height={100} />)
|
||||
: resourceCountsData?.map((clusterResourceCount) => (
|
||||
<ResourceTile
|
||||
count={clusterResourceCount.count}
|
||||
label={clusterResourceCount.label as KubernetesLabelResourceType}
|
||||
key={clusterResourceCount.label}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Anchor, Center, Stack, Text } from "@mantine/core";
|
||||
import { IconCubeOff } from "@tabler/icons-react";
|
||||
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
export default function KubernetesErrorPage() {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<Stack align="center">
|
||||
<IconCubeOff size={48} stroke={1.5} />
|
||||
<Stack align="center" gap="xs">
|
||||
<Text size="lg" fw={500}>
|
||||
{t("kubernetes.error.internalServerError")}
|
||||
</Text>
|
||||
<Anchor size="sm" component={Link} href="/manage/tools/logs">
|
||||
{t("common.action.checkLogs")}
|
||||
</Anchor>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
.header {
|
||||
direction: inherit;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: var(--mantine-spacing-xs);
|
||||
padding-left: calc(var(--mantine-spacing-xs) * 2);
|
||||
|
||||
@mixin hover {
|
||||
box-shadow: var(--mantine-shadow-md);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 6px;
|
||||
background-image: linear-gradient(0, var(--mantine-color-blue-4), var(--mantine-color-blue-9));
|
||||
}
|
||||
|
||||
@mixin rtl {
|
||||
padding-left: var(--mantine-spacing-xs);
|
||||
padding-right: calc(var(--mantine-spacing-xs) * 2);
|
||||
|
||||
&::before {
|
||||
left: unset;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Card, Flex, Text, ThemeIcon } from "@mantine/core";
|
||||
|
||||
import { isLocaleRTL } from "@homarr/translation";
|
||||
import { useCurrentLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { HeaderIcon } from "~/app/[locale]/manage/tools/kubernetes/cluster-dashboard/header-card/header-icon";
|
||||
import classes from "./header-card.module.css";
|
||||
|
||||
export type HeaderTypes = "providers" | "version" | "architecture";
|
||||
|
||||
interface HeaderCardProps {
|
||||
headerType: HeaderTypes;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function HeaderCard(props: HeaderCardProps) {
|
||||
const t = useI18n();
|
||||
const currentLocale = useCurrentLocale();
|
||||
const isRTL = isLocaleRTL(currentLocale);
|
||||
|
||||
return (
|
||||
<Card className={classes.header}>
|
||||
<Flex align="center" justify={isRTL ? "space-between" : "flex-start"} gap="md" direction={"row"}>
|
||||
<ThemeIcon
|
||||
size="xl"
|
||||
radius="md"
|
||||
variant="gradient"
|
||||
gradient={{
|
||||
deg: 0,
|
||||
from: "var(--mantine-color-blue-4)",
|
||||
to: "var(--mantine-color-blue-9)",
|
||||
}}
|
||||
>
|
||||
<HeaderIcon type={props.headerType} />
|
||||
</ThemeIcon>
|
||||
<Text size="xl" fw={500} dir={isRTL ? "rtl" : "ltr"}>
|
||||
{isRTL
|
||||
? `${props.value} : ${t(`kubernetes.cluster.${props.headerType}`)}`
|
||||
: `${t(`kubernetes.cluster.${props.headerType}`)} : ${props.value}`}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { IconBrandGit, IconCloudShare, IconGeometry } from "@tabler/icons-react";
|
||||
|
||||
import type { HeaderTypes } from "~/app/[locale]/manage/tools/kubernetes/cluster-dashboard/header-card/header-card";
|
||||
|
||||
interface HeaderIconProps {
|
||||
type: HeaderTypes;
|
||||
}
|
||||
|
||||
export function HeaderIcon({ type }: HeaderIconProps) {
|
||||
switch (type) {
|
||||
case "providers":
|
||||
return <IconCloudShare size={28} stroke={1.5} />;
|
||||
case "version":
|
||||
return <IconBrandGit size={28} stroke={1.5} />;
|
||||
default:
|
||||
return <IconGeometry size={28} stroke={1.5} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
.paper {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
padding: var(--mantine-spacing-xl);
|
||||
padding-top: calc(var(--mantine-spacing-xl) * 1.5 + 20px);
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: calc(50% - 30px);
|
||||
border: groove white;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family:
|
||||
Greycliff CF,
|
||||
var(--mantine-font-family);
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from "react";
|
||||
import { Group, Paper, Progress, Text, ThemeIcon } from "@mantine/core";
|
||||
|
||||
import type { KubernetesCapacity } from "@homarr/definitions";
|
||||
import { isLocaleRTL } from "@homarr/translation";
|
||||
import { useCurrentLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { ResourceIcon } from "~/app/[locale]/manage/tools/kubernetes/cluster-dashboard/resource-gauge/resource-icon";
|
||||
import classes from "./resource-gauge.module.css";
|
||||
|
||||
interface KubernetesResourceGaugeProps {
|
||||
kubernetesCapacity: KubernetesCapacity;
|
||||
}
|
||||
|
||||
export function ResourceGauge(props: KubernetesResourceGaugeProps) {
|
||||
const t = useI18n();
|
||||
const currentLocale = useCurrentLocale();
|
||||
const isRTL = Boolean(isLocaleRTL(currentLocale));
|
||||
|
||||
return (
|
||||
<Paper radius="md" withBorder className={classes.paper} mt={20}>
|
||||
<ThemeIcon className={classes.icon} size={60} radius={60} bg={"#326ce5"}>
|
||||
<ResourceIcon type={props.kubernetesCapacity.type} />
|
||||
</ThemeIcon>
|
||||
|
||||
<Text ta="center" fw={700} className={classes.title}>
|
||||
{props.kubernetesCapacity.type}
|
||||
</Text>
|
||||
|
||||
{props.kubernetesCapacity.resourcesStats.map((stat) => {
|
||||
const isReserved = stat.type === "Reserved";
|
||||
const labelKey = isReserved
|
||||
? "kubernetes.cluster.capacity.resource.reserved"
|
||||
: "kubernetes.cluster.capacity.resource.used";
|
||||
|
||||
return (
|
||||
<div key={stat.percentageValue}>
|
||||
<Group justify={"space-between"} mt="xs">
|
||||
<Text fz="sm" c="dimmed">
|
||||
{isRTL ? (
|
||||
<>
|
||||
{stat.capacityUnit && (
|
||||
<Text component="span" mr={4}>
|
||||
{stat.capacityUnit}
|
||||
</Text>
|
||||
)}
|
||||
<Text component="span">
|
||||
{stat.maxUsedValue} / {stat.usedValue}{" "}
|
||||
</Text>
|
||||
<Text component="span" fw={500}>
|
||||
{t(labelKey)}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text component="span" fw={500}>
|
||||
{t(labelKey)}
|
||||
</Text>
|
||||
<Text component="span">
|
||||
{" "}
|
||||
{stat.usedValue} / {stat.maxUsedValue}{" "}
|
||||
</Text>
|
||||
{stat.capacityUnit && (
|
||||
<Text component="span" ml={4}>
|
||||
{stat.capacityUnit}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed">
|
||||
{isRTL ? `%${stat.percentageValue}` : `${stat.percentageValue}%`}
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress value={stat.percentageValue} mt={5} color={getProgressBarColor(stat.percentageValue)} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
function getProgressBarColor(value: number): string {
|
||||
if (value > 50 && value < 65) {
|
||||
return "yellow";
|
||||
} else if (value >= 65) {
|
||||
return "red";
|
||||
}
|
||||
return "blue";
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { IconCpu, IconCube, IconDeviceDesktopAnalytics } from "@tabler/icons-react";
|
||||
|
||||
import type { KubernetesCapacityType } from "@homarr/definitions";
|
||||
|
||||
const resourceIcons = {
|
||||
CPU: IconCpu,
|
||||
Memory: IconDeviceDesktopAnalytics,
|
||||
Pods: IconCube,
|
||||
} satisfies Record<KubernetesCapacityType, React.ComponentType<{ size: number; stroke: number }>>;
|
||||
|
||||
interface ResourceIconProps {
|
||||
type: KubernetesCapacityType;
|
||||
size?: number;
|
||||
stroke?: number;
|
||||
}
|
||||
|
||||
export const ResourceIcon: React.FC<ResourceIconProps> = ({ type, size = 32, stroke = 1.5 }) => {
|
||||
const Icon = resourceIcons[type];
|
||||
return <Icon size={size} stroke={stroke} />;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
.cardContainer {
|
||||
transition:
|
||||
box-shadow 150ms ease,
|
||||
transform 100ms ease;
|
||||
|
||||
@mixin hover {
|
||||
box-shadow: var(--mantine-shadow-md);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Card, Group, Text } from "@mantine/core";
|
||||
import { IconArrowRight } from "@tabler/icons-react";
|
||||
|
||||
import type { KubernetesLabelResourceType } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import classes from "./resource-tile.module.css";
|
||||
|
||||
interface ResourceTileProps {
|
||||
count: number;
|
||||
label: KubernetesLabelResourceType;
|
||||
}
|
||||
|
||||
export function ResourceTile(props: ResourceTileProps) {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<Card
|
||||
withBorder
|
||||
component={Link}
|
||||
href={`/manage/tools/kubernetes/${props.label}`}
|
||||
className={classes.cardContainer}
|
||||
>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Image src={`/images/kubernetes/${props.label}.svg`} alt={props.label} width={64} height={64} />
|
||||
<Group gap="xs">
|
||||
<Text size="xl" fw={700} tt="capitalize">
|
||||
{props.count} {t(`kubernetes.cluster.resources.${props.label}`)}
|
||||
</Text>
|
||||
<IconArrowRight />
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import type { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { MantineReactTable } from "mantine-react-table";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { KubernetesBaseResource } from "@homarr/definitions";
|
||||
import type { ScopedTranslationFunction } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
interface ConfigMapsTableComponentProps {
|
||||
initialConfigMaps: RouterOutputs["kubernetes"]["configMaps"]["getConfigMaps"];
|
||||
}
|
||||
|
||||
const createColumns = (
|
||||
t: ScopedTranslationFunction<"kubernetes.configmaps">,
|
||||
): MRT_ColumnDef<KubernetesBaseResource>[] => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: t("field.name.label"),
|
||||
enableClickToCopy: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "namespace",
|
||||
header: t("field.namespace.label"),
|
||||
enableClickToCopy: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "creationTimestamp",
|
||||
header: t("field.creationTimestamp.label"),
|
||||
Cell: ({ row }) => dayjs(row.original.creationTimestamp).fromNow(false),
|
||||
},
|
||||
];
|
||||
|
||||
export function ConfigmapsTable(initialData: ConfigMapsTableComponentProps) {
|
||||
const tConfigMaps = useScopedI18n("kubernetes.configmaps");
|
||||
|
||||
const { data } = clientApi.kubernetes.configMaps.getConfigMaps.useQuery(undefined, {
|
||||
initialData: initialData.initialConfigMaps,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
const table = useTranslatedMantineReactTable({
|
||||
data,
|
||||
enableDensityToggle: false,
|
||||
enableColumnActions: false,
|
||||
enableColumnFilters: false,
|
||||
enablePagination: false,
|
||||
enableRowSelection: true,
|
||||
positionToolbarAlertBanner: "top",
|
||||
enableTableFooter: false,
|
||||
enableBottomToolbar: false,
|
||||
positionGlobalFilter: "right",
|
||||
initialState: { density: "xs", showGlobalFilter: true },
|
||||
mantineSearchTextInputProps: {
|
||||
placeholder: tConfigMaps("table.search", { count: String(data.length) }),
|
||||
style: { minWidth: 300 },
|
||||
autoFocus: true,
|
||||
},
|
||||
|
||||
columns: createColumns(tConfigMaps),
|
||||
});
|
||||
|
||||
return <MantineReactTable table={table} />;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Stack, Title } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { env } from "@homarr/docker/env";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { ConfigmapsTable } from "~/app/[locale]/manage/tools/kubernetes/configmaps/configmaps-table";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
|
||||
export default async function ConfigMapsPage() {
|
||||
const session = await auth();
|
||||
if (!(session?.user.permissions.includes("admin") && env.ENABLE_KUBERNETES)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const configMaps = await api.kubernetes.configMaps.getConfigMaps();
|
||||
const tConfigMaps = await getScopedI18n("kubernetes.configmaps");
|
||||
|
||||
return (
|
||||
<>
|
||||
<DynamicBreadcrumb />
|
||||
<Stack>
|
||||
<Title order={1}>{tConfigMaps("label")}</Title>
|
||||
<ConfigmapsTable initialConfigMaps={configMaps} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Anchor, Flex } from "@mantine/core";
|
||||
import { IconArrowRight } from "@tabler/icons-react";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import type { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { MantineReactTable } from "mantine-react-table";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { createId } from "@homarr/db/client";
|
||||
import type { KubernetesIngress } from "@homarr/definitions";
|
||||
import type { ScopedTranslationFunction } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
interface IngressesTableComponentProps {
|
||||
initialIngresses: RouterOutputs["kubernetes"]["ingresses"]["getIngresses"];
|
||||
}
|
||||
|
||||
const createColumns = (t: ScopedTranslationFunction<"kubernetes.ingresses">): MRT_ColumnDef<KubernetesIngress>[] => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: t("field.name.label"),
|
||||
enableClickToCopy: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "namespace",
|
||||
header: t("field.namespace.label"),
|
||||
enableClickToCopy: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "className",
|
||||
header: t("field.className.label"),
|
||||
enableClickToCopy: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "rulesAndPaths",
|
||||
header: t("field.rulesAndPaths.label"),
|
||||
Cell({ cell }) {
|
||||
const getAbsoluteUrl = (host: string) =>
|
||||
host.startsWith("http://") || host.startsWith("https://") ? host : `https://${host}`;
|
||||
return (
|
||||
<>
|
||||
{cell.row.original.rulesAndPaths.map((ruleAndPaths) => (
|
||||
<div key={ruleAndPaths.host}>
|
||||
<Flex align="flex-end">
|
||||
<Anchor href={getAbsoluteUrl(ruleAndPaths.host)} target="_blank">
|
||||
{getAbsoluteUrl(ruleAndPaths.host)}
|
||||
</Anchor>
|
||||
<IconArrowRight size={22} stroke={2} />
|
||||
</Flex>
|
||||
{ruleAndPaths.paths.map((path) => (
|
||||
<div key={createId()}>
|
||||
{path.serviceName}:{path.port}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "creationTimestamp",
|
||||
header: t("field.creationTimestamp.label"),
|
||||
Cell: ({ row }) => dayjs(row.original.creationTimestamp).fromNow(false),
|
||||
},
|
||||
];
|
||||
|
||||
export function IngressesTable(initialData: IngressesTableComponentProps) {
|
||||
const tIngresses = useScopedI18n("kubernetes.ingresses");
|
||||
|
||||
const { data } = clientApi.kubernetes.ingresses.getIngresses.useQuery(undefined, {
|
||||
initialData: initialData.initialIngresses,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
const table = useTranslatedMantineReactTable({
|
||||
data,
|
||||
enableDensityToggle: false,
|
||||
enableColumnActions: false,
|
||||
enableColumnFilters: false,
|
||||
enablePagination: false,
|
||||
enableRowSelection: true,
|
||||
positionToolbarAlertBanner: "top",
|
||||
enableTableFooter: false,
|
||||
enableBottomToolbar: false,
|
||||
positionGlobalFilter: "right",
|
||||
initialState: { density: "xs", showGlobalFilter: true },
|
||||
mantineSearchTextInputProps: {
|
||||
placeholder: tIngresses("table.search", { count: String(data.length) }),
|
||||
style: { minWidth: 300 },
|
||||
autoFocus: true,
|
||||
},
|
||||
|
||||
columns: createColumns(tIngresses),
|
||||
});
|
||||
|
||||
return <MantineReactTable table={table} />;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Stack, Title } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { env } from "@homarr/docker/env";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { IngressesTable } from "~/app/[locale]/manage/tools/kubernetes/ingresses/ingresses-table";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
|
||||
export default async function NamespacesPage() {
|
||||
const session = await auth();
|
||||
if (!(session?.user.permissions.includes("admin") && env.ENABLE_KUBERNETES)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const ingresses = await api.kubernetes.ingresses.getIngresses();
|
||||
const tIngresses = await getScopedI18n("kubernetes.ingresses");
|
||||
return (
|
||||
<>
|
||||
<DynamicBreadcrumb />
|
||||
<Stack>
|
||||
<Title order={1}>{tIngresses("label")}</Title>
|
||||
<IngressesTable initialIngresses={ingresses} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Badge, rem } from "@mantine/core";
|
||||
import { IconCircleDashedCheck, IconHeartBroken } from "@tabler/icons-react";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import type { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { MantineReactTable } from "mantine-react-table";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { KubernetesNamespace } from "@homarr/definitions";
|
||||
import type { ScopedTranslationFunction } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
interface NamespacesTableComponentProps {
|
||||
initialNamespaces: RouterOutputs["kubernetes"]["namespaces"]["getNamespaces"];
|
||||
}
|
||||
|
||||
const createColumns = (t: ScopedTranslationFunction<"kubernetes.namespaces">): MRT_ColumnDef<KubernetesNamespace>[] => [
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: t("field.state.label"),
|
||||
|
||||
Cell({ cell }) {
|
||||
const checkIcon = <IconCircleDashedCheck style={{ width: rem(12), height: rem(12) }} />;
|
||||
const downIcon = <IconHeartBroken style={{ width: rem(12), height: rem(12) }} />;
|
||||
|
||||
const badgeKubernetesNamespaceStatusColor = cell.row.original.status === "Active" ? "green" : "yellow";
|
||||
const badgeKubernetesNamespaceStatusIcon = cell.row.original.status === "Active" ? checkIcon : downIcon;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
leftSection={badgeKubernetesNamespaceStatusIcon}
|
||||
color={badgeKubernetesNamespaceStatusColor}
|
||||
variant="light"
|
||||
>
|
||||
{cell.row.original.status}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: t("field.name.label"),
|
||||
enableClickToCopy: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "creationTimestamp",
|
||||
header: t("field.creationTimestamp.label"),
|
||||
Cell: ({ row }) => dayjs(row.original.creationTimestamp).fromNow(false),
|
||||
},
|
||||
];
|
||||
|
||||
export function NamespacesTable(initialData: NamespacesTableComponentProps) {
|
||||
const tNamespaces = useScopedI18n("kubernetes.namespaces");
|
||||
|
||||
const { data } = clientApi.kubernetes.namespaces.getNamespaces.useQuery(undefined, {
|
||||
initialData: initialData.initialNamespaces,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
const table = useTranslatedMantineReactTable({
|
||||
data,
|
||||
enableDensityToggle: false,
|
||||
enableColumnActions: false,
|
||||
enableColumnFilters: false,
|
||||
enablePagination: false,
|
||||
enableRowSelection: true,
|
||||
positionToolbarAlertBanner: "top",
|
||||
enableTableFooter: false,
|
||||
enableBottomToolbar: false,
|
||||
positionGlobalFilter: "right",
|
||||
initialState: { density: "xs", showGlobalFilter: true },
|
||||
mantineSearchTextInputProps: {
|
||||
placeholder: tNamespaces("table.search", { count: String(data.length) }),
|
||||
style: { minWidth: 300 },
|
||||
autoFocus: true,
|
||||
},
|
||||
|
||||
columns: createColumns(tNamespaces),
|
||||
});
|
||||
|
||||
return <MantineReactTable table={table} />;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Stack, Title } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { env } from "@homarr/docker/env";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { NamespacesTable } from "~/app/[locale]/manage/tools/kubernetes/namespaces/namespaces-table";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
|
||||
export default async function NamespacesPage() {
|
||||
const session = await auth();
|
||||
if (!(session?.user.permissions.includes("admin") && env.ENABLE_KUBERNETES)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const namespaces = await api.kubernetes.namespaces.getNamespaces();
|
||||
const tNamespaces = await getScopedI18n("kubernetes.namespaces");
|
||||
return (
|
||||
<>
|
||||
<DynamicBreadcrumb />
|
||||
<Stack>
|
||||
<Title order={1}>{tNamespaces("label")}</Title>
|
||||
<NamespacesTable initialNamespaces={namespaces} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Badge, rem, RingProgress, Text } from "@mantine/core";
|
||||
import { IconCircleDashedCheck, IconHeartBroken } from "@tabler/icons-react";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import type { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { MantineReactTable } from "mantine-react-table";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { KubernetesNode } from "@homarr/definitions";
|
||||
import type { ScopedTranslationFunction } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
interface NodesListComponentProps {
|
||||
initialNodes: RouterOutputs["kubernetes"]["nodes"]["getNodes"];
|
||||
}
|
||||
|
||||
const createColumns = (t: ScopedTranslationFunction<"kubernetes.nodes">): MRT_ColumnDef<KubernetesNode>[] => [
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: t("field.state.label"),
|
||||
|
||||
Cell({ cell }) {
|
||||
const checkIcon = <IconCircleDashedCheck style={{ width: rem(12), height: rem(12) }} />;
|
||||
const downIcon = <IconHeartBroken style={{ width: rem(12), height: rem(12) }} />;
|
||||
|
||||
const badgeKubernetesNodeStatusColor = cell.row.original.status === "Ready" ? "green" : "red";
|
||||
const badgeKubernetesNodeStatusIcon = cell.row.original.status === "Ready" ? checkIcon : downIcon;
|
||||
|
||||
return (
|
||||
<Badge leftSection={badgeKubernetesNodeStatusIcon} color={badgeKubernetesNodeStatusColor} variant="light">
|
||||
{cell.row.original.status}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: t("field.name.label"),
|
||||
enableClickToCopy: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "allocatableCpuPercentage",
|
||||
header: t("field.cpu.label"),
|
||||
Cell({ cell }) {
|
||||
return getRingProgress(cell.row.original.allocatableCpuPercentage);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "allocatableRamPercentage",
|
||||
header: t("field.memory.label"),
|
||||
Cell({ cell }) {
|
||||
return getRingProgress(cell.row.original.allocatableRamPercentage);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "operatingSystem",
|
||||
header: t("field.operatingSystem.label"),
|
||||
},
|
||||
{
|
||||
accessorKey: "podsCount",
|
||||
header: t("field.pods.label"),
|
||||
},
|
||||
{
|
||||
accessorKey: "architecture",
|
||||
header: t("field.architecture.label"),
|
||||
},
|
||||
{
|
||||
accessorKey: "kubernetesVersion",
|
||||
header: t("field.kubernetesVersion.label"),
|
||||
},
|
||||
{
|
||||
accessorKey: "creationTimestamp",
|
||||
header: t("field.creationTimestamp.label"),
|
||||
Cell: ({ row }) => dayjs(row.original.creationTimestamp).fromNow(false),
|
||||
},
|
||||
];
|
||||
|
||||
export function NodesTable(initialData: NodesListComponentProps) {
|
||||
const tNodes = useScopedI18n("kubernetes.nodes");
|
||||
|
||||
const { data } = clientApi.kubernetes.nodes.getNodes.useQuery(undefined, {
|
||||
initialData: initialData.initialNodes,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
const table = useTranslatedMantineReactTable({
|
||||
data,
|
||||
enableDensityToggle: false,
|
||||
enableColumnActions: false,
|
||||
enableColumnFilters: false,
|
||||
enablePagination: false,
|
||||
enableRowSelection: true,
|
||||
positionToolbarAlertBanner: "top",
|
||||
enableTableFooter: false,
|
||||
enableBottomToolbar: false,
|
||||
positionGlobalFilter: "right",
|
||||
initialState: { density: "xs", showGlobalFilter: true },
|
||||
mantineSearchTextInputProps: {
|
||||
placeholder: tNodes("table.search", { count: String(data.length) }),
|
||||
style: { minWidth: 300 },
|
||||
autoFocus: true,
|
||||
},
|
||||
columns: createColumns(tNodes),
|
||||
});
|
||||
|
||||
return <MantineReactTable table={table} />;
|
||||
}
|
||||
|
||||
function getRingProgress(value: number) {
|
||||
return (
|
||||
<RingProgress
|
||||
size={70}
|
||||
roundCaps
|
||||
thickness={7}
|
||||
sections={[{ value, color: "blue" }]}
|
||||
label={
|
||||
<Text c="blue" fw={400} ta="center" size="md">
|
||||
{value}%
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Stack, Title } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { env } from "@homarr/docker/env";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { NodesTable } from "~/app/[locale]/manage/tools/kubernetes/nodes/nodes-table";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
|
||||
export default async function NodesPage() {
|
||||
const session = await auth();
|
||||
if (!(session?.user.permissions.includes("admin") && env.ENABLE_KUBERNETES)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const nodes = await api.kubernetes.nodes.getNodes();
|
||||
const tNodes = await getScopedI18n("kubernetes.nodes");
|
||||
return (
|
||||
<>
|
||||
<DynamicBreadcrumb />
|
||||
<Stack>
|
||||
<Title order={1}>{tNodes("label")}</Title>
|
||||
<NodesTable initialNodes={nodes} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { env } from "@homarr/docker/env";
|
||||
|
||||
import { ClusterDashboard } from "~/app/[locale]/manage/tools/kubernetes/cluster-dashboard/cluster-dashboard";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
|
||||
export default async function KubernetesPage() {
|
||||
const session = await auth();
|
||||
if (!(session?.user.permissions.includes("admin") && env.ENABLE_KUBERNETES)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DynamicBreadcrumb />
|
||||
<ClusterDashboard />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Stack, Title } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { env } from "@homarr/docker/env";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { PodsTable } from "~/app/[locale]/manage/tools/kubernetes/pods/pods-table";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
|
||||
export default async function PodsPage() {
|
||||
const session = await auth();
|
||||
if (!(session?.user.permissions.includes("admin") && env.ENABLE_KUBERNETES)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const pods = await api.kubernetes.pods.getPods();
|
||||
const tPods = await getScopedI18n("kubernetes.pods");
|
||||
return (
|
||||
<>
|
||||
<DynamicBreadcrumb />
|
||||
<Stack>
|
||||
<Title order={1}>{tPods("label")}</Title>
|
||||
<PodsTable initialPods={pods} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import type { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { MantineReactTable } from "mantine-react-table";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { KubernetesPod } from "@homarr/definitions";
|
||||
import type { ScopedTranslationFunction } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
interface PodsTableComponentProps {
|
||||
initialPods: RouterOutputs["kubernetes"]["pods"]["getPods"];
|
||||
}
|
||||
|
||||
const createColumns = (t: ScopedTranslationFunction<"kubernetes.pods">): MRT_ColumnDef<KubernetesPod>[] => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: t("field.name.label"),
|
||||
},
|
||||
{
|
||||
accessorKey: "namespace",
|
||||
header: t("field.namespace.label"),
|
||||
},
|
||||
{
|
||||
accessorKey: "image",
|
||||
header: t("field.image.label"),
|
||||
},
|
||||
{
|
||||
accessorKey: "applicationType",
|
||||
header: t("field.applicationType.label"),
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: t("field.status.label"),
|
||||
},
|
||||
{
|
||||
accessorKey: "creationTimestamp",
|
||||
header: t("field.creationTimestamp.label"),
|
||||
Cell: ({ row }) => dayjs(row.original.creationTimestamp).fromNow(false),
|
||||
},
|
||||
];
|
||||
|
||||
export function PodsTable(initialData: PodsTableComponentProps) {
|
||||
const tPods = useScopedI18n("kubernetes.pods");
|
||||
|
||||
const { data } = clientApi.kubernetes.pods.getPods.useQuery(undefined, {
|
||||
initialData: initialData.initialPods,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
const table = useTranslatedMantineReactTable({
|
||||
data,
|
||||
enableDensityToggle: false,
|
||||
enableColumnActions: false,
|
||||
enableColumnFilters: false,
|
||||
enablePagination: false,
|
||||
enableRowSelection: true,
|
||||
positionToolbarAlertBanner: "top",
|
||||
enableTableFooter: false,
|
||||
enableBottomToolbar: false,
|
||||
positionGlobalFilter: "right",
|
||||
initialState: { density: "xs", showGlobalFilter: true, expanded: true },
|
||||
mantineSearchTextInputProps: {
|
||||
placeholder: tPods("table.search", { count: String(data.length) }),
|
||||
style: { minWidth: 300 },
|
||||
autoFocus: true,
|
||||
},
|
||||
enableGrouping: true,
|
||||
columns: createColumns(tPods),
|
||||
});
|
||||
|
||||
return <MantineReactTable table={table} />;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Stack, Title } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { env } from "@homarr/docker/env";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { SecretsTable } from "~/app/[locale]/manage/tools/kubernetes/secrets/secrets-table";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
|
||||
export default async function SecretsPage() {
|
||||
const session = await auth();
|
||||
if (!(session?.user.permissions.includes("admin") && env.ENABLE_KUBERNETES)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const secrets = await api.kubernetes.secrets.getSecrets();
|
||||
const tSecrets = await getScopedI18n("kubernetes.secrets");
|
||||
return (
|
||||
<>
|
||||
<DynamicBreadcrumb />
|
||||
<Stack>
|
||||
<Title order={1}>{tSecrets("label")}</Title>
|
||||
<SecretsTable initialSecrets={secrets} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import type { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { MantineReactTable } from "mantine-react-table";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { KubernetesSecret } from "@homarr/definitions";
|
||||
import type { ScopedTranslationFunction } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
interface SecretsTableComponentProps {
|
||||
initialSecrets: RouterOutputs["kubernetes"]["secrets"]["getSecrets"];
|
||||
}
|
||||
|
||||
const createColumns = (t: ScopedTranslationFunction<"kubernetes.secrets">): MRT_ColumnDef<KubernetesSecret>[] => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: t("field.name.label"),
|
||||
enableClickToCopy: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "namespace",
|
||||
header: t("field.namespace.label"),
|
||||
enableClickToCopy: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: t("field.type.label"),
|
||||
enableClickToCopy: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "creationTimestamp",
|
||||
header: t("field.creationTimestamp.label"),
|
||||
Cell: ({ row }) => dayjs(row.original.creationTimestamp).fromNow(false),
|
||||
},
|
||||
];
|
||||
|
||||
export function SecretsTable(initialData: SecretsTableComponentProps) {
|
||||
const tSecrets = useScopedI18n("kubernetes.secrets");
|
||||
|
||||
const { data } = clientApi.kubernetes.secrets.getSecrets.useQuery(undefined, {
|
||||
initialData: initialData.initialSecrets,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
const table = useTranslatedMantineReactTable({
|
||||
data,
|
||||
enableDensityToggle: false,
|
||||
enableColumnActions: false,
|
||||
enableColumnFilters: false,
|
||||
enablePagination: false,
|
||||
enableRowSelection: true,
|
||||
positionToolbarAlertBanner: "top",
|
||||
enableTableFooter: false,
|
||||
enableBottomToolbar: false,
|
||||
positionGlobalFilter: "right",
|
||||
initialState: { density: "xs", showGlobalFilter: true },
|
||||
mantineSearchTextInputProps: {
|
||||
placeholder: tSecrets("table.search", { count: String(data.length) }),
|
||||
style: { minWidth: 300 },
|
||||
autoFocus: true,
|
||||
},
|
||||
|
||||
columns: createColumns(tSecrets),
|
||||
});
|
||||
|
||||
return <MantineReactTable table={table} />;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Stack, Title } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { env } from "@homarr/docker/env";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { ServicesTable } from "~/app/[locale]/manage/tools/kubernetes/services/services-table";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
|
||||
export default async function ServicesPage() {
|
||||
const session = await auth();
|
||||
if (!(session?.user.permissions.includes("admin") && env.ENABLE_KUBERNETES)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const services = await api.kubernetes.services.getServices();
|
||||
const tServices = await getScopedI18n("kubernetes.services");
|
||||
return (
|
||||
<>
|
||||
<DynamicBreadcrumb />
|
||||
<Stack>
|
||||
<Title order={1}>{tServices("label")}</Title>
|
||||
<ServicesTable initialServices={services} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import type { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { MantineReactTable } from "mantine-react-table";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { createId } from "@homarr/db/client";
|
||||
import type { KubernetesService } from "@homarr/definitions";
|
||||
import type { ScopedTranslationFunction } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
interface ServicesTableComponentProps {
|
||||
initialServices: RouterOutputs["kubernetes"]["services"]["getServices"];
|
||||
}
|
||||
|
||||
const createColumns = (t: ScopedTranslationFunction<"kubernetes.services">): MRT_ColumnDef<KubernetesService>[] => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: t("field.name.label"),
|
||||
enableClickToCopy: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "namespace",
|
||||
header: t("field.namespace.label"),
|
||||
enableClickToCopy: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: t("field.type.label"),
|
||||
},
|
||||
{
|
||||
accessorKey: "ports",
|
||||
header: t("field.ports.label"),
|
||||
Cell({ cell }) {
|
||||
return cell.row.original.ports?.map((port) => <div key={createId()}>{port}</div>);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "targetPorts",
|
||||
header: t("field.targetPorts.label"),
|
||||
Cell({ cell }) {
|
||||
return cell.row.original.targetPorts?.map((targetPort) => <div key={createId()}>{targetPort}</div>);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "clusterIP",
|
||||
header: t("field.clusterIP.label"),
|
||||
enableClickToCopy: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "creationTimestamp",
|
||||
header: t("field.creationTimestamp.label"),
|
||||
Cell: ({ row }) => dayjs(row.original.creationTimestamp).fromNow(false),
|
||||
},
|
||||
];
|
||||
|
||||
export function ServicesTable(initialData: ServicesTableComponentProps) {
|
||||
const tServices = useScopedI18n("kubernetes.services");
|
||||
|
||||
const { data } = clientApi.kubernetes.services.getServices.useQuery(undefined, {
|
||||
initialData: initialData.initialServices,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
const table = useTranslatedMantineReactTable({
|
||||
data,
|
||||
enableDensityToggle: false,
|
||||
enableColumnActions: false,
|
||||
enableColumnFilters: false,
|
||||
enablePagination: false,
|
||||
enableRowSelection: true,
|
||||
positionToolbarAlertBanner: "top",
|
||||
enableTableFooter: false,
|
||||
enableBottomToolbar: false,
|
||||
positionGlobalFilter: "right",
|
||||
initialState: { density: "xs", showGlobalFilter: true },
|
||||
mantineSearchTextInputProps: {
|
||||
placeholder: tServices("table.search", { count: String(data.length) }),
|
||||
style: { minWidth: 300 },
|
||||
autoFocus: true,
|
||||
},
|
||||
columns: createColumns(tServices),
|
||||
});
|
||||
|
||||
return <MantineReactTable table={table} />;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Stack, Title } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { env } from "@homarr/docker/env";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { VolumesTable } from "~/app/[locale]/manage/tools/kubernetes/volumes/volumes-table";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
|
||||
export default async function VolumesPage() {
|
||||
const session = await auth();
|
||||
if (!(session?.user.permissions.includes("admin") && env.ENABLE_KUBERNETES)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const volumes = await api.kubernetes.volumes.getVolumes();
|
||||
const tVolumes = await getScopedI18n("kubernetes.volumes");
|
||||
return (
|
||||
<>
|
||||
<DynamicBreadcrumb />
|
||||
<Stack>
|
||||
<Title order={1}>{tVolumes("label")}</Title>
|
||||
<VolumesTable initialVolumes={volumes} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import type { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { MantineReactTable } from "mantine-react-table";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { KubernetesVolume } from "@homarr/definitions";
|
||||
import type { ScopedTranslationFunction } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
interface VolumesTableComponentProps {
|
||||
initialVolumes: RouterOutputs["kubernetes"]["volumes"]["getVolumes"];
|
||||
}
|
||||
|
||||
const createColumns = (t: ScopedTranslationFunction<"kubernetes.volumes">): MRT_ColumnDef<KubernetesVolume>[] => [
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: t("field.status.label"),
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: t("field.name.label"),
|
||||
enableClickToCopy: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "namespace",
|
||||
header: t("field.namespace.label"),
|
||||
enableClickToCopy: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "storage",
|
||||
header: t("field.storage.label"),
|
||||
},
|
||||
{
|
||||
accessorKey: "storageClassName",
|
||||
header: t("field.storageClassName.label"),
|
||||
enableClickToCopy: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "volumeMode",
|
||||
header: t("field.volumeMode.label"),
|
||||
},
|
||||
{
|
||||
accessorKey: "volumeName",
|
||||
header: t("field.volumeName.label"),
|
||||
enableClickToCopy: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "accessModes",
|
||||
header: t("field.accessModes.label"),
|
||||
Cell({ cell }) {
|
||||
return cell.row.original.accessModes.map((accessMode) => <div key={accessMode}>{accessMode}</div>);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "creationTimestamp",
|
||||
header: t("field.creationTimestamp.label"),
|
||||
Cell: ({ row }) => dayjs(row.original.creationTimestamp).fromNow(false),
|
||||
},
|
||||
];
|
||||
|
||||
export function VolumesTable(initialData: VolumesTableComponentProps) {
|
||||
const tVolumes = useScopedI18n("kubernetes.volumes");
|
||||
|
||||
const { data } = clientApi.kubernetes.volumes.getVolumes.useQuery(undefined, {
|
||||
initialData: initialData.initialVolumes,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
const table = useTranslatedMantineReactTable({
|
||||
data,
|
||||
enableDensityToggle: false,
|
||||
enableColumnActions: false,
|
||||
enableColumnFilters: false,
|
||||
enablePagination: false,
|
||||
enableRowSelection: true,
|
||||
positionToolbarAlertBanner: "top",
|
||||
enableTableFooter: false,
|
||||
enableBottomToolbar: false,
|
||||
positionGlobalFilter: "right",
|
||||
initialState: { density: "xs", showGlobalFilter: true },
|
||||
mantineSearchTextInputProps: {
|
||||
placeholder: tVolumes("table.search", { count: String(data.length) }),
|
||||
style: { minWidth: 300 },
|
||||
autoFocus: true,
|
||||
},
|
||||
|
||||
columns: createColumns(tVolumes),
|
||||
});
|
||||
|
||||
return <MantineReactTable table={table} />;
|
||||
}
|
||||
@@ -66,15 +66,17 @@ export const JobsList = ({ initialJobs }: JobsListProps) => {
|
||||
{job.status && <TimeAgo timestamp={job.status.lastExecutionTimestamp} />}
|
||||
</Stack>
|
||||
|
||||
<ActionIcon
|
||||
onClick={() => handleJobTrigger(job)}
|
||||
disabled={job.status?.status === "running"}
|
||||
variant={"default"}
|
||||
size={"xl"}
|
||||
radius={"xl"}
|
||||
>
|
||||
<IconPlayerPlay stroke={1.5} />
|
||||
</ActionIcon>
|
||||
{!job.job.preventManualExecution && (
|
||||
<ActionIcon
|
||||
onClick={() => handleJobTrigger(job)}
|
||||
disabled={job.status?.status === "running"}
|
||||
variant={"default"}
|
||||
size={"xl"}
|
||||
radius={"xl"}
|
||||
>
|
||||
<IconPlayerPlay stroke={1.5} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
@@ -28,7 +28,8 @@ export const DeleteUserButton = ({ user }: DeleteUserButtonProps) => {
|
||||
() =>
|
||||
openConfirmModal({
|
||||
title: t("user.action.delete.label"),
|
||||
children: t("user.action.delete.confirm", { username: user.name }),
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
children: t("user.action.delete.confirm", { username: user.name! }),
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
async onConfirm() {
|
||||
await mutateUserDeletionAsync({
|
||||
|
||||
@@ -41,7 +41,8 @@ export async function generateMetadata(props: Props) {
|
||||
const t = await getScopedI18n("management.page.user.edit");
|
||||
|
||||
return {
|
||||
title: createMetaTitle(t("metaTitle", { username: user.name })),
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
title: createMetaTitle(t("metaTitle", { username: user.name! })),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -117,15 +118,13 @@ export default async function EditUserPage(props: Props) {
|
||||
<PingIconsEnabled user={user} />
|
||||
</Stack>
|
||||
|
||||
{isCredentialsUser && (
|
||||
<DangerZoneRoot>
|
||||
<DangerZoneItem
|
||||
label={t("user.action.delete.label")}
|
||||
description={t("user.action.delete.description")}
|
||||
action={<DeleteUserButton user={user} />}
|
||||
/>
|
||||
</DangerZoneRoot>
|
||||
)}
|
||||
<DangerZoneRoot>
|
||||
<DangerZoneItem
|
||||
label={t("user.action.delete.label")}
|
||||
description={t("user.action.delete.description")}
|
||||
action={<DeleteUserButton user={user} />}
|
||||
/>
|
||||
</DangerZoneRoot>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -49,6 +49,7 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie
|
||||
integrationIds: [],
|
||||
advancedOptions: {
|
||||
customCssClasses: [],
|
||||
borderColor: "",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ const handlerAsync = async (req: NextRequest) => {
|
||||
endpoint: "/",
|
||||
router: appRouter,
|
||||
createContext: () => createTRPCContext({ session, headers: req.headers }),
|
||||
onError({ error, path, type }) {
|
||||
logger.error(new Error(`tRPC Error with ${type} on '${path}'`, { cause: error.cause }));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -31,9 +31,7 @@ const handler = auth(async (req) => {
|
||||
req,
|
||||
createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }),
|
||||
onError({ error, path, type }) {
|
||||
logger.error(
|
||||
`tRPC Error with ${type} on '${path}': (${error.code}) - ${error.message}\n${error.stack}\n${error.cause}`,
|
||||
);
|
||||
logger.error(new Error(`tRPC Error with ${type} on '${path}'`, { cause: error.cause }));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ export const createItemCallback =
|
||||
integrationIds: [],
|
||||
advancedOptions: {
|
||||
customCssClasses: [],
|
||||
borderColor: "",
|
||||
},
|
||||
} satisfies Modify<
|
||||
Item,
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("item actions duplicate-item", () => {
|
||||
kind: itemKind,
|
||||
integrationIds: ["1"],
|
||||
options: { address: "localhost" },
|
||||
advancedOptions: { customCssClasses: ["test"] },
|
||||
advancedOptions: { customCssClasses: ["test"], borderColor: "#ff0000" },
|
||||
})
|
||||
.addLayout({ layoutId, sectionId: currentSectionId, ...currentItemSize })
|
||||
.build();
|
||||
|
||||
@@ -10,6 +10,7 @@ export class DynamicSectionMockBuilder {
|
||||
id: createId(),
|
||||
kind: "dynamic",
|
||||
options: {
|
||||
title: "",
|
||||
borderColor: "",
|
||||
},
|
||||
layouts: [],
|
||||
|
||||
@@ -14,6 +14,7 @@ export class ItemMockBuilder {
|
||||
integrationIds: [],
|
||||
advancedOptions: {
|
||||
customCssClasses: [],
|
||||
borderColor: "",
|
||||
},
|
||||
...item,
|
||||
} satisfies Item;
|
||||
|
||||
@@ -5,6 +5,8 @@ import combineClasses from "clsx";
|
||||
import { NoIntegrationSelectedError } from "node_modules/@homarr/widgets/src/errors";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { isWidgetRestricted } from "@homarr/auth/shared";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
@@ -15,6 +17,7 @@ import type { SectionItem } from "~/app/[locale]/boards/_types";
|
||||
import classes from "../sections/item.module.css";
|
||||
import { useItemActions } from "./item-actions";
|
||||
import { BoardItemMenu } from "./item-menu";
|
||||
import { RestrictedWidgetContent } from "./restricted";
|
||||
|
||||
interface BoardItemContentProps {
|
||||
item: SectionItem;
|
||||
@@ -40,6 +43,7 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
|
||||
"--opacity": board.opacity / 100,
|
||||
containerType: "size",
|
||||
overflow: item.kind === "iframe" ? "hidden" : undefined,
|
||||
"--border-color": item.advancedOptions.borderColor !== "" ? item.advancedOptions.borderColor : undefined,
|
||||
},
|
||||
}}
|
||||
p={0}
|
||||
@@ -58,6 +62,7 @@ interface InnerContentProps {
|
||||
const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
|
||||
const settings = useSettings();
|
||||
const board = useRequiredBoard();
|
||||
const { data: session } = useSession();
|
||||
const [isEditMode] = useEditMode();
|
||||
const Comp = loadWidgetDynamic(item.kind);
|
||||
const { definition } = widgetImports[item.kind];
|
||||
@@ -69,6 +74,16 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
|
||||
const widgetSupportsIntegrations =
|
||||
"supportedIntegrations" in definition && definition.supportedIntegrations.length >= 1;
|
||||
|
||||
if (
|
||||
isWidgetRestricted({
|
||||
definition,
|
||||
user: session?.user ?? null,
|
||||
check: (level) => level === "all",
|
||||
})
|
||||
) {
|
||||
return <RestrictedWidgetContent kind={item.kind} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryErrorResetBoundary>
|
||||
{({ reset }) => (
|
||||
|
||||
@@ -3,6 +3,8 @@ import { ActionIcon, Menu } from "@mantine/core";
|
||||
import { IconCopy, IconDotsVertical, IconLayoutKanban, IconPencil, IconTrash } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { isWidgetRestricted } from "@homarr/auth/shared";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { useConfirmModal, useModalAction } from "@homarr/modals";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
@@ -37,6 +39,7 @@ export const BoardItemMenu = ({
|
||||
const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]);
|
||||
const { gridstack } = useSectionContext().refs;
|
||||
const settings = useSettings();
|
||||
const { data: session } = useSession();
|
||||
|
||||
// Reset error boundary on next render if item has been edited
|
||||
useEffect(() => {
|
||||
@@ -91,6 +94,16 @@ export const BoardItemMenu = ({
|
||||
});
|
||||
};
|
||||
|
||||
if (
|
||||
isWidgetRestricted({
|
||||
definition: currentDefinition,
|
||||
user: session?.user ?? null,
|
||||
check: (level) => level !== "none",
|
||||
})
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu withinPortal withArrow position="right-start" arrowPosition="center">
|
||||
<Menu.Target>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useMemo, useState } from "react";
|
||||
import { Button, Card, Center, Grid, Input, Stack, Text } from "@mantine/core";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { isWidgetRestricted } from "@homarr/auth/shared";
|
||||
import { objectEntries } from "@homarr/common";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { createModal } from "@homarr/modals";
|
||||
@@ -15,10 +17,18 @@ export const ItemSelectModal = createModal<void>(({ actions }) => {
|
||||
const [search, setSearch] = useState("");
|
||||
const t = useI18n();
|
||||
const { createItem } = useItemActions();
|
||||
const { data: session } = useSession();
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
objectEntries(widgetImports)
|
||||
.filter(([, value]) => {
|
||||
return !isWidgetRestricted({
|
||||
definition: value.definition,
|
||||
user: session?.user ?? null,
|
||||
check: (level) => level !== "none",
|
||||
});
|
||||
})
|
||||
.map(([kind, value]) => ({
|
||||
kind,
|
||||
icon: value.definition.icon,
|
||||
@@ -26,7 +36,7 @@ export const ItemSelectModal = createModal<void>(({ actions }) => {
|
||||
description: t(`widget.${kind}.description`),
|
||||
}))
|
||||
.sort((itemA, itemB) => itemA.name.localeCompare(itemB.name)),
|
||||
[t],
|
||||
[t, session?.user],
|
||||
);
|
||||
|
||||
const filteredItems = useMemo(
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Center, Group, Stack, Text } from "@mantine/core";
|
||||
import { IconShield } from "@tabler/icons-react";
|
||||
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
interface RestrictedWidgetProps {
|
||||
kind: WidgetKind;
|
||||
}
|
||||
|
||||
export const RestrictedWidgetContent = ({ kind }: RestrictedWidgetProps) => {
|
||||
const tCurrentWidget = useScopedI18n(`widget.${kind}`);
|
||||
const tCommonWidget = useScopedI18n("widget.common");
|
||||
|
||||
return (
|
||||
<Center h="100%">
|
||||
<Stack ta="center" gap="xs" align="center">
|
||||
<Group gap="sm">
|
||||
<IconShield size={16} />
|
||||
<Text size="sm" fw="bold">
|
||||
{tCommonWidget("restricted.title")}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm">{tCommonWidget("restricted.description", { name: tCurrentWidget("name") })}</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Card } from "@mantine/core";
|
||||
import { Badge, Box, Card } from "@mantine/core";
|
||||
|
||||
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
|
||||
|
||||
@@ -17,7 +17,12 @@ export const BoardDynamicSection = ({ section }: Props) => {
|
||||
const options = section.options;
|
||||
|
||||
return (
|
||||
<Box className="grid-stack-item-content">
|
||||
<Box
|
||||
className="grid-stack-item-content"
|
||||
style={{
|
||||
overflow: "visible",
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
className={classes.itemCard}
|
||||
w="100%"
|
||||
@@ -25,14 +30,28 @@ export const BoardDynamicSection = ({ section }: Props) => {
|
||||
withBorder
|
||||
styles={{
|
||||
root: {
|
||||
overflow: "visible",
|
||||
"--opacity": board.opacity / 100,
|
||||
overflow: "hidden",
|
||||
"--border-color": options.borderColor !== "" ? options.borderColor : undefined,
|
||||
"--border-color": options.borderColor || undefined,
|
||||
},
|
||||
}}
|
||||
radius={board.itemRadius}
|
||||
p={0}
|
||||
>
|
||||
{options.title && (
|
||||
<Badge
|
||||
pos="absolute"
|
||||
top={-15}
|
||||
left={10}
|
||||
size="md"
|
||||
radius={board.itemRadius}
|
||||
color="var(--background-color)"
|
||||
c="var(--mantine-color-text)"
|
||||
bd="1px solid var(--border-color)"
|
||||
>
|
||||
{options.title}
|
||||
</Badge>
|
||||
)}
|
||||
{/* Use unique key by layout to reinitialize gridstack */}
|
||||
<GridStack key={`${currentLayoutId}-${section.id}`} section={section} className="min-row" />
|
||||
</Card>
|
||||
|
||||
@@ -17,6 +17,7 @@ export const addDynamicSectionCallback = () => (board: Board) => {
|
||||
id: createId(),
|
||||
kind: "dynamic",
|
||||
options: {
|
||||
title: "",
|
||||
borderColor: "",
|
||||
},
|
||||
layouts: createDynamicSectionLayouts(board, firstSection),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button, CloseButton, ColorInput, Group, Stack, useMantineTheme } from "@mantine/core";
|
||||
import { Button, CloseButton, ColorInput, Group, Stack, TextInput, useMantineTheme } from "@mantine/core";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { useZodForm } from "@homarr/form";
|
||||
@@ -30,6 +30,7 @@ export const DynamicSectionEditModal = createModal<ModalProps>(({ actions, inner
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<TextInput label={t("section.dynamic.option.title.label")} {...form.getInputProps("title")} />
|
||||
<ColorInput
|
||||
label={t("section.dynamic.option.borderColor.label")}
|
||||
format="hex"
|
||||
|
||||
@@ -74,7 +74,10 @@ export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuPro
|
||||
leftSection={<IconBellRinging size="1rem" />}
|
||||
>
|
||||
<Text fw="bold" size="sm">
|
||||
{t("updateAvailable", { countUpdates: availableUpdates.length, tag: availableUpdates[0].tagName })}
|
||||
{t("updateAvailable", {
|
||||
countUpdates: String(availableUpdates.length),
|
||||
tag: availableUpdates[0].tagName,
|
||||
})}
|
||||
</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
|
||||
@@ -38,15 +38,15 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.7",
|
||||
"superjson": "2.2.2",
|
||||
"undici": "7.4.0"
|
||||
"undici": "7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node": "^22.13.9",
|
||||
"@types/node": "^22.13.14",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint": "^9.23.0",
|
||||
"prettier": "^3.5.3",
|
||||
"tsx": "4.19.3",
|
||||
"typescript": "^5.8.2"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// This import has to be the first import in the file so that the agent is overridden before any other modules are imported.
|
||||
import "./undici-log-agent-override";
|
||||
|
||||
import { registerCronJobRunner } from "@homarr/cron-job-runner";
|
||||
import { registerCronJobRunner } from "@homarr/cron-job-runner/register";
|
||||
import { jobGroup } from "@homarr/cron-jobs";
|
||||
|
||||
void (async () => {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/ws": "^8.18.0",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint": "^9.23.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
|
||||
@@ -11,4 +11,20 @@ services:
|
||||
container_name: redis
|
||||
image: redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
- "6379:6379"
|
||||
|
||||
mysql:
|
||||
container_name: mysql
|
||||
image: mysql:8.0
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: homarr
|
||||
MYSQL_DATABASE: homarrdb
|
||||
MYSQL_USER: homarr
|
||||
MYSQL_PASSWORD: homarr
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homarr",
|
||||
"version": "1.9.0",
|
||||
"version": "1.13.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env CI=true turbo build",
|
||||
@@ -40,20 +40,20 @@
|
||||
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||
"@turbo/gen": "^2.4.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "^3.0.7",
|
||||
"@vitest/ui": "^3.0.7",
|
||||
"@vitest/coverage-v8": "^3.0.9",
|
||||
"@vitest/ui": "^3.0.9",
|
||||
"conventional-changelog-conventionalcommits": "^8.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"jsdom": "^26.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"semantic-release": "^24.2.3",
|
||||
"testcontainers": "^10.18.0",
|
||||
"testcontainers": "^10.23.0",
|
||||
"turbo": "^2.4.4",
|
||||
"typescript": "^5.8.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.7"
|
||||
"vitest": "^3.0.9"
|
||||
},
|
||||
"packageManager": "pnpm@10.5.2",
|
||||
"packageManager": "pnpm@10.7.0",
|
||||
"engines": {
|
||||
"node": ">=22.14.0"
|
||||
},
|
||||
@@ -69,13 +69,13 @@
|
||||
"tree-sitter",
|
||||
"tree-sitter-json"
|
||||
],
|
||||
"allowNonAppliedPatches": true,
|
||||
"overrides": {
|
||||
"proxmox-api>undici": "7.4.0"
|
||||
"proxmox-api>undici": "7.6.0"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"pretty-print-error": "patches/pretty-print-error.patch"
|
||||
},
|
||||
"allowUnusedPatches": true,
|
||||
"ignoredBuiltDependencies": [
|
||||
"@scarf/scarf",
|
||||
"core-js-pure",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -40,14 +40,15 @@
|
||||
"@homarr/request-handler": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@trpc/client": "next",
|
||||
"@trpc/react-query": "next",
|
||||
"@trpc/server": "next",
|
||||
"@kubernetes/client-node": "^1.1.0",
|
||||
"@trpc/client": "^11.0.1",
|
||||
"@trpc/react-query": "^11.0.1",
|
||||
"@trpc/server": "^11.0.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"next": "15.1.7",
|
||||
"next": "15.2.4",
|
||||
"pretty-print-error": "^1.1.2",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"superjson": "2.2.2",
|
||||
"trpc-to-openapi": "^2.1.3",
|
||||
"zod": "^3.24.2"
|
||||
@@ -56,7 +57,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint": "^9.23.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createEnv } from "@t3-oss/env-nextjs";
|
||||
import { z } from "zod";
|
||||
|
||||
import { shouldSkipEnvValidation } from "@homarr/common/env-validation";
|
||||
|
||||
export const env = createEnv({
|
||||
server: {
|
||||
KUBERNETES_SERVICE_ACCOUNT_NAME: z.string().optional(),
|
||||
},
|
||||
runtimeEnv: {
|
||||
KUBERNETES_SERVICE_ACCOUNT_NAME: process.env.KUBERNETES_SERVICE_ACCOUNT_NAME,
|
||||
},
|
||||
skipValidation: shouldSkipEnvValidation(),
|
||||
emptyStringAsUndefined: true,
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { env } from "@homarr/docker/env";
|
||||
|
||||
import { publicProcedure } from "../trpc";
|
||||
|
||||
export const dockerMiddleware = () => {
|
||||
return publicProcedure.use(async ({ next }) => {
|
||||
if (env.ENABLE_DOCKER) {
|
||||
return await next();
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Docker route is not available",
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { env } from "@homarr/docker/env";
|
||||
|
||||
import { publicProcedure } from "../trpc";
|
||||
|
||||
export const kubernetesMiddleware = () => {
|
||||
return publicProcedure.use(async ({ next }) => {
|
||||
if (env.ENABLE_KUBERNETES) {
|
||||
return await next();
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Kubernetes route is not available",
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import { iconsRouter } from "./router/icons";
|
||||
import { importRouter } from "./router/import/import-router";
|
||||
import { integrationRouter } from "./router/integration/integration-router";
|
||||
import { inviteRouter } from "./router/invite";
|
||||
import { kubernetesRouter } from "./router/kubernetes/router/kubernetes-router";
|
||||
import { locationRouter } from "./router/location";
|
||||
import { logRouter } from "./router/log";
|
||||
import { mediaRouter } from "./router/medias/media-router";
|
||||
@@ -39,6 +40,7 @@ export const appRouter = createTRPCRouter({
|
||||
onboard: onboardRouter,
|
||||
home: homeRouter,
|
||||
docker: dockerRouter,
|
||||
kubernetes: kubernetesRouter,
|
||||
serverSettings: serverSettingsRouter,
|
||||
cronJobs: cronJobsRouter,
|
||||
apiKeys: apiKeysRouter,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server";
|
||||
import superjson from "superjson";
|
||||
import { z } from "zod";
|
||||
|
||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||
import { constructBoardPermissions, isWidgetRestricted } from "@homarr/auth/shared";
|
||||
import type { DeviceType } from "@homarr/common/server";
|
||||
import type { Database, InferInsertModel, InferSelectModel, SQL } from "@homarr/db";
|
||||
import { and, asc, createId, eq, handleTransactionsAsync, inArray, isNull, like, not, or, sql } from "@homarr/db";
|
||||
@@ -40,6 +40,7 @@ import { oldmarrConfigSchema } from "@homarr/old-schema";
|
||||
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
||||
import { sectionSchema, sharedItemSchema, validation, zodUnionFromArray } from "@homarr/validation";
|
||||
|
||||
import { widgetImports } from "../../../widgets/src";
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
|
||||
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
||||
import { generateResponsiveGridFor } from "./board/grid-algorithm";
|
||||
@@ -251,6 +252,13 @@ export const boardRouter = createTRPCRouter({
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const boardId = createId();
|
||||
|
||||
const user = await ctx.db.query.users.findFirst({
|
||||
where: eq(users.id, ctx.session.user.id),
|
||||
columns: {
|
||||
homeBoardId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const createBoardCollection = createDbInsertCollectionWithoutTransaction(["boards", "sections", "layouts"]);
|
||||
|
||||
createBoardCollection.boards.push({
|
||||
@@ -275,6 +283,12 @@ export const boardRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
await createBoardCollection.insertAllAsync(ctx.db);
|
||||
|
||||
if (!user?.homeBoardId) {
|
||||
await ctx.db.update(users).set({ homeBoardId: boardId }).where(eq(users.id, ctx.session.user.id));
|
||||
}
|
||||
|
||||
return { boardId };
|
||||
}),
|
||||
duplicateBoard: permissionRequiredProcedure
|
||||
.requiresPermission("board-create")
|
||||
@@ -310,6 +324,13 @@ export const boardRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const { sections: boardSections, items: boardItems, layouts: boardLayouts, ...boardProps } = board;
|
||||
const allowedBoardItems = boardItems.filter((item) => {
|
||||
return !isWidgetRestricted({
|
||||
definition: widgetImports[item.kind].definition,
|
||||
user: ctx.session.user,
|
||||
check: (level) => level !== "none",
|
||||
});
|
||||
});
|
||||
|
||||
const newBoardId = createId();
|
||||
|
||||
@@ -357,8 +378,8 @@ export const boardRouter = createTRPCRouter({
|
||||
),
|
||||
);
|
||||
|
||||
const itemMap = new Map<string, string>(boardItems.map((item) => [item.id, createId()]));
|
||||
const itemsToInsert: InferInsertModel<typeof items>[] = boardItems.map(
|
||||
const itemMap = new Map<string, string>(allowedBoardItems.map((item) => [item.id, createId()]));
|
||||
const itemsToInsert: InferInsertModel<typeof items>[] = allowedBoardItems.map(
|
||||
({ integrations: _, layouts: _layouts, ...item }) => ({
|
||||
...item,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
@@ -367,7 +388,7 @@ export const boardRouter = createTRPCRouter({
|
||||
}),
|
||||
);
|
||||
|
||||
const itemLayoutsToInsert: InferInsertModel<typeof itemLayouts>[] = boardItems.flatMap((item) =>
|
||||
const itemLayoutsToInsert: InferInsertModel<typeof itemLayouts>[] = allowedBoardItems.flatMap((item) =>
|
||||
item.layouts.map(
|
||||
(layoutSection): InferInsertModel<typeof itemLayouts> => ({
|
||||
...layoutSection,
|
||||
@@ -400,7 +421,7 @@ export const boardRouter = createTRPCRouter({
|
||||
)
|
||||
.then((result) => result.map((row) => row.id));
|
||||
|
||||
const itemIntegrationsToInsert = boardItems.flatMap((item) =>
|
||||
const itemIntegrationsToInsert = allowedBoardItems.flatMap((item) =>
|
||||
item.integrations
|
||||
// Restrict integrations to only those the user has access to
|
||||
.filter(({ integrationId }) => integrationIdsWithAccess.includes(integrationId) || hasAccessForAll)
|
||||
@@ -730,105 +751,140 @@ export const boardRouter = createTRPCRouter({
|
||||
|
||||
const dbBoard = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
|
||||
|
||||
const addedSections = filterAddedItems(input.sections, dbBoard.sections);
|
||||
const sectionsToInsert = addedSections.map(
|
||||
(section): InferInsertModel<typeof sections> => ({
|
||||
id: section.id,
|
||||
kind: section.kind,
|
||||
yOffset: section.kind !== "dynamic" ? section.yOffset : null,
|
||||
xOffset: section.kind === "dynamic" ? null : 0,
|
||||
options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON,
|
||||
name: "name" in section ? section.name : null,
|
||||
boardId: dbBoard.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const sectionLayoutsToInsert = addedSections
|
||||
.filter((section) => section.kind === "dynamic")
|
||||
.flatMap((section) =>
|
||||
section.layouts.map(
|
||||
(sectionLayout): InferInsertModel<typeof sectionLayouts> => ({
|
||||
layoutId: sectionLayout.layoutId,
|
||||
sectionId: section.id,
|
||||
parentSectionId: sectionLayout.parentSectionId,
|
||||
height: sectionLayout.height,
|
||||
width: sectionLayout.width,
|
||||
xOffset: sectionLayout.xOffset,
|
||||
yOffset: sectionLayout.yOffset,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const addedItems = filterAddedItems(input.items, dbBoard.items).filter((item) => {
|
||||
return !isWidgetRestricted({
|
||||
definition: widgetImports[item.kind].definition,
|
||||
user: ctx.session.user,
|
||||
check: (level) => level !== "none",
|
||||
});
|
||||
});
|
||||
const itemsToInsert = addedItems.map(
|
||||
(item): InferInsertModel<typeof items> => ({
|
||||
id: item.id,
|
||||
kind: item.kind,
|
||||
options: superjson.stringify(item.options),
|
||||
advancedOptions: superjson.stringify(item.advancedOptions),
|
||||
boardId: dbBoard.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const itemLayoutsToInsert = addedItems.flatMap((item) =>
|
||||
item.layouts.map(
|
||||
(layoutSection): InferInsertModel<typeof itemLayouts> => ({
|
||||
layoutId: layoutSection.layoutId,
|
||||
sectionId: layoutSection.sectionId,
|
||||
itemId: item.id,
|
||||
height: layoutSection.height,
|
||||
width: layoutSection.width,
|
||||
xOffset: layoutSection.xOffset,
|
||||
yOffset: layoutSection.yOffset,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const inputIntegrationRelations = input.items.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const dbIntegrationRelations = dbBoard.items.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const addedIntegrationRelations = inputIntegrationRelations.filter(
|
||||
(inputRelation) =>
|
||||
!dbIntegrationRelations.some(
|
||||
(dbRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId && dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
const integrationItemsToInsert = addedIntegrationRelations.map((relation) => ({
|
||||
itemId: relation.itemId,
|
||||
integrationId: relation.integrationId,
|
||||
}));
|
||||
|
||||
const updatedItems = filterUpdatedItems(input.items, dbBoard.items).filter((item) => {
|
||||
return !isWidgetRestricted({
|
||||
definition: widgetImports[item.kind].definition,
|
||||
user: ctx.session.user,
|
||||
check: (level) => level !== "none",
|
||||
});
|
||||
});
|
||||
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
|
||||
|
||||
const removedIntegrationRelations = dbIntegrationRelations.filter(
|
||||
(dbRelation) =>
|
||||
!inputIntegrationRelations.some(
|
||||
(inputRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId && dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
const removedItems = filterRemovedItems(input.items, dbBoard.items).filter((item) => {
|
||||
return !isWidgetRestricted({
|
||||
definition: widgetImports[item.kind].definition,
|
||||
user: ctx.session.user,
|
||||
check: (level) => level !== "none",
|
||||
});
|
||||
});
|
||||
const itemIdsToRemove = removedItems.map((item) => item.id);
|
||||
|
||||
const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
|
||||
const sectionIdsToRemove = removedSections.map((section) => section.id);
|
||||
|
||||
await handleTransactionsAsync(ctx.db, {
|
||||
async handleAsync(db, schema) {
|
||||
await db.transaction(async (transaction) => {
|
||||
const addedSections = filterAddedItems(input.sections, dbBoard.sections);
|
||||
|
||||
if (addedSections.length > 0) {
|
||||
await transaction.insert(schema.sections).values(
|
||||
addedSections.map((section) => ({
|
||||
id: section.id,
|
||||
kind: section.kind,
|
||||
yOffset: section.kind !== "dynamic" ? section.yOffset : null,
|
||||
xOffset: section.kind === "dynamic" ? null : 0,
|
||||
options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON,
|
||||
name: "name" in section ? section.name : null,
|
||||
boardId: dbBoard.id,
|
||||
})),
|
||||
);
|
||||
|
||||
if (addedSections.some((section) => section.kind === "dynamic")) {
|
||||
await transaction.insert(schema.sectionLayouts).values(
|
||||
addedSections
|
||||
.filter((section) => section.kind === "dynamic")
|
||||
.flatMap((section) =>
|
||||
section.layouts.map(
|
||||
(sectionLayout): InferInsertModel<typeof schema.sectionLayouts> => ({
|
||||
layoutId: sectionLayout.layoutId,
|
||||
sectionId: section.id,
|
||||
parentSectionId: sectionLayout.parentSectionId,
|
||||
height: sectionLayout.height,
|
||||
width: sectionLayout.width,
|
||||
xOffset: sectionLayout.xOffset,
|
||||
yOffset: sectionLayout.yOffset,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (sectionsToInsert.length > 0) {
|
||||
await transaction.insert(schema.sections).values(sectionsToInsert);
|
||||
}
|
||||
|
||||
const addedItems = filterAddedItems(input.items, dbBoard.items);
|
||||
|
||||
if (addedItems.length > 0) {
|
||||
await transaction.insert(schema.items).values(
|
||||
addedItems.map((item) => ({
|
||||
id: item.id,
|
||||
kind: item.kind,
|
||||
options: superjson.stringify(item.options),
|
||||
advancedOptions: superjson.stringify(item.advancedOptions),
|
||||
boardId: dbBoard.id,
|
||||
})),
|
||||
);
|
||||
await transaction.insert(schema.itemLayouts).values(
|
||||
addedItems.flatMap((item) =>
|
||||
item.layouts.map(
|
||||
(layoutSection): InferInsertModel<typeof schema.itemLayouts> => ({
|
||||
layoutId: layoutSection.layoutId,
|
||||
sectionId: layoutSection.sectionId,
|
||||
itemId: item.id,
|
||||
height: layoutSection.height,
|
||||
width: layoutSection.width,
|
||||
xOffset: layoutSection.xOffset,
|
||||
yOffset: layoutSection.yOffset,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (sectionLayoutsToInsert.length > 0) {
|
||||
await transaction.insert(schema.sectionLayouts).values(sectionLayoutsToInsert);
|
||||
}
|
||||
|
||||
const inputIntegrationRelations = input.items.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const dbIntegrationRelations = dbBoard.items.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const addedIntegrationRelations = inputIntegrationRelations.filter(
|
||||
(inputRelation) =>
|
||||
!dbIntegrationRelations.some(
|
||||
(dbRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId &&
|
||||
dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
if (addedIntegrationRelations.length > 0) {
|
||||
await transaction.insert(schema.integrationItems).values(
|
||||
addedIntegrationRelations.map((relation) => ({
|
||||
itemId: relation.itemId,
|
||||
integrationId: relation.integrationId,
|
||||
})),
|
||||
);
|
||||
if (itemsToInsert.length > 0) {
|
||||
await transaction.insert(schema.items).values(itemsToInsert);
|
||||
}
|
||||
if (itemLayoutsToInsert.length > 0) {
|
||||
await transaction.insert(schema.itemLayouts).values(itemLayoutsToInsert);
|
||||
}
|
||||
|
||||
const updatedItems = filterUpdatedItems(input.items, dbBoard.items);
|
||||
if (integrationItemsToInsert.length > 0) {
|
||||
await transaction.insert(schema.integrationItems).values(integrationItemsToInsert);
|
||||
}
|
||||
|
||||
for (const item of updatedItems) {
|
||||
await transaction
|
||||
@@ -859,8 +915,6 @@ export const boardRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
|
||||
|
||||
for (const section of updatedSections) {
|
||||
const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id);
|
||||
await transaction
|
||||
@@ -894,15 +948,6 @@ export const boardRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
const removedIntegrationRelations = dbIntegrationRelations.filter(
|
||||
(dbRelation) =>
|
||||
!inputIntegrationRelations.some(
|
||||
(inputRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId &&
|
||||
dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
for (const relation of removedIntegrationRelations) {
|
||||
await transaction
|
||||
.delete(schema.integrationItems)
|
||||
@@ -914,134 +959,36 @@ export const boardRouter = createTRPCRouter({
|
||||
);
|
||||
}
|
||||
|
||||
const removedItems = filterRemovedItems(input.items, dbBoard.items);
|
||||
|
||||
const itemIds = removedItems.map((item) => item.id);
|
||||
if (itemIds.length > 0) {
|
||||
await transaction.delete(schema.items).where(inArray(schema.items.id, itemIds));
|
||||
if (itemIdsToRemove.length > 0) {
|
||||
await transaction.delete(schema.items).where(inArray(schema.items.id, itemIdsToRemove));
|
||||
}
|
||||
|
||||
const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
|
||||
const sectionIds = removedSections.map((section) => section.id);
|
||||
|
||||
if (sectionIds.length > 0) {
|
||||
await transaction.delete(schema.sections).where(inArray(schema.sections.id, sectionIds));
|
||||
if (sectionIdsToRemove.length > 0) {
|
||||
await transaction.delete(schema.sections).where(inArray(schema.sections.id, sectionIdsToRemove));
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSync(db) {
|
||||
db.transaction((transaction) => {
|
||||
const addedSections = filterAddedItems(input.sections, dbBoard.sections);
|
||||
|
||||
if (addedSections.length > 0) {
|
||||
transaction
|
||||
.insert(sections)
|
||||
.values(
|
||||
addedSections.map((section) => ({
|
||||
id: section.id,
|
||||
kind: section.kind,
|
||||
yOffset: section.kind !== "dynamic" ? section.yOffset : null,
|
||||
xOffset: section.kind === "dynamic" ? null : 0,
|
||||
options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON,
|
||||
name: "name" in section ? section.name : null,
|
||||
boardId: dbBoard.id,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
|
||||
if (addedSections.some((section) => section.kind === "dynamic")) {
|
||||
transaction
|
||||
.insert(sectionLayouts)
|
||||
.values(
|
||||
addedSections
|
||||
.filter((section) => section.kind === "dynamic")
|
||||
.flatMap((section) =>
|
||||
section.layouts.map(
|
||||
(sectionLayout): InferInsertModel<typeof sectionLayouts> => ({
|
||||
layoutId: sectionLayout.layoutId,
|
||||
sectionId: section.id,
|
||||
parentSectionId: sectionLayout.parentSectionId,
|
||||
height: sectionLayout.height,
|
||||
width: sectionLayout.width,
|
||||
xOffset: sectionLayout.xOffset,
|
||||
yOffset: sectionLayout.yOffset,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
if (sectionsToInsert.length > 0) {
|
||||
transaction.insert(sections).values(sectionsToInsert).run();
|
||||
}
|
||||
|
||||
const addedItems = filterAddedItems(input.items, dbBoard.items);
|
||||
|
||||
if (addedItems.length > 0) {
|
||||
transaction
|
||||
.insert(items)
|
||||
.values(
|
||||
addedItems.map((item) => ({
|
||||
id: item.id,
|
||||
kind: item.kind,
|
||||
options: superjson.stringify(item.options),
|
||||
advancedOptions: superjson.stringify(item.advancedOptions),
|
||||
boardId: dbBoard.id,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
transaction
|
||||
.insert(itemLayouts)
|
||||
.values(
|
||||
addedItems.flatMap((item) =>
|
||||
item.layouts.map(
|
||||
(layoutSection): InferInsertModel<typeof itemLayouts> => ({
|
||||
layoutId: layoutSection.layoutId,
|
||||
sectionId: layoutSection.sectionId,
|
||||
itemId: item.id,
|
||||
height: layoutSection.height,
|
||||
width: layoutSection.width,
|
||||
xOffset: layoutSection.xOffset,
|
||||
yOffset: layoutSection.yOffset,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
if (sectionLayoutsToInsert.length > 0) {
|
||||
transaction.insert(sectionLayouts).values(sectionLayoutsToInsert).run();
|
||||
}
|
||||
|
||||
const inputIntegrationRelations = input.items.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const dbIntegrationRelations = dbBoard.items.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const addedIntegrationRelations = inputIntegrationRelations.filter(
|
||||
(inputRelation) =>
|
||||
!dbIntegrationRelations.some(
|
||||
(dbRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId &&
|
||||
dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
if (addedIntegrationRelations.length > 0) {
|
||||
transaction
|
||||
.insert(integrationItems)
|
||||
.values(
|
||||
addedIntegrationRelations.map((relation) => ({
|
||||
itemId: relation.itemId,
|
||||
integrationId: relation.integrationId,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
if (itemsToInsert.length > 0) {
|
||||
transaction.insert(items).values(itemsToInsert).run();
|
||||
}
|
||||
|
||||
const updatedItems = filterUpdatedItems(input.items, dbBoard.items);
|
||||
if (itemLayoutsToInsert.length > 0) {
|
||||
transaction.insert(itemLayouts).values(itemLayoutsToInsert).run();
|
||||
}
|
||||
|
||||
if (integrationItemsToInsert.length > 0) {
|
||||
transaction.insert(integrationItems).values(integrationItemsToInsert).run();
|
||||
}
|
||||
|
||||
for (const item of updatedItems) {
|
||||
transaction
|
||||
@@ -1069,8 +1016,6 @@ export const boardRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
|
||||
|
||||
for (const section of updatedSections) {
|
||||
const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id);
|
||||
transaction
|
||||
@@ -1103,15 +1048,6 @@ export const boardRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
const removedIntegrationRelations = dbIntegrationRelations.filter(
|
||||
(dbRelation) =>
|
||||
!inputIntegrationRelations.some(
|
||||
(inputRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId &&
|
||||
dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
for (const relation of removedIntegrationRelations) {
|
||||
transaction
|
||||
.delete(integrationItems)
|
||||
@@ -1124,18 +1060,12 @@ export const boardRouter = createTRPCRouter({
|
||||
.run();
|
||||
}
|
||||
|
||||
const removedItems = filterRemovedItems(input.items, dbBoard.items);
|
||||
|
||||
const itemIds = removedItems.map((item) => item.id);
|
||||
if (itemIds.length > 0) {
|
||||
transaction.delete(items).where(inArray(items.id, itemIds)).run();
|
||||
if (itemIdsToRemove.length > 0) {
|
||||
transaction.delete(items).where(inArray(items.id, itemIdsToRemove)).run();
|
||||
}
|
||||
|
||||
const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
|
||||
const sectionIds = removedSections.map((section) => section.id);
|
||||
|
||||
if (sectionIds.length > 0) {
|
||||
transaction.delete(sections).where(inArray(sections.id, sectionIds)).run();
|
||||
if (sectionIdsToRemove.length > 0) {
|
||||
transaction.delete(sections).where(inArray(sections.id, sectionIdsToRemove)).run();
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -1305,7 +1235,7 @@ export const boardRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const content = await input.file.text();
|
||||
const oldmarr = oldmarrConfigSchema.parse(JSON.parse(content));
|
||||
await importOldmarrAsync(ctx.db, oldmarr, input.configuration);
|
||||
await importOldmarrAsync(ctx.db, oldmarr, input.configuration, ctx.session);
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
|
||||
import { jobNameSchema, triggerCronJobAsync } from "@homarr/cron-job-runner";
|
||||
import { objectEntries } from "@homarr/common";
|
||||
import { cronJobNames, cronJobs, jobNameSchema, triggerCronJobAsync } from "@homarr/cron-job-runner";
|
||||
import type { TaskStatus } from "@homarr/cron-job-status";
|
||||
import { createCronJobStatusChannel } from "@homarr/cron-job-status";
|
||||
import { jobGroup } from "@homarr/cron-jobs";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
|
||||
@@ -16,18 +16,17 @@ export const cronJobsRouter = createTRPCRouter({
|
||||
await triggerCronJobAsync(input);
|
||||
}),
|
||||
getJobs: permissionRequiredProcedure.requiresPermission("admin").query(() => {
|
||||
const registry = jobGroup.getJobRegistry();
|
||||
return [...registry.values()].map((job) => ({
|
||||
name: job.name,
|
||||
expression: job.cronExpression,
|
||||
return objectEntries(cronJobs).map(([name, options]) => ({
|
||||
name,
|
||||
preventManualExecution: options.preventManualExecution,
|
||||
}));
|
||||
}),
|
||||
subscribeToStatusUpdates: permissionRequiredProcedure.requiresPermission("admin").subscription(() => {
|
||||
return observable<TaskStatus>((emit) => {
|
||||
const unsubscribes: (() => void)[] = [];
|
||||
|
||||
for (const job of jobGroup.getJobRegistry().values()) {
|
||||
const channel = createCronJobStatusChannel(job.name);
|
||||
for (const name of cronJobNames) {
|
||||
const channel = createCronJobStatusChannel(name);
|
||||
const unsubscribe = channel.subscribe((data) => {
|
||||
emit.next(data);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { Container, ContainerInfo, ContainerState, Docker, Port } from "@ho
|
||||
import { logger } from "@homarr/log";
|
||||
import { createCacheChannel } from "@homarr/redis";
|
||||
|
||||
import { dockerMiddleware } from "../../middlewares/docker";
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
|
||||
|
||||
const dockerCache = createCacheChannel<{
|
||||
@@ -15,72 +16,79 @@ const dockerCache = createCacheChannel<{
|
||||
}>("docker-containers", 5 * 60 * 1000);
|
||||
|
||||
export const dockerRouter = createTRPCRouter({
|
||||
getContainers: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
|
||||
const result = await dockerCache
|
||||
.consumeAsync(async () => {
|
||||
const dockerInstances = DockerSingleton.getInstances();
|
||||
const containers = await Promise.all(
|
||||
// Return all the containers of all the instances into only one item
|
||||
dockerInstances.map(({ instance, host: key }) =>
|
||||
instance.listContainers({ all: true }).then((containers) =>
|
||||
containers.map((container) => ({
|
||||
...container,
|
||||
instance: key,
|
||||
})),
|
||||
getContainers: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(dockerMiddleware())
|
||||
.query(async () => {
|
||||
const result = await dockerCache
|
||||
.consumeAsync(async () => {
|
||||
const dockerInstances = DockerSingleton.getInstances();
|
||||
const containers = await Promise.all(
|
||||
// Return all the containers of all the instances into only one item
|
||||
dockerInstances.map(({ instance, host: key }) =>
|
||||
instance.listContainers({ all: true }).then((containers) =>
|
||||
containers.map((container) => ({
|
||||
...container,
|
||||
instance: key,
|
||||
})),
|
||||
),
|
||||
),
|
||||
),
|
||||
).then((containers) => containers.flat());
|
||||
).then((containers) => containers.flat());
|
||||
|
||||
const extractImage = (container: ContainerInfo) => container.Image.split("/").at(-1)?.split(":").at(0) ?? "";
|
||||
const likeQueries = containers.map((container) => like(icons.name, `%${extractImage(container)}%`));
|
||||
const dbIcons =
|
||||
likeQueries.length >= 1
|
||||
? await db.query.icons.findMany({
|
||||
where: or(...likeQueries),
|
||||
})
|
||||
: [];
|
||||
const extractImage = (container: ContainerInfo) => container.Image.split("/").at(-1)?.split(":").at(0) ?? "";
|
||||
const likeQueries = containers.map((container) => like(icons.name, `%${extractImage(container)}%`));
|
||||
const dbIcons =
|
||||
likeQueries.length >= 1
|
||||
? await db.query.icons.findMany({
|
||||
where: or(...likeQueries),
|
||||
})
|
||||
: [];
|
||||
|
||||
return {
|
||||
containers: containers.map((container) => ({
|
||||
...container,
|
||||
iconUrl:
|
||||
dbIcons.find((icon) => {
|
||||
const extractedImage = extractImage(container);
|
||||
if (!extractedImage) return false;
|
||||
return icon.name.toLowerCase().includes(extractedImage.toLowerCase());
|
||||
})?.url ?? null,
|
||||
})),
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(error);
|
||||
return {
|
||||
isError: true,
|
||||
error: error as unknown,
|
||||
};
|
||||
});
|
||||
return {
|
||||
containers: containers.map((container) => ({
|
||||
...container,
|
||||
iconUrl:
|
||||
dbIcons.find((icon) => {
|
||||
const extractedImage = extractImage(container);
|
||||
if (!extractedImage) return false;
|
||||
return icon.name.toLowerCase().includes(extractedImage.toLowerCase());
|
||||
})?.url ?? null,
|
||||
})),
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(error);
|
||||
return {
|
||||
isError: true,
|
||||
error: error as unknown,
|
||||
};
|
||||
});
|
||||
|
||||
if ("isError" in result) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching the containers",
|
||||
cause: result.error,
|
||||
});
|
||||
}
|
||||
if ("isError" in result) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching the containers",
|
||||
cause: result.error,
|
||||
});
|
||||
}
|
||||
|
||||
const { data, timestamp } = result;
|
||||
const { data, timestamp } = result;
|
||||
|
||||
return {
|
||||
containers: sanitizeContainers(data.containers),
|
||||
timestamp,
|
||||
};
|
||||
}),
|
||||
invalidate: permissionRequiredProcedure.requiresPermission("admin").mutation(async () => {
|
||||
await dockerCache.invalidateAsync();
|
||||
return;
|
||||
}),
|
||||
return {
|
||||
containers: sanitizeContainers(data.containers),
|
||||
timestamp,
|
||||
};
|
||||
}),
|
||||
invalidate: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(dockerMiddleware())
|
||||
.mutation(async () => {
|
||||
await dockerCache.invalidateAsync();
|
||||
return;
|
||||
}),
|
||||
startAll: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(dockerMiddleware())
|
||||
.input(z.object({ ids: z.array(z.string()) }))
|
||||
.mutation(async ({ input }) => {
|
||||
await Promise.allSettled(
|
||||
@@ -94,6 +102,7 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
stopAll: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(dockerMiddleware())
|
||||
.input(z.object({ ids: z.array(z.string()) }))
|
||||
.mutation(async ({ input }) => {
|
||||
await Promise.allSettled(
|
||||
@@ -107,6 +116,7 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
restartAll: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(dockerMiddleware())
|
||||
.input(z.object({ ids: z.array(z.string()) }))
|
||||
.mutation(async ({ input }) => {
|
||||
await Promise.allSettled(
|
||||
@@ -120,6 +130,7 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
removeAll: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(dockerMiddleware())
|
||||
.input(z.object({ ids: z.array(z.string()) }))
|
||||
.mutation(async ({ input }) => {
|
||||
await Promise.allSettled(
|
||||
|
||||
@@ -37,7 +37,7 @@ export const importRouter = createTRPCRouter({
|
||||
.requiresStep("import")
|
||||
.input(importInitialOldmarrInputSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await importInitialOldmarrAsync(ctx.db, input);
|
||||
await importInitialOldmarrAsync(ctx.db, input, ctx.session);
|
||||
await nextOnboardingStepAsync(ctx.db, undefined);
|
||||
}),
|
||||
});
|
||||
@@ -1,5 +1,3 @@
|
||||
import { formatError } from "pretty-print-error";
|
||||
|
||||
import { decryptSecret } from "@homarr/common/server";
|
||||
import type { Integration } from "@homarr/db/schema";
|
||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||
@@ -41,7 +39,10 @@ export const testConnectionAsync = async (
|
||||
};
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Failed to decrypt secret from database integration="${integration.name}" secretKind="${secret.kind}"\n${formatError(error)}`,
|
||||
new Error(
|
||||
`Failed to decrypt secret from database integration="${integration.name}" secretKind="${secret.kind}"`,
|
||||
{ cause: error },
|
||||
),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as fs from "fs";
|
||||
import { CoreV1Api, KubeConfig, Metrics, NetworkingV1Api, VersionApi } from "@kubernetes/client-node";
|
||||
|
||||
import { env } from "../../env";
|
||||
|
||||
export class KubernetesClient {
|
||||
private static instance: KubernetesClient | null = null;
|
||||
public kubeConfig: KubeConfig;
|
||||
public coreApi: CoreV1Api;
|
||||
public networkingApi: NetworkingV1Api;
|
||||
public metricsApi: Metrics;
|
||||
public versionApi: VersionApi;
|
||||
|
||||
private constructor() {
|
||||
this.kubeConfig = new KubeConfig();
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
this.kubeConfig.loadFromDefault();
|
||||
} else {
|
||||
this.kubeConfig.loadFromCluster();
|
||||
|
||||
const currentCluster = this.kubeConfig.getCurrentCluster();
|
||||
if (!currentCluster) throw new Error("No cluster configuration found");
|
||||
|
||||
const token = fs.readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8");
|
||||
const caData = fs.readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", "utf8");
|
||||
|
||||
const clusterWithCA = {
|
||||
...currentCluster,
|
||||
name: `${currentCluster.name}-service-account`,
|
||||
caData,
|
||||
};
|
||||
|
||||
const serviceAccountUser = {
|
||||
name: env.KUBERNETES_SERVICE_ACCOUNT_NAME ?? "default-sa",
|
||||
token,
|
||||
};
|
||||
|
||||
this.kubeConfig.clusters = [];
|
||||
this.kubeConfig.users = [];
|
||||
|
||||
this.kubeConfig.addCluster(clusterWithCA);
|
||||
this.kubeConfig.addUser(serviceAccountUser);
|
||||
|
||||
const currentContext = this.kubeConfig.getCurrentContext();
|
||||
const originalContext = this.kubeConfig.getContextObject(currentContext);
|
||||
if (!originalContext) throw new Error("No context found");
|
||||
|
||||
const updatedContext = {
|
||||
...originalContext,
|
||||
name: `${originalContext.name}-service-account`,
|
||||
cluster: clusterWithCA.name,
|
||||
user: serviceAccountUser.name,
|
||||
};
|
||||
|
||||
this.kubeConfig.contexts = [];
|
||||
this.kubeConfig.addContext(updatedContext);
|
||||
this.kubeConfig.setCurrentContext(updatedContext.name);
|
||||
}
|
||||
|
||||
this.coreApi = this.kubeConfig.makeApiClient(CoreV1Api);
|
||||
this.networkingApi = this.kubeConfig.makeApiClient(NetworkingV1Api);
|
||||
this.metricsApi = new Metrics(this.kubeConfig);
|
||||
this.versionApi = this.kubeConfig.makeApiClient(VersionApi);
|
||||
}
|
||||
|
||||
public static getInstance(): KubernetesClient {
|
||||
KubernetesClient.instance ??= new KubernetesClient();
|
||||
return KubernetesClient.instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { ResourceParser } from "./resource-parser";
|
||||
|
||||
export class CpuResourceParser implements ResourceParser {
|
||||
private readonly billionthsCore = 1_000_000_000;
|
||||
private readonly millionthsCore = 1_000_000;
|
||||
private readonly MiliCore = 1_000;
|
||||
private readonly ThousandCore = 1_000;
|
||||
|
||||
parse(value: string): number {
|
||||
if (!value.length) {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
value = value.replace(/,/g, "").trim();
|
||||
|
||||
const [, numericValue, unit = ""] = /^([0-9.]+)\s*([a-zA-Z]*)$/.exec(value) ?? [];
|
||||
|
||||
if (numericValue === undefined) {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
const parsedValue = parseFloat(numericValue);
|
||||
|
||||
if (isNaN(parsedValue)) {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
switch (unit.toLowerCase()) {
|
||||
case "n": // nano-cores (billionths of a core)
|
||||
return parsedValue / this.billionthsCore; // 1 NanoCPU = 1/1,000,000,000 cores
|
||||
case "u": // micro-cores (millionths of a core)
|
||||
return parsedValue / this.millionthsCore; // 1 MicroCPU = 1/1,000,000 cores
|
||||
case "m": // milli-cores
|
||||
return parsedValue / this.MiliCore; // 1 milli-core = 1/1000 cores
|
||||
case "k": // thousands of cores
|
||||
return parsedValue * this.ThousandCore; // 1 thousand-core = 1000 cores
|
||||
default: // cores (no unit)
|
||||
return parsedValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { ResourceParser } from "./resource-parser";
|
||||
|
||||
export class MemoryResourceParser implements ResourceParser {
|
||||
private readonly binaryMultipliers: Record<string, number> = {
|
||||
ki: 1024,
|
||||
mi: 1024 ** 2,
|
||||
gi: 1024 ** 3,
|
||||
ti: 1024 ** 4,
|
||||
pi: 1024 ** 5,
|
||||
} as const;
|
||||
|
||||
private readonly decimalMultipliers: Record<string, number> = {
|
||||
k: 1000,
|
||||
m: 1000 ** 2,
|
||||
g: 1000 ** 3,
|
||||
t: 1000 ** 4,
|
||||
p: 1000 ** 5,
|
||||
} as const;
|
||||
|
||||
parse(value: string): number {
|
||||
if (!value.length) {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
value = value.replace(/,/g, "").trim();
|
||||
|
||||
const [, numericValue, unit = ""] = /^([0-9.]+)\s*([a-zA-Z]*)$/.exec(value) ?? [];
|
||||
|
||||
if (!numericValue) {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
const parsedValue = parseFloat(numericValue);
|
||||
|
||||
if (isNaN(parsedValue)) {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
const unitLower = unit.toLowerCase();
|
||||
|
||||
// Handle binary units (Ki, Mi, Gi, etc.)
|
||||
if (unitLower in this.binaryMultipliers) {
|
||||
const multiplier = this.binaryMultipliers[unitLower];
|
||||
const giMultiplier = this.binaryMultipliers.gi;
|
||||
|
||||
if (multiplier !== undefined && giMultiplier !== undefined) {
|
||||
return (parsedValue * multiplier) / giMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle decimal units (K, M, G, etc.)
|
||||
if (unitLower in this.decimalMultipliers) {
|
||||
const multiplier = this.decimalMultipliers[unitLower];
|
||||
const giMultiplier = this.binaryMultipliers.gi;
|
||||
|
||||
if (multiplier !== undefined && giMultiplier !== undefined) {
|
||||
return (parsedValue * multiplier) / giMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
// No unit or unrecognized unit, assume bytes and convert to GiB
|
||||
const giMultiplier = this.binaryMultipliers.gi;
|
||||
if (giMultiplier !== undefined) {
|
||||
return parsedValue / giMultiplier;
|
||||
}
|
||||
|
||||
return NaN; // Return NaN if giMultiplier is undefined
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface ResourceParser {
|
||||
parse(value: string): number;
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import type { V1NodeList, VersionInfo } from "@kubernetes/client-node";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { ClusterResourceCount, KubernetesCluster } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||
import { KubernetesClient } from "../kubernetes-client";
|
||||
import { CpuResourceParser } from "../resource-parser/cpu-resource-parser";
|
||||
import { MemoryResourceParser } from "../resource-parser/memory-resource-parser";
|
||||
|
||||
export const clusterRouter = createTRPCRouter({
|
||||
getCluster: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(kubernetesMiddleware())
|
||||
.query(async (): Promise<KubernetesCluster> => {
|
||||
const { coreApi, metricsApi, versionApi, kubeConfig } = KubernetesClient.getInstance();
|
||||
|
||||
try {
|
||||
const versionInfo = await versionApi.getCode();
|
||||
const nodes = await coreApi.listNode();
|
||||
const nodeMetricsClient = await metricsApi.getNodeMetrics();
|
||||
const listPodForAllNamespaces = await coreApi.listPodForAllNamespaces();
|
||||
|
||||
let totalCPUCapacity = 0;
|
||||
let totalCPUAllocatable = 0;
|
||||
let totalCPUUsage = 0;
|
||||
|
||||
let totalMemoryCapacity = 0;
|
||||
let totalMemoryAllocatable = 0;
|
||||
let totalMemoryUsage = 0;
|
||||
|
||||
let totalCapacityPods = 0;
|
||||
const cpuResourceParser = new CpuResourceParser();
|
||||
const memoryResourceParser = new MemoryResourceParser();
|
||||
|
||||
nodes.items.forEach((node) => {
|
||||
totalCapacityPods += Number(node.status?.capacity?.pods);
|
||||
|
||||
const cpuCapacity = cpuResourceParser.parse(node.status?.capacity?.cpu ?? "0");
|
||||
const cpuAllocatable = cpuResourceParser.parse(node.status?.allocatable?.cpu ?? "0");
|
||||
totalCPUCapacity += cpuCapacity;
|
||||
totalCPUAllocatable += cpuAllocatable;
|
||||
|
||||
const memoryCapacity = memoryResourceParser.parse(node.status?.capacity?.memory ?? "0");
|
||||
const memoryAllocatable = memoryResourceParser.parse(node.status?.allocatable?.memory ?? "0");
|
||||
totalMemoryCapacity += memoryCapacity;
|
||||
totalMemoryAllocatable += memoryAllocatable;
|
||||
|
||||
const nodeName = node.metadata?.name;
|
||||
const nodeMetric = nodeMetricsClient.items.find((metric) => metric.metadata.name === nodeName);
|
||||
if (nodeMetric) {
|
||||
const cpuUsage = cpuResourceParser.parse(nodeMetric.usage.cpu);
|
||||
totalCPUUsage += cpuUsage;
|
||||
|
||||
const memoryUsage = memoryResourceParser.parse(nodeMetric.usage.memory);
|
||||
totalMemoryUsage += memoryUsage;
|
||||
}
|
||||
});
|
||||
|
||||
const reservedCPU = totalCPUCapacity - totalCPUAllocatable;
|
||||
const reservedMemory = totalMemoryCapacity - totalMemoryAllocatable;
|
||||
|
||||
const reservedCPUPercentage = (reservedCPU / totalCPUCapacity) * 100;
|
||||
const reservedMemoryPercentage = (reservedMemory / totalMemoryCapacity) * 100;
|
||||
|
||||
const usagePercentageAllocatable = (totalCPUUsage / totalCPUAllocatable) * 100;
|
||||
const usagePercentageMemoryAllocatable = (totalMemoryUsage / totalMemoryAllocatable) * 100;
|
||||
|
||||
const usedPodsPercentage = (listPodForAllNamespaces.items.length / totalCapacityPods) * 100;
|
||||
|
||||
return {
|
||||
name: kubeConfig.getCurrentContext(),
|
||||
providers: getProviders(versionInfo, nodes),
|
||||
kubernetesVersion: versionInfo.gitVersion,
|
||||
architecture: versionInfo.platform,
|
||||
nodeCount: nodes.items.length,
|
||||
capacity: [
|
||||
{
|
||||
type: "CPU",
|
||||
resourcesStats: [
|
||||
{
|
||||
percentageValue: Number(reservedCPUPercentage.toFixed(2)),
|
||||
type: "Reserved",
|
||||
capacityUnit: "Cores",
|
||||
usedValue: Number(reservedCPU.toFixed(2)),
|
||||
maxUsedValue: Number(totalCPUCapacity.toFixed(2)),
|
||||
},
|
||||
{
|
||||
percentageValue: Number(usagePercentageAllocatable.toFixed(2)),
|
||||
type: "Used",
|
||||
capacityUnit: "Cores",
|
||||
usedValue: Number(totalCPUUsage.toFixed(2)),
|
||||
maxUsedValue: Number(totalCPUAllocatable.toFixed(2)),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "Memory",
|
||||
resourcesStats: [
|
||||
{
|
||||
percentageValue: Number(reservedMemoryPercentage.toFixed(2)),
|
||||
type: "Reserved",
|
||||
capacityUnit: "GiB",
|
||||
usedValue: Number(reservedMemory.toFixed(2)),
|
||||
maxUsedValue: Number(totalMemoryCapacity.toFixed(2)),
|
||||
},
|
||||
{
|
||||
percentageValue: Number(usagePercentageMemoryAllocatable.toFixed(2)),
|
||||
type: "Used",
|
||||
capacityUnit: "GiB",
|
||||
usedValue: Number(totalMemoryUsage.toFixed(2)),
|
||||
maxUsedValue: Number(totalMemoryAllocatable.toFixed(2)),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "Pods",
|
||||
resourcesStats: [
|
||||
{
|
||||
percentageValue: Number(usedPodsPercentage.toFixed(2)),
|
||||
type: "Used",
|
||||
usedValue: listPodForAllNamespaces.items.length,
|
||||
maxUsedValue: totalCapacityPods,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("Unable to retrieve cluster", error);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching Kubernetes cluster",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
getClusterResourceCounts: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.query(async (): Promise<ClusterResourceCount[]> => {
|
||||
const { coreApi, networkingApi } = KubernetesClient.getInstance();
|
||||
|
||||
try {
|
||||
const [pods, ingresses, services, configMaps, namespaces, nodes, secrets, volumes] = await Promise.all([
|
||||
coreApi.listPodForAllNamespaces(),
|
||||
networkingApi.listIngressForAllNamespaces(),
|
||||
coreApi.listServiceForAllNamespaces(),
|
||||
coreApi.listConfigMapForAllNamespaces(),
|
||||
coreApi.listNamespace(),
|
||||
coreApi.listNode(),
|
||||
coreApi.listSecretForAllNamespaces(),
|
||||
coreApi.listPersistentVolumeClaimForAllNamespaces(),
|
||||
]);
|
||||
|
||||
return [
|
||||
{ label: "nodes", count: nodes.items.length },
|
||||
{ label: "namespaces", count: namespaces.items.length },
|
||||
{ label: "ingresses", count: ingresses.items.length },
|
||||
{ label: "services", count: services.items.length },
|
||||
{ label: "pods", count: pods.items.length },
|
||||
{ label: "secrets", count: secrets.items.length },
|
||||
{ label: "configmaps", count: configMaps.items.length },
|
||||
{ label: "volumes", count: volumes.items.length },
|
||||
];
|
||||
} catch (error) {
|
||||
logger.error("Unable to retrieve cluster resource counts", error);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching Kubernetes resources count",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
function getProviders(versionInfo: VersionInfo, nodes: V1NodeList) {
|
||||
const providers = new Set<string>();
|
||||
|
||||
if (versionInfo.gitVersion.includes("k3s")) providers.add("k3s");
|
||||
if (versionInfo.gitVersion.includes("gke")) providers.add("GKE");
|
||||
if (versionInfo.gitVersion.includes("eks")) providers.add("EKS");
|
||||
if (versionInfo.gitVersion.includes("aks")) providers.add("AKS");
|
||||
|
||||
nodes.items.forEach((node) => {
|
||||
const nodeProviderLabel =
|
||||
node.metadata?.labels?.["node.kubernetes.io/instance-type"] ?? node.metadata?.labels?.provider ?? "";
|
||||
if (nodeProviderLabel.includes("aws")) providers.add("EKS");
|
||||
if (nodeProviderLabel.includes("azure")) providers.add("AKS");
|
||||
if (nodeProviderLabel.includes("gce")) providers.add("GKE");
|
||||
if (nodeProviderLabel.includes("k3s")) providers.add("k3s");
|
||||
});
|
||||
|
||||
return Array.from(providers).join(", ");
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { KubernetesBaseResource } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||
import { KubernetesClient } from "../kubernetes-client";
|
||||
|
||||
export const configMapsRouter = createTRPCRouter({
|
||||
getConfigMaps: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(kubernetesMiddleware())
|
||||
.query(async (): Promise<KubernetesBaseResource[]> => {
|
||||
const { coreApi } = KubernetesClient.getInstance();
|
||||
|
||||
try {
|
||||
const configMaps = await coreApi.listConfigMapForAllNamespaces();
|
||||
|
||||
return configMaps.items.map((configMap) => {
|
||||
return {
|
||||
name: configMap.metadata?.name ?? "unknown",
|
||||
namespace: configMap.metadata?.namespace ?? "unknown",
|
||||
creationTimestamp: configMap.metadata?.creationTimestamp,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Unable to retrieve configMaps", error);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching Kubernetes ConfigMaps",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { V1HTTPIngressPath, V1Ingress, V1IngressRule } from "@kubernetes/client-node";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { KubernetesIngress, KubernetesIngressPath, KubernetesIngressRuleAndPath } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||
import { KubernetesClient } from "../kubernetes-client";
|
||||
|
||||
export const ingressesRouter = createTRPCRouter({
|
||||
getIngresses: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(kubernetesMiddleware())
|
||||
.query(async (): Promise<KubernetesIngress[]> => {
|
||||
const { networkingApi } = KubernetesClient.getInstance();
|
||||
try {
|
||||
const ingresses = await networkingApi.listIngressForAllNamespaces();
|
||||
|
||||
const mapIngress = (ingress: V1Ingress): KubernetesIngress => {
|
||||
return {
|
||||
name: ingress.metadata?.name ?? "",
|
||||
namespace: ingress.metadata?.namespace ?? "",
|
||||
className: ingress.spec?.ingressClassName ?? "",
|
||||
rulesAndPaths: getIngressRulesAndPaths(ingress.spec?.rules ?? []),
|
||||
creationTimestamp: ingress.metadata?.creationTimestamp,
|
||||
};
|
||||
};
|
||||
|
||||
const getIngressRulesAndPaths = (rules: V1IngressRule[] = []): KubernetesIngressRuleAndPath[] => {
|
||||
return rules.map((rule) => ({
|
||||
host: rule.host ?? "",
|
||||
paths: getPaths(rule.http?.paths ?? []),
|
||||
}));
|
||||
};
|
||||
|
||||
const getPaths = (paths: V1HTTPIngressPath[] = []): KubernetesIngressPath[] => {
|
||||
return paths.map((path) => ({
|
||||
serviceName: path.backend.service?.name ?? "",
|
||||
port: path.backend.service?.port?.number ?? 0,
|
||||
}));
|
||||
};
|
||||
|
||||
return ingresses.items.map(mapIngress);
|
||||
} catch (error) {
|
||||
logger.error("Unable to retrieve ingresses", error);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching Kubernetes ingresses",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createTRPCRouter } from "../../../trpc";
|
||||
import { clusterRouter } from "./cluster";
|
||||
import { configMapsRouter } from "./configMaps";
|
||||
import { ingressesRouter } from "./ingresses";
|
||||
import { namespacesRouter } from "./namespaces";
|
||||
import { nodesRouter } from "./nodes";
|
||||
import { podsRouter } from "./pods";
|
||||
import { secretsRouter } from "./secrets";
|
||||
import { servicesRouter } from "./services";
|
||||
import { volumesRouter } from "./volumes";
|
||||
|
||||
export const kubernetesRouter = createTRPCRouter({
|
||||
nodes: nodesRouter,
|
||||
cluster: clusterRouter,
|
||||
namespaces: namespacesRouter,
|
||||
ingresses: ingressesRouter,
|
||||
services: servicesRouter,
|
||||
pods: podsRouter,
|
||||
secrets: secretsRouter,
|
||||
configMaps: configMapsRouter,
|
||||
volumes: volumesRouter,
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { KubernetesNamespace, KubernetesNamespaceState } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||
import { KubernetesClient } from "../kubernetes-client";
|
||||
|
||||
export const namespacesRouter = createTRPCRouter({
|
||||
getNamespaces: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.unstable_concat(kubernetesMiddleware())
|
||||
.query(async (): Promise<KubernetesNamespace[]> => {
|
||||
const { coreApi } = KubernetesClient.getInstance();
|
||||
|
||||
try {
|
||||
const namespaces = await coreApi.listNamespace();
|
||||
|
||||
return namespaces.items.map((namespace) => {
|
||||
return {
|
||||
status: namespace.status?.phase as KubernetesNamespaceState,
|
||||
name: namespace.metadata?.name ?? "unknown",
|
||||
creationTimestamp: namespace.metadata?.creationTimestamp,
|
||||
} satisfies KubernetesNamespace;
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Unable to retrieve namespaces", error);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching Kubernetes namespaces",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||