mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-24 15:30:54 +01:00
Merge pull request #788 from ajnart/allow-multiple-widgets-of-same-type
Allow multiple widgets of same type
This commit is contained in:
@@ -1,387 +1,389 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"configProperties": {
|
||||
"name": "default"
|
||||
},
|
||||
"categories": [
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f",
|
||||
"position": 1,
|
||||
"name": "Welcome to Homarr 🎉",
|
||||
"type": "category"
|
||||
"schemaVersion": 1,
|
||||
"configProperties": {
|
||||
"name": "default"
|
||||
},
|
||||
"categories": [
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f",
|
||||
"position": 1,
|
||||
"name": "Welcome to Homarr 🎉",
|
||||
"type": "category"
|
||||
}
|
||||
],
|
||||
"wrappers": [
|
||||
{
|
||||
"id": "default",
|
||||
"position": 0
|
||||
},
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a326",
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
"apps": [
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337",
|
||||
"name": "Discord",
|
||||
"url": "https://discord.com/invite/aCsmEV5RgA",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://discord.com/invite/aCsmEV5RgA",
|
||||
"isOpeningNewTab": true,
|
||||
"externalUrl": "https://discord.com/invite/aCsmEV5RgA"
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"okStatus": [
|
||||
200
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/discord.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 3,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 3,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a990",
|
||||
"name": "Donate",
|
||||
"url": "https://ko-fi.com/ajnart",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://ko-fi.com/ajnart",
|
||||
"externalUrl": "https://ko-fi.com/ajnart",
|
||||
"isOpeningNewTab": true
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"okStatus": [
|
||||
200
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/ko-fi.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 2
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 3,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a330",
|
||||
"name": "Contribute",
|
||||
"url": "https://github.com/ajnart/homarr",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://github.com/ajnart/homarr",
|
||||
"externalUrl": "https://github.com/ajnart/homarr",
|
||||
"isOpeningNewTab": true
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"okStatus": []
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/github.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 2
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 4,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "5df743d9-5cb1-457c-85d2-64ff86855652",
|
||||
"name": "Documentation",
|
||||
"url": "https://homarr.dev",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://homarr.dev",
|
||||
"externalUrl": "https://homarr.dev",
|
||||
"isOpeningNewTab": true
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"okStatus": [
|
||||
200
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "/imgs/logo/logo.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"widgets": [
|
||||
{
|
||||
"id": "971aa859-8570-49a1-8d34-dd5c7b3638d1",
|
||||
"type": "date",
|
||||
"properties": {
|
||||
"display24HourFormat": true
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 4,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "e3004052-6b83-480e-b458-56e8ccdca5f0",
|
||||
"type": "weather",
|
||||
"properties": {
|
||||
"displayInFahrenheit": false,
|
||||
"location": "Paris"
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 1,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"common": {
|
||||
"searchEngine": {
|
||||
"type": "google",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"layout": {
|
||||
"enabledLeftSidebar": false,
|
||||
"enabledRightSidebar": false,
|
||||
"enabledDocker": false,
|
||||
"enabledPing": false,
|
||||
"enabledSearchbar": true
|
||||
},
|
||||
"pageTitle": "Homarr v0.11 ⭐️",
|
||||
"logoImageUrl": "/imgs/logo/logo.png",
|
||||
"faviconUrl": "/imgs/favicon/favicon-squared",
|
||||
"backgroundImageUrl": "",
|
||||
"customCss": "",
|
||||
"colors": {
|
||||
"primary": "red",
|
||||
"secondary": "yellow",
|
||||
"shade": 7
|
||||
},
|
||||
"appOpacity": 100
|
||||
}
|
||||
}
|
||||
],
|
||||
"wrappers": [
|
||||
{
|
||||
"id": "default",
|
||||
"position": 0
|
||||
},
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a326",
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
"apps": [
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337",
|
||||
"name": "Discord",
|
||||
"url": "https://discord.com/invite/aCsmEV5RgA",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://discord.com/invite/aCsmEV5RgA",
|
||||
"isOpeningNewTab": true,
|
||||
"externalUrl": "https://discord.com/invite/aCsmEV5RgA"
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"okStatus": [
|
||||
200
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/discord.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 3,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 3,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a990",
|
||||
"name": "Donate",
|
||||
"url": "https://ko-fi.com/ajnart",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://ko-fi.com/ajnart",
|
||||
"externalUrl": "https://ko-fi.com/ajnart",
|
||||
"isOpeningNewTab": true
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"okStatus": [
|
||||
200
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/ko-fi.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 2
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 3,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a330",
|
||||
"name": "Contribute",
|
||||
"url": "https://github.com/ajnart/homarr",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://github.com/ajnart/homarr",
|
||||
"externalUrl": "https://github.com/ajnart/homarr",
|
||||
"isOpeningNewTab": true
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"okStatus": []
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/github.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 2
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 4,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "5df743d9-5cb1-457c-85d2-64ff86855652",
|
||||
"name": "Documentation",
|
||||
"url": "https://homarr.dev",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://homarr.dev",
|
||||
"externalUrl": "https://homarr.dev",
|
||||
"isOpeningNewTab": true
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"okStatus": [
|
||||
200
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "/imgs/logo/logo.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"widgets": [
|
||||
{
|
||||
"id": "date",
|
||||
"properties": {
|
||||
"display24HourFormat": true
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 4,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "weather",
|
||||
"properties": {
|
||||
"displayInFahrenheit": false,
|
||||
"location": "Paris"
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 1,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"common": {
|
||||
"searchEngine": {
|
||||
"type": "google",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"layout": {
|
||||
"enabledLeftSidebar": false,
|
||||
"enabledRightSidebar": false,
|
||||
"enabledDocker": false,
|
||||
"enabledPing": false,
|
||||
"enabledSearchbar": true
|
||||
},
|
||||
"pageTitle": "Homarr v0.11 ⭐️",
|
||||
"logoImageUrl": "/imgs/logo/logo.png",
|
||||
"faviconUrl": "/imgs/favicon/favicon-squared",
|
||||
"backgroundImageUrl": "",
|
||||
"customCss": "",
|
||||
"colors": {
|
||||
"primary": "red",
|
||||
"secondary": "yellow",
|
||||
"shade": 7
|
||||
},
|
||||
"appOpacity": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"uuid": "^8.3.2",
|
||||
"xml-js": "^1.6.11",
|
||||
"yarn": "^1.22.19",
|
||||
"zod": "^3.21.4",
|
||||
"zustand": "^4.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -54,8 +54,8 @@ export const ChangeWidgetPositionModal = ({
|
||||
closeModal(id);
|
||||
};
|
||||
|
||||
const widthData = useWidthData(innerProps.widgetId);
|
||||
const heightData = useHeightData(innerProps.widgetId);
|
||||
const widthData = useWidthData(innerProps.widgetType);
|
||||
const heightData = useHeightData(innerProps.widgetType);
|
||||
|
||||
return (
|
||||
<ChangePositionModal
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Grid, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfigContext } from '../../../../../../config/provider';
|
||||
import widgets from '../../../../../../widgets';
|
||||
import { SelectorBackArrow } from '../Shared/SelectorBackArrow';
|
||||
import { WidgetElementType } from './WidgetElementType';
|
||||
@@ -13,7 +12,6 @@ export const AvailableIntegrationElements = ({
|
||||
onClickBack,
|
||||
}: AvailableIntegrationElementsProps) => {
|
||||
const { t } = useTranslation('layout/element-selector/selector');
|
||||
const activeWidgets = useConfigContext().config?.widgets ?? [];
|
||||
return (
|
||||
<>
|
||||
<SelectorBackArrow onClickBack={onClickBack} />
|
||||
@@ -23,11 +21,9 @@ export const AvailableIntegrationElements = ({
|
||||
</Text>
|
||||
|
||||
<Grid>
|
||||
{Object.entries(widgets)
|
||||
.filter(([widgetId]) => !activeWidgets.some((aw) => aw.id === widgetId))
|
||||
.map(([k, v]) => (
|
||||
<WidgetElementType key={k} id={k} image={v.icon} widget={v} />
|
||||
))}
|
||||
{Object.entries(widgets).map(([k, v]) => (
|
||||
<WidgetElementType key={k} id={k} image={v.icon} widget={v} />
|
||||
))}
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useModals } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconChecks, TablerIcon } from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useConfigContext } from '../../../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../../../config/store';
|
||||
import { IWidget, IWidgetDefinition } from '../../../../../../widgets/widgets';
|
||||
@@ -32,9 +33,10 @@ export const WidgetElementType = ({ id, image, disabled, widget }: WidgetElement
|
||||
(prev) => ({
|
||||
...prev,
|
||||
widgets: [
|
||||
...prev.widgets.filter((w) => w.id !== widget.id),
|
||||
...prev.widgets,
|
||||
{
|
||||
id: widget.id,
|
||||
id: uuidv4(),
|
||||
type: widget.id,
|
||||
properties: Object.entries(widget.options).reduce((prev, [k, v]) => {
|
||||
const newPrev = prev;
|
||||
newPrev[k] = v.defaultValue;
|
||||
|
||||
@@ -26,6 +26,7 @@ import { DraggableList } from './DraggableList';
|
||||
|
||||
export type WidgetEditModalInnerProps = {
|
||||
widgetId: string;
|
||||
widgetType: string;
|
||||
options: IWidget<string, any>['properties'];
|
||||
widgetOptions: IWidget<string, any>['properties'];
|
||||
};
|
||||
@@ -37,7 +38,7 @@ export const WidgetsEditModal = ({
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<WidgetEditModalInnerProps>) => {
|
||||
const { t } = useTranslation([`modules/${innerProps.widgetId}`, 'common']);
|
||||
const { t } = useTranslation([`modules/${innerProps.widgetType}`, 'common']);
|
||||
const [moduleProperties, setModuleProperties] = useState(innerProps.options);
|
||||
const items = Object.entries(innerProps.widgetOptions ?? {}) as [
|
||||
string,
|
||||
@@ -45,7 +46,7 @@ export const WidgetsEditModal = ({
|
||||
][];
|
||||
|
||||
// Find the Key in the "Widgets" Object that matches the widgetId
|
||||
const currentWidgetDefinition = Widgets[innerProps.widgetId as keyof typeof Widgets];
|
||||
const currentWidgetDefinition = Widgets[innerProps.widgetType as keyof typeof Widgets];
|
||||
const { name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
@@ -100,7 +101,7 @@ export const WidgetsEditModal = ({
|
||||
<WidgetOptionTypeSwitch
|
||||
key={`${key}.${index}`}
|
||||
option={option}
|
||||
widgetId={innerProps.widgetId}
|
||||
widgetId={innerProps.widgetType}
|
||||
propName={key}
|
||||
value={value}
|
||||
handleChange={handleChange}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { WidgetsRemoveModalInnerProps } from './WidgetsRemoveModal';
|
||||
|
||||
export type WidgetChangePositionModalInnerProps = {
|
||||
widgetId: string;
|
||||
widgetType: string;
|
||||
widget: IWidget<string, any>;
|
||||
wrapperColumnCount: number;
|
||||
};
|
||||
@@ -27,8 +28,8 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
|
||||
// Match widget.id with WidgetsDefinitions
|
||||
// First get the keys
|
||||
const keys = Object.keys(WidgetsDefinitions);
|
||||
// Then find the key that matches the widget.id
|
||||
const widgetDefinition = keys.find((key) => key === widget.id);
|
||||
// Then find the key that matches the widget.type
|
||||
const widgetDefinition = keys.find((key) => key === widget.type);
|
||||
// Then get the widget definition
|
||||
const widgetDefinitionObject =
|
||||
WidgetsDefinitions[widgetDefinition as keyof typeof WidgetsDefinitions];
|
||||
@@ -38,13 +39,8 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
|
||||
modal: 'integrationRemove',
|
||||
title: <Title order={4}>{t('common:remove')}</Title>,
|
||||
innerProps: {
|
||||
widgetId: integration,
|
||||
},
|
||||
styles: {
|
||||
inner: {
|
||||
position: 'sticky',
|
||||
top: 30,
|
||||
},
|
||||
widgetId: widget.id,
|
||||
widgetType: integration,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -55,16 +51,11 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
|
||||
size: 'xl',
|
||||
title: null,
|
||||
innerProps: {
|
||||
widgetId: integration,
|
||||
widgetId: widget.id,
|
||||
widgetType: integration,
|
||||
widget,
|
||||
wrapperColumnCount,
|
||||
},
|
||||
styles: {
|
||||
inner: {
|
||||
position: 'sticky',
|
||||
top: 30,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -73,19 +64,13 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
|
||||
modal: 'integrationOptions',
|
||||
title: <Title order={4}>{t('descriptor.settings.title')}</Title>,
|
||||
innerProps: {
|
||||
widgetId: integration,
|
||||
widgetId: widget.id,
|
||||
widgetType: integration,
|
||||
options: widget.properties,
|
||||
// Cast as the right type for the correct widget
|
||||
widgetOptions: widgetDefinitionObject.options as any,
|
||||
},
|
||||
zIndex: 5,
|
||||
styles: {
|
||||
inner: {
|
||||
position: 'sticky',
|
||||
top: 30,
|
||||
maxHeight: '100%',
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useConfigStore } from '../../../../config/store';
|
||||
|
||||
export type WidgetsRemoveModalInnerProps = {
|
||||
widgetId: string;
|
||||
widgetType: string;
|
||||
};
|
||||
|
||||
export const WidgetsRemoveModal = ({
|
||||
@@ -14,7 +15,7 @@ export const WidgetsRemoveModal = ({
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<WidgetsRemoveModalInnerProps>) => {
|
||||
const { t } = useTranslation([`modules/${innerProps.widgetId}`, 'common']);
|
||||
const { t } = useTranslation([`modules/${innerProps.widgetType}`, 'common']);
|
||||
const { name: configName } = useConfigContext();
|
||||
if (!configName) return null;
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
@@ -35,7 +36,7 @@ export const WidgetsRemoveModal = ({
|
||||
<Trans
|
||||
i18nKey="common:removeConfirm"
|
||||
components={[<Text weight={500} />]}
|
||||
values={{ item: innerProps.widgetId }}
|
||||
values={{ item: innerProps.widgetType }}
|
||||
/>
|
||||
<Group position="right">
|
||||
<Button onClick={() => context.closeModal(id)} variant="light">
|
||||
|
||||
@@ -42,7 +42,7 @@ export function WrapperContent({ apps, refs, widgets }: WrapperContentProps) {
|
||||
);
|
||||
})}
|
||||
{widgets.map((widget) => {
|
||||
const definition = Widgets[widget.id as keyof typeof Widgets] as
|
||||
const definition = Widgets[widget.type as keyof typeof Widgets] as
|
||||
| IWidgetDefinition
|
||||
| undefined;
|
||||
if (!definition) return null;
|
||||
@@ -52,7 +52,7 @@ export function WrapperContent({ apps, refs, widgets }: WrapperContentProps) {
|
||||
type="widget"
|
||||
key={widget.id}
|
||||
itemRef={refs.items.current[widget.id]}
|
||||
id={definition.id}
|
||||
id={widget.id}
|
||||
{...definition.gridstack}
|
||||
{...widget.shape[shapeSize]?.location}
|
||||
{...widget.shape[shapeSize]?.size}
|
||||
@@ -60,7 +60,7 @@ export function WrapperContent({ apps, refs, widgets }: WrapperContentProps) {
|
||||
<WidgetWrapper
|
||||
className="grid-stack-item-content"
|
||||
widget={widget}
|
||||
widgetId={widget.id}
|
||||
widgetType={widget.type}
|
||||
WidgetComponent={definition.component}
|
||||
/>
|
||||
</GridstackTileWrapper>
|
||||
|
||||
@@ -48,6 +48,7 @@ export const useConfigStore = create<UseConfigStoreType>((set, get) => ({
|
||||
const previousConfig: ConfigType = JSON.parse(JSON.stringify(currentConfig.value));
|
||||
|
||||
const updatedConfig = updateCallback(currentConfig.value);
|
||||
|
||||
set((old) => ({
|
||||
...old,
|
||||
configs: [
|
||||
|
||||
@@ -153,7 +153,7 @@ export function RadarrMediaDisplay(props: any) {
|
||||
export function SonarrMediaDisplay(props: any) {
|
||||
const { media }: { media: any } = props;
|
||||
const { config } = useConfigContext();
|
||||
const calendar = config?.widgets.find((w) => w.id === 'calendar');
|
||||
const calendar = config?.widgets.find((w) => w.type === 'calendar');
|
||||
const useSonarrv4 = calendar?.properties.useSonarrv4 ?? false;
|
||||
|
||||
// Find a poster CoverType
|
||||
|
||||
@@ -4,6 +4,7 @@ import Consola from 'consola';
|
||||
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { AppIntegrationType } from '../../../types/app';
|
||||
import { getConfig } from '../../../tools/config/getConfig';
|
||||
|
||||
@@ -18,28 +19,36 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getQuerySchema = z.object({
|
||||
month: z
|
||||
.string()
|
||||
.regex(/^\d+$/)
|
||||
.transform((x) => parseInt(x, 10)),
|
||||
year: z
|
||||
.string()
|
||||
.regex(/^\d+$/)
|
||||
.transform((x) => parseInt(x, 10)),
|
||||
widgetId: z.string().uuid(),
|
||||
configName: z.string(),
|
||||
});
|
||||
|
||||
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
||||
// Parse req.body as a AppItem
|
||||
const {
|
||||
month: monthString,
|
||||
year: yearString,
|
||||
configName,
|
||||
} = req.query as { month: string; year: string; configName: string };
|
||||
const parseResult = getQuerySchema.safeParse(req.query);
|
||||
|
||||
const month = parseInt(monthString, 10);
|
||||
const year = parseInt(yearString, 10);
|
||||
|
||||
if (Number.isNaN(month) || Number.isNaN(year) || !configName) {
|
||||
if (!parseResult.success) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
message: 'Missing required parameter in url: year, month or configName',
|
||||
message: 'Invalid query parameters, please specify the widgetId, month, year and configName',
|
||||
});
|
||||
}
|
||||
|
||||
// Parse req.body as a AppItem
|
||||
const { month, year, widgetId, configName } = parseResult.data;
|
||||
|
||||
const config = getConfig(configName);
|
||||
|
||||
// Find the calendar widget in the config
|
||||
const calendar = config.widgets.find((w) => w.id === 'calendar');
|
||||
const calendar = config.widgets.find((w) => w.type === 'calendar' && w.id === widgetId);
|
||||
const useSonarrv4 = calendar?.properties.useSonarrv4 ?? false;
|
||||
|
||||
const mediaAppIntegrationTypes: AppIntegrationType['type'][] = [
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
import axios from 'axios';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { z } from 'zod';
|
||||
import { getConfig } from '../../../../tools/config/getConfig';
|
||||
import { IDashDotTile } from '../../../../widgets/dashDot/DashDotTile';
|
||||
|
||||
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { configName } = req.query;
|
||||
const getQuerySchema = z.object({
|
||||
configName: z.string(),
|
||||
widgetId: z.string().uuid(),
|
||||
});
|
||||
|
||||
if (!configName || typeof configName !== 'string') {
|
||||
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
||||
const parseResult = getQuerySchema.safeParse(req.query);
|
||||
|
||||
if (!parseResult.success) {
|
||||
return res.status(400).json({
|
||||
message: 'Missing required configName in url',
|
||||
statusCode: 400,
|
||||
message: 'Invalid query parameters, please specify the widgetId and configName',
|
||||
});
|
||||
}
|
||||
|
||||
const { configName, widgetId } = parseResult.data;
|
||||
|
||||
const config = getConfig(configName);
|
||||
|
||||
const dashDotWidget = config.widgets.find((x) => x.id === 'dashdot');
|
||||
const dashDotWidget = config.widgets.find((x) => x.type === 'dashdot' && x.id === widgetId);
|
||||
|
||||
if (!dashDotWidget) {
|
||||
return res.status(400).json({
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import axios from 'axios';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { z } from 'zod';
|
||||
import { getConfig } from '../../../../tools/config/getConfig';
|
||||
import { IDashDotTile } from '../../../../widgets/dashDot/DashDotTile';
|
||||
|
||||
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { configName } = req.query;
|
||||
const getQuerySchema = z.object({
|
||||
configName: z.string(),
|
||||
widgetId: z.string().uuid(),
|
||||
});
|
||||
|
||||
if (!configName || typeof configName !== 'string') {
|
||||
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
||||
const parseResult = getQuerySchema.safeParse(req.query);
|
||||
|
||||
if (!parseResult.success) {
|
||||
return res.status(400).json({
|
||||
message: 'Missing required configName in url',
|
||||
statusCode: 400,
|
||||
message: 'Invalid query parameters, please specify the widgetId and configName',
|
||||
});
|
||||
}
|
||||
|
||||
const { configName, widgetId } = parseResult.data;
|
||||
|
||||
const config = getConfig(configName);
|
||||
const dashDotWidget = config.widgets.find((x) => x.id === 'dashdot');
|
||||
const dashDotWidget = config.widgets.find((x) => x.type === 'dashdot' && x.id === widgetId);
|
||||
|
||||
if (!dashDotWidget) {
|
||||
return res.status(400).json({
|
||||
|
||||
@@ -8,6 +8,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import Parser from 'rss-parser';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { getConfig } from '../../../../tools/config/getConfig';
|
||||
import { IRssWidget } from '../../../../widgets/rss/RssWidgetTile';
|
||||
import { Stopwatch } from '../../../../tools/shared/time/stopwatch.tool';
|
||||
@@ -25,11 +26,24 @@ const parser: Parser<any, CustomItem> = new Parser({
|
||||
},
|
||||
});
|
||||
|
||||
const getQuerySchema = z.object({
|
||||
widgetId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export const Get = async (request: NextApiRequest, response: NextApiResponse) => {
|
||||
const configName = getCookie('config-name', { req: request });
|
||||
const config = getConfig(configName?.toString() ?? 'default');
|
||||
|
||||
const rssWidget = config.widgets.find((x) => x.id === 'rss') as IRssWidget | undefined;
|
||||
const parseResult = getQuerySchema.safeParse(request.query);
|
||||
|
||||
if (!parseResult.success) {
|
||||
response.status(400).json({ message: 'invalid query parameters, please specify the widgetId' });
|
||||
return;
|
||||
}
|
||||
|
||||
const rssWidget = config.widgets.find(
|
||||
(x) => x.type === 'rss' && x.id === parseResult.data.widgetId
|
||||
) as IRssWidget | undefined;
|
||||
|
||||
if (
|
||||
!rssWidget ||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import Consola from 'consola';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { BackendConfigType, ConfigType } from '../../types/config';
|
||||
import { backendMigrateConfig } from './backendMigrateConfig';
|
||||
import { configExists } from './configExists';
|
||||
import { getFallbackConfig } from './getFallbackConfig';
|
||||
import { readConfig } from './readConfig';
|
||||
import { writeConfig } from './writeConfig';
|
||||
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
||||
|
||||
export const getConfig = (name: string): BackendConfigType => {
|
||||
if (!configExists(name)) return getFallbackConfig() as unknown as ConfigType;
|
||||
@@ -12,9 +16,29 @@ export const getConfig = (name: string): BackendConfigType => {
|
||||
// to the new format.
|
||||
const config = readConfig(name);
|
||||
if (config.schemaVersion === undefined) {
|
||||
Consola.log('Migrating config file...', config);
|
||||
Consola.log('Migrating config file...', config.name);
|
||||
return backendMigrateConfig(config, name);
|
||||
}
|
||||
|
||||
return config;
|
||||
let backendConfig = config as BackendConfigType;
|
||||
|
||||
if (backendConfig.widgets.some((widget) => !uuidRegex.test(widget.id))) {
|
||||
backendConfig = {
|
||||
...backendConfig,
|
||||
widgets: backendConfig.widgets.map((widget) => ({
|
||||
...widget,
|
||||
id: uuidRegex.test(widget.id) ? widget.id : uuidv4(),
|
||||
type: !uuidRegex.test(widget.id) ? widget.id : widget.type,
|
||||
})),
|
||||
};
|
||||
|
||||
Consola.log(
|
||||
'Migrating config file to multiple widgets...',
|
||||
backendConfig.configProperties.name
|
||||
);
|
||||
|
||||
writeConfig(backendConfig);
|
||||
}
|
||||
|
||||
return backendConfig;
|
||||
};
|
||||
|
||||
@@ -183,7 +183,8 @@ const migrateModules = (config: Config): IWidget<string, any>[] => {
|
||||
case 'torrent-status':
|
||||
case 'Torrent':
|
||||
return {
|
||||
id: 'torrents-status',
|
||||
id: uuidv4(),
|
||||
type: 'torrents-status',
|
||||
properties: {
|
||||
refreshInterval: 10,
|
||||
displayCompletedTorrents: oldModule.options?.hideComplete?.value ?? false,
|
||||
@@ -199,7 +200,8 @@ const migrateModules = (config: Config): IWidget<string, any>[] => {
|
||||
} as ITorrent;
|
||||
case 'weather':
|
||||
return {
|
||||
id: 'weather',
|
||||
id: uuidv4(),
|
||||
type: 'weather',
|
||||
properties: {
|
||||
displayInFahrenheit: oldModule.options?.freedomunit?.value ?? false,
|
||||
location: oldModule.options?.location?.value ?? 'Paris',
|
||||
@@ -216,7 +218,8 @@ const migrateModules = (config: Config): IWidget<string, any>[] => {
|
||||
case 'Dash.': {
|
||||
const oldDashDotService = config.services.find((service) => service.type === 'Dash.');
|
||||
return {
|
||||
id: 'dashdot',
|
||||
id: uuidv4(),
|
||||
type: 'dashdot',
|
||||
properties: {
|
||||
url: oldModule.options?.url?.value ?? oldDashDotService?.url ?? '',
|
||||
cpuMultiView: oldModule.options?.cpuMultiView?.value ?? false,
|
||||
@@ -235,7 +238,8 @@ const migrateModules = (config: Config): IWidget<string, any>[] => {
|
||||
}
|
||||
case 'date':
|
||||
return {
|
||||
id: 'date',
|
||||
id: uuidv4(),
|
||||
type: 'date',
|
||||
properties: {
|
||||
display24HourFormat: oldModule.options?.full?.value ?? true,
|
||||
},
|
||||
@@ -249,7 +253,8 @@ const migrateModules = (config: Config): IWidget<string, any>[] => {
|
||||
} as IDateWidget;
|
||||
case 'Download Speed' || 'dlspeed':
|
||||
return {
|
||||
id: 'dlspeed',
|
||||
id: uuidv4(),
|
||||
type: 'dlspeed',
|
||||
properties: {},
|
||||
area: {
|
||||
type: 'wrapper',
|
||||
@@ -261,7 +266,8 @@ const migrateModules = (config: Config): IWidget<string, any>[] => {
|
||||
} as ITorrentNetworkTraffic;
|
||||
case 'calendar':
|
||||
return {
|
||||
id: 'calendar',
|
||||
id: uuidv4(),
|
||||
type: 'calendar',
|
||||
properties: {},
|
||||
area: {
|
||||
type: 'wrapper',
|
||||
@@ -273,7 +279,8 @@ const migrateModules = (config: Config): IWidget<string, any>[] => {
|
||||
} as ICalendarWidget;
|
||||
case 'usenet':
|
||||
return {
|
||||
id: 'usenet',
|
||||
id: uuidv4(),
|
||||
type: 'usenet',
|
||||
properties: {},
|
||||
area: {
|
||||
type: 'wrapper',
|
||||
|
||||
10
src/tools/config/writeConfig.ts
Normal file
10
src/tools/config/writeConfig.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import fs from 'fs';
|
||||
import { BackendConfigType } from '../../types/config';
|
||||
import { generateConfigPath } from './generateConfigPath';
|
||||
|
||||
export function writeConfig(config: BackendConfigType) {
|
||||
const path = generateConfigPath(config.configProperties.name);
|
||||
return fs.writeFileSync(path, JSON.stringify(config, null, 4), {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentType, useMemo } from 'react';
|
||||
import { ComponentType } from 'react';
|
||||
import Widgets from '.';
|
||||
import { HomarrCardWrapper } from '../components/Dashboard/Tiles/HomarrCardWrapper';
|
||||
import { WidgetsMenu } from '../components/Dashboard/Tiles/Widgets/WidgetsMenu';
|
||||
@@ -6,7 +6,7 @@ import ErrorBoundary from './boundary';
|
||||
import { IWidget } from './widgets';
|
||||
|
||||
interface WidgetWrapperProps {
|
||||
widgetId: string;
|
||||
widgetType: string;
|
||||
widget: IWidget<string, any>;
|
||||
className: string;
|
||||
WidgetComponent: ComponentType<{ widget: IWidget<string, any> }>;
|
||||
@@ -14,26 +14,24 @@ interface WidgetWrapperProps {
|
||||
|
||||
// If a property has no value, set it to the default value
|
||||
const useWidget = <T extends IWidget<string, any>>(widget: T): T => {
|
||||
const definition = Widgets[widget.id as keyof typeof Widgets];
|
||||
const definition = Widgets[widget.type as keyof typeof Widgets];
|
||||
|
||||
return useMemo(() => {
|
||||
const newProps = { ...widget.properties };
|
||||
const newProps = { ...widget.properties };
|
||||
|
||||
Object.entries(definition.options).forEach(([key, option]) => {
|
||||
if (newProps[key] == null) {
|
||||
newProps[key] = option.defaultValue;
|
||||
}
|
||||
});
|
||||
Object.entries(definition.options).forEach(([key, option]) => {
|
||||
if (newProps[key] == null) {
|
||||
newProps[key] = option.defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...widget,
|
||||
properties: newProps,
|
||||
};
|
||||
}, [widget]);
|
||||
return {
|
||||
...widget,
|
||||
properties: newProps,
|
||||
};
|
||||
};
|
||||
|
||||
export const WidgetWrapper = ({
|
||||
widgetId,
|
||||
widgetType,
|
||||
widget,
|
||||
className,
|
||||
WidgetComponent,
|
||||
@@ -43,7 +41,7 @@ export const WidgetWrapper = ({
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<HomarrCardWrapper className={className}>
|
||||
<WidgetsMenu integration={widgetId} widget={widgetWithDefaultProps} />
|
||||
<WidgetsMenu integration={widgetType} widget={widgetWithDefaultProps} />
|
||||
<WidgetComponent widget={widgetWithDefaultProps} />
|
||||
</HomarrCardWrapper>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -94,7 +94,7 @@ class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundarySta
|
||||
mt="md"
|
||||
fullWidth
|
||||
>
|
||||
{(this.props.t('modal.reportButton'))}
|
||||
{this.props.t('modal.reportButton')}
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
|
||||
@@ -63,7 +63,7 @@ function CalendarTile({ widget }: CalendarTileProps) {
|
||||
await fetch(
|
||||
`/api/modules/calendar?year=${month.getFullYear()}&month=${
|
||||
month.getMonth() + 1
|
||||
}&configName=${configName}`
|
||||
}&configName=${configName}&widgetId=${widget.id}`
|
||||
)
|
||||
).json()) as MediasType,
|
||||
});
|
||||
|
||||
@@ -9,11 +9,12 @@ import { DashDotInfo } from './DashDotCompactNetwork';
|
||||
|
||||
interface DashDotCompactStorageProps {
|
||||
info: DashDotInfo;
|
||||
widgetId: string;
|
||||
}
|
||||
|
||||
export const DashDotCompactStorage = ({ info }: DashDotCompactStorageProps) => {
|
||||
export const DashDotCompactStorage = ({ info, widgetId }: DashDotCompactStorageProps) => {
|
||||
const { t } = useTranslation('modules/dashdot');
|
||||
const { data: storageLoad } = useDashDotStorage();
|
||||
const { data: storageLoad } = useDashDotStorage(widgetId);
|
||||
|
||||
const totalUsed = calculateTotalLayoutSize({
|
||||
layout: storageLoad?.layout ?? [],
|
||||
@@ -50,7 +51,7 @@ interface CalculateTotalLayoutSizeProps<TLayoutItem> {
|
||||
key: keyof TLayoutItem;
|
||||
}
|
||||
|
||||
const useDashDotStorage = () => {
|
||||
const useDashDotStorage = (widgetId: string) => {
|
||||
const { name: configName, config } = useConfigContext();
|
||||
|
||||
return useQuery({
|
||||
@@ -58,17 +59,18 @@ const useDashDotStorage = () => {
|
||||
'dashdot/storage',
|
||||
{
|
||||
configName,
|
||||
url: config?.widgets.find((x) => x.id === 'dashdot')?.properties.url,
|
||||
url: config?.widgets.find((x) => x.type === 'dashdot')?.properties.url,
|
||||
widgetId,
|
||||
},
|
||||
],
|
||||
queryFn: () => fetchDashDotStorageLoad(configName),
|
||||
queryFn: () => fetchDashDotStorageLoad(configName, widgetId),
|
||||
});
|
||||
};
|
||||
|
||||
async function fetchDashDotStorageLoad(configName: string | undefined) {
|
||||
async function fetchDashDotStorageLoad(configName: string | undefined, widgetId: string) {
|
||||
if (!configName) throw new Error('configName is undefined');
|
||||
return (await (
|
||||
await axios.get('/api/modules/dashdot/storage', { params: { configName } })
|
||||
await axios.get('/api/modules/dashdot/storage', { params: { configName, widgetId } })
|
||||
).data) as DashDotStorageLoad;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ interface DashDotGraphProps {
|
||||
dashDotUrl: string;
|
||||
usePercentages: boolean;
|
||||
info: DashDotInfo;
|
||||
widgetId: string;
|
||||
}
|
||||
|
||||
export const DashDotGraph = ({
|
||||
@@ -21,12 +22,13 @@ export const DashDotGraph = ({
|
||||
dashDotUrl,
|
||||
usePercentages,
|
||||
info,
|
||||
widgetId,
|
||||
}: DashDotGraphProps) => {
|
||||
const { t } = useTranslation('modules/dashdot');
|
||||
const { classes } = useStyles();
|
||||
|
||||
if (graph === 'storage' && isCompact) {
|
||||
return <DashDotCompactStorage info={info} />;
|
||||
return <DashDotCompactStorage info={info} widgetId={widgetId} />;
|
||||
}
|
||||
|
||||
if (graph === 'network' && isCompact) {
|
||||
|
||||
@@ -160,6 +160,7 @@ function DashDotTile({ widget }: DashDotTileProps) {
|
||||
const { data: info } = useDashDotInfo({
|
||||
dashDotUrl,
|
||||
enabled: !detectedProtocolDowngrade,
|
||||
widgetId: widget.id,
|
||||
});
|
||||
|
||||
if (detectedProtocolDowngrade) {
|
||||
@@ -197,6 +198,7 @@ function DashDotTile({ widget }: DashDotTileProps) {
|
||||
isCompact={g.subValues.compactView ?? false}
|
||||
multiView={g.subValues.multiView ?? false}
|
||||
usePercentages={usePercentages}
|
||||
widgetId={widget.id}
|
||||
/>
|
||||
</Grid.Col>
|
||||
))}
|
||||
@@ -207,7 +209,15 @@ function DashDotTile({ widget }: DashDotTileProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const useDashDotInfo = ({ dashDotUrl, enabled }: { dashDotUrl: string; enabled: boolean }) => {
|
||||
const useDashDotInfo = ({
|
||||
dashDotUrl,
|
||||
enabled,
|
||||
widgetId,
|
||||
}: {
|
||||
dashDotUrl: string;
|
||||
enabled: boolean;
|
||||
widgetId: string;
|
||||
}) => {
|
||||
const { name: configName } = useConfigContext();
|
||||
return useQuery({
|
||||
refetchInterval: 50000,
|
||||
@@ -218,15 +228,15 @@ const useDashDotInfo = ({ dashDotUrl, enabled }: { dashDotUrl: string; enabled:
|
||||
dashDotUrl,
|
||||
},
|
||||
],
|
||||
queryFn: () => fetchDashDotInfo(configName),
|
||||
queryFn: () => fetchDashDotInfo(configName, widgetId),
|
||||
enabled,
|
||||
});
|
||||
};
|
||||
|
||||
const fetchDashDotInfo = async (configName: string | undefined) => {
|
||||
const fetchDashDotInfo = async (configName: string | undefined, widgetId: string) => {
|
||||
if (!configName) return {} as DashDotInfo;
|
||||
return (await (
|
||||
await axios.get('/api/modules/dashdot/info', { params: { configName } })
|
||||
await axios.get('/api/modules/dashdot/info', { params: { configName, widgetId } })
|
||||
).data) as DashDotInfo;
|
||||
};
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { defineWidget } from '../helper';
|
||||
import { IWidget } from '../widgets';
|
||||
import { defineWidget } from '../helper';
|
||||
|
||||
const definition = defineWidget({
|
||||
id: 'rss',
|
||||
@@ -56,11 +56,11 @@ interface RssTileProps {
|
||||
widget: IRssWidget;
|
||||
}
|
||||
|
||||
const useGetRssFeed = (feedUrl: string) =>
|
||||
export const useGetRssFeed = (feedUrl: string, widgetId: string) =>
|
||||
useQuery({
|
||||
queryKey: ['rss-feed', feedUrl],
|
||||
queryFn: async () => {
|
||||
const response = await fetch('/api/modules/rss');
|
||||
const response = await fetch(`/api/modules/rss?widgetId=${widgetId}`);
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
@@ -68,7 +68,8 @@ const useGetRssFeed = (feedUrl: string) =>
|
||||
function RssTile({ widget }: RssTileProps) {
|
||||
const { t } = useTranslation('modules/rss');
|
||||
const { data, isLoading, isFetching, isError, refetch } = useGetRssFeed(
|
||||
widget.properties.rssFeedUrl
|
||||
widget.properties.rssFeedUrl,
|
||||
widget.id
|
||||
);
|
||||
const { classes } = useStyles();
|
||||
const [loadingOverlayVisible, setLoadingOverlayVisible] = useState(false);
|
||||
|
||||
@@ -13,7 +13,8 @@ import { ShapeType } from '../types/shape';
|
||||
|
||||
// Type of widgets which are saved to config
|
||||
export type IWidget<TKey extends string, TDefinition extends IWidgetDefinition> = {
|
||||
id: TKey;
|
||||
id: string;
|
||||
type: TKey;
|
||||
properties: {
|
||||
[key in keyof TDefinition['options']]: MakeLessSpecific<
|
||||
TDefinition['options'][key]['defaultValue']
|
||||
|
||||
@@ -4914,6 +4914,7 @@ __metadata:
|
||||
vitest-fetch-mock: ^0.2.2
|
||||
xml-js: ^1.6.11
|
||||
yarn: ^1.22.19
|
||||
zod: ^3.21.4
|
||||
zustand: ^4.1.4
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -8760,6 +8761,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zod@npm:^3.21.4":
|
||||
version: 3.21.4
|
||||
resolution: "zod@npm:3.21.4"
|
||||
checksum: f185ba87342ff16f7a06686767c2b2a7af41110c7edf7c1974095d8db7a73792696bcb4a00853de0d2edeb34a5b2ea6a55871bc864227dace682a0a28de33e1f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zustand@npm:^4.1.4":
|
||||
version: 4.3.6
|
||||
resolution: "zustand@npm:4.3.6"
|
||||
|
||||
Reference in New Issue
Block a user