diff --git a/data/configs/default.json b/data/configs/default.json index 6a7df11ae..a14211f4e 100644 --- a/data/configs/default.json +++ b/data/configs/default.json @@ -4,205 +4,37 @@ "name": "default" }, "categories": [ - { - "id": "c1c4bec3-1044-4a80-957f-afe7ff49f421", - "name": "Test", - "position": 2 - }, { "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f", "position": 0, "name": "Example Category" + }, + { + "id": "c8407d2c-2353-4775-87c3-602f6f2684d5", + "name": "Test", + "position": 4 + }, + { + "id": "c1c4bec3-1044-4a80-957f-afe7ff49f421", + "name": "Test", + "position": 2 } ], "wrappers": [ { - "id": "943f0681-a15b-4576-9a61-a74bd6fdd3ab", + "id": "5823c4d6-6baf-4436-b990-93fe77e1dc62", "position": 1 }, { - "id": "default", + "id": "943f0681-a15b-4576-9a61-a74bd6fdd3ab", "position": 3 + }, + { + "id": "default", + "position": 5 } ], "apps": [ - { - "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://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/61e1116779fc0a9bd5bdbcc7_Frame%206.png" - }, - "integration": { - "type": null, - "properties": [] - }, - "area": { - "type": "sidebar", - "properties": { - "location": "left" - } - }, - "shape": { - "md": { - "location": { - "x": 1, - "y": 0 - }, - "size": { - "width": 1, - "height": 1 - } - }, - "sm": { - "location": { - "x": 1, - "y": 0 - }, - "size": { - "width": 1, - "height": 1 - } - }, - "lg": { - "location": { - "x": 1, - "y": 0 - }, - "size": { - "width": 1, - "height": 1 - } - } - } - }, - { - "id": "76217a87-7151-42d0-b0cf-1b72aef63f83", - "name": "Small app", - "url": "https://homarr.dev", - "appearance": { - "iconUrl": "/imgs/logo/logo.png" - }, - "network": { - "enabledStatusChecker": false, - "okStatus": [] - }, - "behaviour": { - "isOpeningNewTab": true, - "externalUrl": "https://homarr.dev" - }, - "area": { - "type": "sidebar", - "properties": { - "location": "left" - } - }, - "shape": { - "md": { - "location": { - "x": 0, - "y": 0 - }, - "size": { - "width": 1, - "height": 1 - } - }, - "sm": { - "location": { - "x": 0, - "y": 0 - }, - "size": { - "width": 1, - "height": 1 - } - }, - "lg": { - "location": { - "x": 0, - "y": 0 - }, - "size": { - "width": 1, - "height": 1 - } - } - }, - "integration": { - "type": null, - "properties": [] - } - }, - { - "id": "e41a11f5-9c6e-41bc-ac0e-4c4c47582faa", - "name": "Haha", - "url": "https://homarr.dev", - "appearance": { - "iconUrl": "/imgs/logo/logo.png" - }, - "network": { - "enabledStatusChecker": false, - "okStatus": [] - }, - "behaviour": { - "isOpeningNewTab": true, - "externalUrl": "" - }, - "area": { - "type": "category", - "properties": { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" - } - }, - "shape": { - "md": { - "location": { - "x": 5, - "y": 1 - }, - "size": { - "width": 1, - "height": 1 - } - }, - "sm": { - "location": { - "x": 2, - "y": 2 - }, - "size": { - "width": 1, - "height": 1 - } - }, - "lg": { - "location": { - "x": 4, - "y": 2 - }, - "size": { - "width": 1, - "height": 1 - } - } - }, - "integration": { - "type": null, - "properties": [] - } - }, { "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337", "name": "Discord", @@ -234,8 +66,8 @@ "shape": { "md": { "location": { - "x": 3, - "y": 0 + "x": 2, + "y": 1 }, "size": { "width": 1, @@ -254,7 +86,7 @@ }, "lg": { "location": { - "x": 0, + "x": 2, "y": 0 }, "size": { @@ -268,16 +100,23 @@ "id": "5df743d9-5cb1-457c-85d2-64ff86855652", "name": "Your app", "url": "https://homarr.dev", - "appearance": { - "iconUrl": "/imgs/logo/logo.png" + "behaviour": { + "onClickUrl": "https://homarr.dev", + "externalUrl": "https://homarr.dev", + "isOpeningNewTab": true }, "network": { "enabledStatusChecker": false, - "okStatus": [] + "okStatus": [ + 200 + ] }, - "behaviour": { - "isOpeningNewTab": true, - "externalUrl": "https://homarr.dev" + "appearance": { + "iconUrl": "/imgs/logo/logo.png" + }, + "integration": { + "type": null, + "properties": [] }, "area": { "type": "category", @@ -288,8 +127,69 @@ "shape": { "md": { "location": { - "x": 2, - "y": 0 + "x": 0, + "y": 1 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "sm": { + "location": { + "x": 0, + "y": 1 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "lg": { + "location": { + "x": 0, + "y": 1 + }, + "size": { + "width": 2, + "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://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/61e1116779fc0a9bd5bdbcc7_Frame%206.png" + }, + "integration": { + "type": null, + "properties": [] + }, + "area": { + "type": "category", + "properties": { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" + } + }, + "shape": { + "md": { + "location": { + "x": 3, + "y": 1 }, "size": { "width": 1, @@ -298,7 +198,7 @@ }, "sm": { "location": { - "x": 2, + "x": 3, "y": 1 }, "size": { @@ -308,8 +208,62 @@ }, "lg": { "location": { - "x": 2, - "y": 2 + "x": 3, + "y": 1 + }, + "size": { + "width": 1, + "height": 1 + } + } + } + }, + { + "id": "e41a11f5-9c6e-41bc-ac0e-4c4c47582faa", + "name": "Your app", + "url": "https://homarr.dev", + "appearance": { + "iconUrl": "/imgs/logo/logo.png" + }, + "network": { + "enabledStatusChecker": false, + "okStatus": [] + }, + "behaviour": { + "isOpeningNewTab": true, + "externalUrl": "" + }, + "area": { + "type": "sidebar", + "properties": { + "location": "left" + } + }, + "shape": { + "md": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "sm": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 0, + "y": 0 }, "size": { "width": 1, @@ -333,12 +287,10 @@ }, "network": { "enabledStatusChecker": false, - "okStatus": [ - 200 - ] + "okStatus": [] }, "appearance": { - "iconUrl": "https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/github.png" + "iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/github.png" }, "integration": { "type": null, @@ -354,7 +306,7 @@ "md": { "location": { "x": 0, - "y": 1 + "y": 2 }, "size": { "width": 1, @@ -363,8 +315,8 @@ }, "sm": { "location": { - "x": 1, - "y": 1 + "x": 0, + "y": 2 }, "size": { "width": 1, @@ -373,7 +325,7 @@ }, "lg": { "location": { - "x": 3, + "x": 0, "y": 2 }, "size": { @@ -383,6 +335,64 @@ } } }, + { + "id": "76217a87-7151-42d0-b0cf-1b72aef63f83", + "name": "Small app", + "url": "https://homarr.dev", + "appearance": { + "iconUrl": "/imgs/logo/logo.png" + }, + "network": { + "enabledStatusChecker": false, + "okStatus": [] + }, + "behaviour": { + "isOpeningNewTab": true, + "externalUrl": "https://homarr.dev" + }, + "area": { + "type": "category", + "properties": { + "id": "c8407d2c-2353-4775-87c3-602f6f2684d5" + } + }, + "shape": { + "md": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 6, + "height": 1 + } + }, + "sm": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 8, + "height": 1 + } + }, + "lg": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 8, + "height": 1 + } + } + }, + "integration": { + "type": null, + "properties": [] + } + }, { "id": "615e43bd-f0aa-4117-ba49-b6495c039f3e", "name": "Your app", @@ -418,7 +428,7 @@ "md": { "location": { "x": 0, - "y": 9 + "y": 5 }, "size": { "width": 1, @@ -440,110 +450,49 @@ "type": null, "properties": [] } - }, - { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33a", - "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": "c1c4bec3-1044-4a80-957f-afe7ff49f421" - } - }, - "shape": { - "md": { - "location": { - "x": 2, - "y": 0 - }, - "size": { - "width": 2, - "height": 1 - } - }, - "sm": { - "location": { - "x": 0, - "y": 0 - }, - "size": { - "width": 2, - "height": 1 - } - }, - "lg": { - "location": { - "x": 1, - "y": 2 - }, - "size": { - "width": 1, - "height": 1 - } - } - } } ], "widgets": [ { - "id": "date", + "id": "calendar", "properties": { - "display24HourFormat": true + "sundayStart": false }, "area": { - "type": "category", + "type": "wrapper", "properties": { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" + "id": "default" } }, "shape": { - "sm": { - "location": { - "x": 0, - "y": 2 - }, - "size": { - "width": 2, - "height": 1 - } - }, "md": { "location": { - "x": 4, + "x": 0, "y": 0 }, "size": { - "width": 2, - "height": 1 + "width": 12, + "height": 5 + } + }, + "sm": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 12, + "height": 5 } }, "lg": { "location": { - "x": 7, + "x": 0, "y": 0 }, "size": { - "width": 5, - "height": 2 + "width": 12, + "height": 5 } } } @@ -577,7 +526,7 @@ "y": 0 }, "size": { - "width": 3, + "width": 2, "height": 1 } }, @@ -587,52 +536,52 @@ "y": 0 }, "size": { - "width": 7, - "height": 2 + "width": 2, + "height": 1 } } } }, { - "id": "calendar", + "id": "date", "properties": { - "sundayStart": false + "display24HourFormat": true }, "area": { - "type": "wrapper", + "type": "category", "properties": { - "id": "default" + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" } }, "shape": { - "md": { - "location": { - "x": 0, - "y": 4 - }, - "size": { - "width": 3, - "height": 5 - } - }, "sm": { "location": { - "x": 0, + "x": 2, "y": 0 }, "size": { - "width": 3, - "height": 5 + "width": 2, + "height": 1 + } + }, + "md": { + "location": { + "x": 2, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 } }, "lg": { "location": { - "x": 0, + "x": 2, "y": 0 }, "size": { - "width": 3, - "height": 5 + "width": 2, + "height": 1 } } } @@ -647,8 +596,8 @@ }, "customization": { "layout": { - "enabledLeftSidebar": false, - "enabledRightSidebar": false, + "enabledLeftSidebar": true, + "enabledRightSidebar": true, "enabledDocker": false, "enabledPing": false, "enabledSearchbar": true @@ -659,9 +608,9 @@ "backgroundImageUrl": "", "customCss": "", "colors": { - "primary": "red", - "secondary": "orange", - "shade": 5 + "primary": "pink", + "secondary": "yellow", + "shade": 4 }, "appOpacity": 100 } diff --git a/next.config.js b/next.config.js index 6d3cf54a0..dde3ec977 100644 --- a/next.config.js +++ b/next.config.js @@ -8,7 +8,7 @@ module.exports = withBundleAnalyzer({ images: { domains: ['cdn.jsdelivr.net'], }, - reactStrictMode: false, + reactStrictMode: true, output: 'standalone', i18n, }); diff --git a/public/locales/en/layout/mobile/drawer.json b/public/locales/en/layout/mobile/drawer.json new file mode 100644 index 000000000..ac34cee62 --- /dev/null +++ b/public/locales/en/layout/mobile/drawer.json @@ -0,0 +1,3 @@ +{ + "title": "{{position}} sidebar" +} \ No newline at end of file diff --git a/public/locales/en/layout/modals/add-app.json b/public/locales/en/layout/modals/add-app.json index 1f08dd639..ff66e30df 100644 --- a/public/locales/en/layout/modals/add-app.json +++ b/public/locales/en/layout/modals/add-app.json @@ -46,11 +46,17 @@ "type": { "label": "Integration configuration", "description": "Treats this app as the selected integration and provides you with per-app configuration", - "placeholder": "Select an integration" + "placeholder": "Select an integration", + "defined": "Defined", + "undefined": "Undefined", + "public": "Public", + "private": "Private", + "explanationPublic": "A private secret will be sent to the server. Once your browser has refreshed the page, it will never be sent to the client.", + "explanationPrivate": "A public secret will always be sent to the client and is accessible over the API. It should not contain any confidential values such as usernames, passwords, tokens, certificates and similar" }, "secrets": { "description": "To update a secret, enter a value and click the save button. To remove a secret, use the clear button.", - "warning": "Please note that Homarr removes secrets from the configuration for security reasons. Thus, you can only either define or unset any credentials. Your credentials act as the main access for your integrations and you should never share them with anybody else. Make sure to store and manage your secrets safely.", + "warning": "Your credentials act as the access for your integrations and you should never share them with anybody else. The official Homarr team will never ask for credentials. Make sure to store and manage your secrets safely.", "clear": "Clear secret", "save": "Save secret", "update": "Update secret" diff --git a/public/locales/en/modules/search.json b/public/locales/en/modules/search.json index cf3f50de0..8b3ed6302 100644 --- a/public/locales/en/modules/search.json +++ b/public/locales/en/modules/search.json @@ -26,5 +26,6 @@ } }, "tip": "You can select the search bar with the shortcut ", - "switchedSearchEngine": "Switched to searching with {{searchEngine}}" + "switchedSearchEngine": "Switched to searching with {{searchEngine}}", + "configurationName": "Search engine configuration" } \ No newline at end of file diff --git a/public/locales/en/modules/torrents-status.json b/public/locales/en/modules/torrents-status.json index d3ab4be86..83d75d6dd 100644 --- a/public/locales/en/modules/torrents-status.json +++ b/public/locales/en/modules/torrents-status.json @@ -4,6 +4,9 @@ "description": "Displays a list of the torrent which are currently downloading", "settings": { "title": "Settings for BitTorrent integration", + "refreshInterval": { + "label": "Refresh interval (in seconds)" + }, "displayCompletedTorrents": { "label": "Display completed torrents" }, diff --git a/public/locales/en/settings/general/config-changer.json b/public/locales/en/settings/general/config-changer.json index 2ca62d86f..927547d02 100644 --- a/public/locales/en/settings/general/config-changer.json +++ b/public/locales/en/settings/general/config-changer.json @@ -1,6 +1,8 @@ { "configSelect": { - "label": "Config loader" + "label": "Config loader", + "loadingNew": "Loading your config...", + "pleaseWait": "Please wait until your new config is loaded" }, "modal": { "title": "Choose the name of your new config", diff --git a/src/components/About/AboutModal.tsx b/src/components/About/AboutModal.tsx index 0a29bf330..8d258ebd3 100644 --- a/src/components/About/AboutModal.tsx +++ b/src/components/About/AboutModal.tsx @@ -31,6 +31,7 @@ import { CURRENT_VERSION } from '../../../data/constants'; import { useConfigContext } from '../../config/provider'; import { useConfigStore } from '../../config/store'; import { usePrimaryGradient } from '../layout/useGradient'; +import Credits from '../Settings/Common/Credits'; interface AboutModalProps { opened: boolean; @@ -113,6 +114,7 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod Discord + ); }; diff --git a/src/components/Config/ConfigChanger.tsx b/src/components/Config/ConfigChanger.tsx index c9f1d204f..7d5c43c33 100644 --- a/src/components/Config/ConfigChanger.tsx +++ b/src/components/Config/ConfigChanger.tsx @@ -1,5 +1,7 @@ -import { Center, Loader, Select, Tooltip } from '@mantine/core'; +import { Center, Dialog, Loader, Notification, Select, Tooltip } from '@mantine/core'; +import { useToggle } from '@mantine/hooks'; import { useQuery } from '@tanstack/react-query'; +import { setCookie } from 'cookies-next'; import { useTranslation } from 'next-i18next'; import { useState } from 'react'; import { useConfigContext } from '../../config/provider'; @@ -7,23 +9,26 @@ import { useConfigContext } from '../../config/provider'; export default function ConfigChanger() { const { t } = useTranslation('settings/general/config-changer'); const { name: configName } = useConfigContext(); - //const loadConfig = useConfigStore((x) => x.loadConfig); + // const loadConfig = useConfigStore((x) => x.loadConfig); const { data: configs, isLoading, isError } = useConfigsQuery(); const [activeConfig, setActiveConfig] = useState(configName); + const [isRefreshing, toggle] = useToggle(); const onConfigChange = (value: string) => { // TODO: check what should happen here with @manuel-rw // Wheter it should check for the current url and then load the new config only on index // Or it should always load the selected config and open index or ? --> change url to page + setCookie('config-name', value ?? 'default', { + maxAge: 60 * 60 * 24 * 30, + sameSite: 'strict', + }); setActiveConfig(value); - /* - loadConfig(e ?? 'default'); - setCookie('config-name', e ?? 'default', { - maxAge: 60 * 60 * 24 * 30, - sameSite: 'strict', - }); - */ + toggle(); + // Use timeout to wait for the cookie to be set + setTimeout(() => { + window.location.reload(); + }, 1000); }; // If configlist is empty, return a loading indicator @@ -38,12 +43,26 @@ export default function ConfigChanger() { } return ( - + toggle()} + size="lg" + radius="md" + > + + {t('configSelect.pleaseWait')} + + + ); } diff --git a/src/components/Config/LoadConfig.tsx b/src/components/Config/LoadConfig.tsx index d2cd232ff..6f374bcd6 100644 --- a/src/components/Config/LoadConfig.tsx +++ b/src/components/Config/LoadConfig.tsx @@ -2,6 +2,7 @@ import { Group, Stack, Text, Title, useMantineTheme } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { showNotification } from '@mantine/notifications'; import { IconCheck as Check, IconPhoto, IconUpload, IconX, IconX as X } from '@tabler/icons'; +import Consola from 'consola'; import { setCookie } from 'cookies-next'; import { useTranslation } from 'next-i18next'; import { useConfigStore } from '../../config/store'; @@ -36,9 +37,7 @@ export const LoadConfigComponent = () => { let newConfig: ConfigType = JSON.parse(fileText); if (!newConfig.schemaVersion) { - // client side logging - // eslint-disable-next-line no-console - console.warn( + Consola.warn( 'a legacy configuration schema was deteced and migrated to the current schema' ); const oldConfig = JSON.parse(fileText) as Config; diff --git a/src/components/Dashboard/Mobile/Ribbon/MobileRibbon.tsx b/src/components/Dashboard/Mobile/Ribbon/MobileRibbon.tsx index 3c958e5cb..66b38249f 100644 --- a/src/components/Dashboard/Mobile/Ribbon/MobileRibbon.tsx +++ b/src/components/Dashboard/Mobile/Ribbon/MobileRibbon.tsx @@ -1,4 +1,4 @@ -import { ActionIcon, createStyles } from '@mantine/core'; +import { ActionIcon, createStyles, Space } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { IconChevronLeft, IconChevronRight } from '@tabler/icons'; import { useConfigContext } from '../../../../config/provider'; @@ -35,7 +35,9 @@ export const MobileRibbons = () => { location="left" /> - ) : null} + ) : ( + + )} {layoutSettings.enabledRightSidebar ? ( <> diff --git a/src/components/Dashboard/Mobile/Ribbon/MobileRibbonSidebarDrawer.tsx b/src/components/Dashboard/Mobile/Ribbon/MobileRibbonSidebarDrawer.tsx index 22fb2df77..071af487d 100644 --- a/src/components/Dashboard/Mobile/Ribbon/MobileRibbonSidebarDrawer.tsx +++ b/src/components/Dashboard/Mobile/Ribbon/MobileRibbonSidebarDrawer.tsx @@ -1,4 +1,5 @@ import { Drawer, Title } from '@mantine/core'; +import { useTranslation } from 'next-i18next'; import { DashboardSidebar } from '../../Wrappers/Sidebar/Sidebar'; interface MobileRibbonSidebarDrawerProps { @@ -10,16 +11,25 @@ interface MobileRibbonSidebarDrawerProps { export const MobileRibbonSidebarDrawer = ({ location, ...props -}: MobileRibbonSidebarDrawerProps) => ( - {location} sidebar} - style={{ - display: 'flex', - justifyContent: 'center', - }} - {...props} - > - - -); +}: MobileRibbonSidebarDrawerProps) => { + const { t } = useTranslation('layout/mobile/drawer'); + return ( + {t('title', { position: location })}} + style={{ + display: 'flex', + justifyContent: 'center', + }} + styles={{ + title: { + width: '100%', + }, + }} + {...props} + > + + + ); +}; diff --git a/src/components/Dashboard/Modals/EditAppModal/Tabs/AppereanceTab/IconSelector/IconSelector.tsx b/src/components/Dashboard/Modals/EditAppModal/Tabs/AppereanceTab/IconSelector/IconSelector.tsx index 8a39ed21d..8ec13cbb3 100644 --- a/src/components/Dashboard/Modals/EditAppModal/Tabs/AppereanceTab/IconSelector/IconSelector.tsx +++ b/src/components/Dashboard/Modals/EditAppModal/Tabs/AppereanceTab/IconSelector/IconSelector.tsx @@ -36,7 +36,7 @@ export const IconSelector = ({ onChange, allowAppNamePropagation, form }: IconSe const { data, isLoading } = useRepositoryIconsQuery({ url: 'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png', converter: (item) => ({ - url: `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${item.name}`, + url: `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/${item.name}`, fileName: item.name, }), }); diff --git a/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/GenericSecretInput.tsx b/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/GenericSecretInput.tsx index 778fc61b3..ad8cca248 100644 --- a/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/GenericSecretInput.tsx +++ b/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/GenericSecretInput.tsx @@ -9,15 +9,21 @@ import { Stack, ThemeIcon, Title, + Text, + Badge, + Tooltip, } from '@mantine/core'; import { TablerIcon } from '@tabler/icons'; import { useTranslation } from 'next-i18next'; import { useState } from 'react'; +import { AppIntegrationPropertyAccessabilityType } from '../../../../../../../../types/app'; interface GenericSecretInputProps { label: string; value: string; setIcon: TablerIcon; + secretIsPresent: boolean; + type: AppIntegrationPropertyAccessabilityType; onClickUpdateButton: (value: string | undefined) => void; } @@ -25,6 +31,8 @@ export const GenericSecretInput = ({ label, value, setIcon, + secretIsPresent, + type, onClickUpdateButton, ...props }: GenericSecretInputProps) => { @@ -36,17 +44,61 @@ export const GenericSecretInput = ({ const { t } = useTranslation(['layout/modals/add-app', 'common']); return ( - + - + - - {t(label)} - + + + {t(label)} + + + + {secretIsPresent ? ( + + {t('integration.type.defined')} + + ) : ( + + {t('integration.type.undefined')} + + )} + {type === 'private' ? ( + + + {t('integration.type.private')} + + + ) : ( + + + {t('integration.type.public')} + + + )} + + + + {type === 'private' + ? 'Private: Once saved, you cannot read out this value again' + : 'Public: Can be read out repeatedly'} + @@ -80,4 +132,7 @@ const useStyles = createStyles(() => ({ alignSelfCenter: { alignSelf: 'center', }, + textTransformUnset: { + textTransform: 'inherit', + }, })); diff --git a/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx b/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx index e5a64d992..b1b6dc523 100644 --- a/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx +++ b/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx @@ -36,7 +36,7 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => { label: 'Transmission', }, { - value: 'qbittorrent', + value: 'qBittorrent', image: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/qbittorrent.png', label: 'qBittorrent', }, @@ -100,16 +100,20 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => { placeholder={t('integration.type.placeholder')} itemComponent={SelectItemComponent} data={data} - maxDropdownHeight={150} + maxDropdownHeight={250} dropdownPosition="bottom" clearable variant="default" searchable + filter={(value, item) => + item.label?.toLowerCase().includes(value.toLowerCase().trim()) || + item.description?.toLowerCase().includes(value.toLowerCase().trim()) + } icon={ form.values.integration?.type && ( x.value === form.values.integration?.type)?.image} - alt="test" + alt="integration" width={20} height={20} /> @@ -119,6 +123,7 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => { form.setFieldValue('integration.properties', getNewProperties(value)); inputProps.onChange(value); }} + withinPortal {...inputProps} /> ); @@ -126,17 +131,23 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => { interface ItemProps extends React.ComponentPropsWithoutRef<'div'> { image: string; + description: string; label: string; } const SelectItemComponent = forwardRef( - ({ image, label, ...others }: ItemProps, ref) => ( + ({ image, label, description, ...others }: ItemProps, ref) => (
integration icon
{label} + {description && ( + + {description} + + )}
diff --git a/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer.tsx b/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer.tsx index 182539796..145dd3f00 100644 --- a/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer.tsx +++ b/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer.tsx @@ -45,6 +45,7 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP const formValue = form.values.integration?.properties[indexInFormValue]; const isPresent = formValue?.isDefined; + const accessabilityType = formValue?.type; if (!definition) { return ( @@ -57,6 +58,7 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP secretIsPresent={isPresent} setIcon={IconKey} value={formValue.value} + type={accessabilityType} {...form.getInputProps(`integration.properties.${index}.value`)} /> ); @@ -72,6 +74,7 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP value="" secretIsPresent={isPresent} setIcon={definition.icon} + type={accessabilityType} {...form.getInputProps(`integration.properties.${index}.value`)} /> ); diff --git a/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx b/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx index b77669b49..be42293aa 100644 --- a/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx +++ b/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx @@ -97,8 +97,8 @@ export const AvailableElementTypes = ({ iconUrl: '/imgs/logo/logo.png', }, network: { - enabledStatusChecker: false, - okStatus: [], + enabledStatusChecker: true, + okStatus: [200], }, behaviour: { isOpeningNewTab: true, diff --git a/src/components/Dashboard/Modals/SelectElement/Components/WidgetsTab/WidgetElementType.tsx b/src/components/Dashboard/Modals/SelectElement/Components/WidgetsTab/WidgetElementType.tsx index abb890ece..ce3a2b708 100644 --- a/src/components/Dashboard/Modals/SelectElement/Components/WidgetsTab/WidgetElementType.tsx +++ b/src/components/Dashboard/Modals/SelectElement/Components/WidgetsTab/WidgetElementType.tsx @@ -1,5 +1,6 @@ import { useModals } from '@mantine/modals'; -import { TablerIcon } from '@tabler/icons'; +import { showNotification } from '@mantine/notifications'; +import { IconChecks, TablerIcon } from '@tabler/icons'; import { useTranslation } from 'next-i18next'; import { useConfigContext } from '../../../../../../config/provider'; import { useConfigStore } from '../../../../../../config/store'; @@ -83,8 +84,13 @@ export const WidgetElementType = ({ id, image, disabled, widget }: WidgetElement true, !isEditMode ); - closeModal('selectElement'); + showNotification({ + title: t('descriptor.name'), + message: t('descriptor.description'), + icon: , + color: 'teal', + }); }; return ( diff --git a/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx b/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx index 20518c414..b5714c805 100644 --- a/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx +++ b/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx @@ -1,4 +1,15 @@ -import { Alert, Button, Group, MultiSelect, Stack, Switch, TextInput, Text } from '@mantine/core'; +import { + Alert, + Button, + Group, + MultiSelect, + Stack, + Switch, + TextInput, + Text, + NumberInput, + Slider, +} from '@mantine/core'; import { ContextModalProps } from '@mantine/modals'; import { IconAlertTriangle } from '@tabler/icons'; import { Trans, useTranslation } from 'next-i18next'; @@ -8,10 +19,12 @@ import type { IWidgetOptionValue } from '../../../../widgets/widgets'; import { useConfigContext } from '../../../../config/provider'; import { useConfigStore } from '../../../../config/store'; import { IWidget } from '../../../../widgets/widgets'; +import { useColorTheme } from '../../../../tools/color'; export type WidgetEditModalInnerProps = { widgetId: string; options: IWidget['properties']; + widgetOptions: IWidget['properties']; }; type IntegrationOptionsValueType = IWidget['properties'][string]; @@ -23,7 +36,11 @@ export const WidgetsEditModal = ({ }: ContextModalProps) => { const { t } = useTranslation([`modules/${innerProps.widgetId}`, 'common']); const [moduleProperties, setModuleProperties] = useState(innerProps.options); - const items = Object.entries(moduleProperties ?? {}) as [string, IntegrationOptionsValueType][]; + // const items = Object.entries(moduleProperties ?? {}) as [string, IntegrationOptionsValueType][]; + const items = Object.entries(innerProps.widgetOptions ?? {}) as [ + string, + IntegrationOptionsValueType + ][]; // Find the Key in the "Widgets" Object that matches the widgetId const currentWidgetDefinition = Widgets[innerProps.widgetId as keyof typeof Widgets]; @@ -67,8 +84,9 @@ export const WidgetsEditModal = ({ return ( - {items.map(([key, value], index) => { + {items.map(([key, defaultValue], index) => { const option = (currentWidgetDefinition as any).options[key] as IWidgetOptionValue; + const value = moduleProperties[key] ?? defaultValue; if (!option) { return ( @@ -83,39 +101,15 @@ export const WidgetsEditModal = ({ ); } - - switch (option.type) { - case 'switch': - return ( - handleChange(key, ev.currentTarget.checked)} - /> - ); - case 'text': - return ( - handleChange(key, ev.currentTarget.value)} - /> - ); - case 'multi-select': - return ( - handleChange(key, v)} - /> - ); - default: - return null; - } + return WidgetOptionTypeSwitch( + option, + index, + t, + key, + value, + handleChange, + getMutliselectData + ); })} - + + ); } diff --git a/src/components/Settings/Customization/Theme/OpacitySelector.tsx b/src/components/Settings/Customization/Theme/OpacitySelector.tsx index cc7888af6..f0eaf3bd1 100644 --- a/src/components/Settings/Customization/Theme/OpacitySelector.tsx +++ b/src/components/Settings/Customization/Theme/OpacitySelector.tsx @@ -32,7 +32,7 @@ export function OpacitySelector({ defaultValue }: OpacitySelectorProps) { }; return ( - + {t('label')} {t('tabs.customizations')} - - - + @@ -37,6 +36,8 @@ export function SettingsDrawer({ newVersionAvailable, }: SettingsDrawerProps & { newVersionAvailable: string }) { const { t } = useTranslation('settings/common'); + const { config, name: configName } = useConfigContext(); + const { updateConfig } = useConfigStore(); return ( {t('title')}} opened={opened} - onClose={closeDrawer} + onClose={() => { + closeDrawer(); + if (!configName || !config) { + return; + } + + updateConfig(configName, (_) => config, false, true); + }} > - ); } diff --git a/src/components/layout/header/Actions/AddElementAction/AddElementAction.tsx b/src/components/layout/header/Actions/AddElementAction/AddElementAction.tsx index 3c64032e2..c979b5ca8 100644 --- a/src/components/layout/header/Actions/AddElementAction/AddElementAction.tsx +++ b/src/components/layout/header/Actions/AddElementAction/AddElementAction.tsx @@ -15,9 +15,8 @@ export const AddElementAction = ({ type }: AddElementActionProps) => { return ( + + + ); const ToggleActionIconMobile = () => ( @@ -59,45 +82,24 @@ export const ToggleEditModeAction = () => { ); return ( - - - {smallerThanSm ? ( - enabled ? ( - - - - - ) : ( + <> + {smallerThanSm ? ( + enabled ? ( + + - ) - ) : enabled ? ( - - - {enabled && } - + ) : ( + + ) + ) : enabled ? ( + - )} - - - -
- setPopoverManuallyHidden(true)}> - - -
- - {t('popover.title')} - - - - -
-
+ {enabled && } + + ) : ( + + )} + ); }; diff --git a/src/components/layout/header/Header.tsx b/src/components/layout/header/Header.tsx index 21f0cd648..c70fa7128 100644 --- a/src/components/layout/header/Header.tsx +++ b/src/components/layout/header/Header.tsx @@ -4,7 +4,7 @@ import { CURRENT_VERSION, REPO_URL } from '../../../../data/constants'; import { useConfigContext } from '../../../config/provider'; import { Logo } from '../Logo'; import { useCardStyles } from '../useCardStyles'; -import DockerMenuButton from './Actions/Docker/DockerModule'; +import DockerMenuButton from '../../../modules/Docker/DockerModule'; import { ToggleEditModeAction } from './Actions/ToggleEditMode/ToggleEditMode'; import { Search } from './Search'; import { SettingsMenu } from './SettingsMenu'; diff --git a/src/components/layout/header/Search.tsx b/src/components/layout/header/Search.tsx index 619b10fce..5c28e7fcb 100644 --- a/src/components/layout/header/Search.tsx +++ b/src/components/layout/header/Search.tsx @@ -55,9 +55,6 @@ export function Search() { const [searchQuery, setSearchQuery] = useState(''); const [debounced, cancel] = useDebouncedValue(searchQuery, 250); - // TODO: ask manuel-rw about overseerr - // Answer: We can simply check if there is a app of the type overseer and display results if there is one. - // Overseerr is not use anywhere else, so it makes no sense to add a standalone toggle for displaying results const isOverseerrEnabled = config?.apps.some( (x) => x.integration.type === 'overseerr' || x.integration.type === 'jellyseerr' ); diff --git a/src/components/layout/header/SettingsMenu.tsx b/src/components/layout/header/SettingsMenu.tsx index 8452b73f7..f6a381fc0 100644 --- a/src/components/layout/header/SettingsMenu.tsx +++ b/src/components/layout/header/SettingsMenu.tsx @@ -1,4 +1,4 @@ -import { ActionIcon, Badge, Menu } from '@mantine/core'; +import { Badge, Button, Menu } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { IconInfoCircle, IconMenu2, IconSettings } from '@tabler/icons'; import { useTranslation } from 'next-i18next'; @@ -15,9 +15,9 @@ export function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: str <> - + diff --git a/src/components/layout/header/SmallAppItem.tsx b/src/components/layout/header/SmallAppItem.tsx index caa198785..02aeed486 100644 --- a/src/components/layout/header/SmallAppItem.tsx +++ b/src/components/layout/header/SmallAppItem.tsx @@ -8,7 +8,6 @@ interface smallAppItem { export default function SmallAppItem(props: any) { const { app }: { app: smallAppItem } = props; - // TODO : Use Next/link return ( {app.icon && } diff --git a/src/hooks/widgets/torrents/useGetTorrentData.tsx b/src/hooks/widgets/torrents/useGetTorrentData.tsx index 796a7b99c..b3136e03f 100644 --- a/src/hooks/widgets/torrents/useGetTorrentData.tsx +++ b/src/hooks/widgets/torrents/useGetTorrentData.tsx @@ -6,6 +6,7 @@ const POLLING_INTERVAL = 2000; interface TorrentsDataRequestParams { appId: string; + refreshInterval: number; } export const useGetTorrentData = (params: TorrentsDataRequestParams) => @@ -15,7 +16,7 @@ export const useGetTorrentData = (params: TorrentsDataRequestParams) => refetchOnWindowFocus: true, refetchInterval(_: any, query: Query) { if (query.state.fetchFailureCount < 3) { - return 5000; + return params.refreshInterval; } return false; }, diff --git a/src/components/layout/header/Actions/Docker/ContainerActionBar.tsx b/src/modules/Docker/ContainerActionBar.tsx similarity index 78% rename from src/components/layout/header/Actions/Docker/ContainerActionBar.tsx rename to src/modules/Docker/ContainerActionBar.tsx index 1b59ebd69..5a0ad3efd 100644 --- a/src/components/layout/header/Actions/Docker/ContainerActionBar.tsx +++ b/src/modules/Docker/ContainerActionBar.tsx @@ -16,8 +16,11 @@ import { useTranslation } from 'next-i18next'; import { useState } from 'react'; import { TFunction } from 'react-i18next'; import { v4 as uuidv4 } from 'uuid'; -import { openContextModalGeneric } from '../../../../../tools/mantineModalManagerExtensions'; -import { AppType } from '../../../../../types/app'; +import { useConfigContext } from '../../config/provider'; +import { tryMatchService } from '../../tools/addToHomarr'; +import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions'; +import { AppType } from '../../types/app'; +import { appTileDefinition } from '../../components/Dashboard/Tiles/Apps/AppTile'; let t: TFunction<'modules/docker', undefined>; @@ -68,6 +71,8 @@ export interface ContainerActionBarProps { export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) { t = useTranslation('modules/docker').t; const [isLoading, setisLoading] = useState(false); + const { name: configName, config } = useConfigContext(); + const getLowestWrapper = () => config?.wrappers.sort((a, b) => a.position - b.position)[0]; return ( @@ -158,61 +163,40 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction radius="md" disabled={selected.length === 0 || selected.length > 1} onClick={() => { + const app = tryMatchService(selected.at(0)!); const containerUrl = `http://localhost:${selected[0].Ports[0].PublicPort}`; - openContextModalGeneric<{ service: AppType }>({ - modal: 'editService', + openContextModalGeneric<{ app: AppType; allowAppNamePropagation: boolean }>({ + modal: 'editApp', innerProps: { - service: { + app: { id: uuidv4(), - name: selected[0].Names[0], + name: app.name ? app.name : selected[0].Names[0].substring(1), url: containerUrl, appearance: { - iconUrl: '/imgs/logo/logo.png', // TODO: find icon automatically + iconUrl: app.icon ? app.icon : '/imgs/logo/logo.png', }, network: { - enabledStatusChecker: false, - okStatus: [], + enabledStatusChecker: true, + okStatus: [200], }, behaviour: { isOpeningNewTab: true, externalUrl: '', }, area: { - type: 'sidebar', // TODO: Set the wrapper automatically + type: 'wrapper', properties: { - location: 'right', + id: getLowestWrapper()?.id ?? 'default', }, }, shape: { - lg: { - location: { - x: 0, - y: 0, - }, - size: { - height: 1, - width: 1, - }, + location: { + x: 0, + y: 0, }, - md: { - location: { - x: 0, - y: 0, - }, - size: { - height: 1, - width: 1, - }, - }, - sm: { - location: { - x: 0, - y: 0, - }, - size: { - height: 1, - width: 1, - }, + size: { + width: appTileDefinition.minWidth, + height: appTileDefinition.minHeight, }, }, integration: { @@ -220,7 +204,9 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction properties: [], }, }, + allowAppNamePropagation: true, }, + size: 'xl', }); }} > diff --git a/src/components/layout/header/Actions/Docker/ContainerState.tsx b/src/modules/Docker/ContainerState.tsx similarity index 100% rename from src/components/layout/header/Actions/Docker/ContainerState.tsx rename to src/modules/Docker/ContainerState.tsx diff --git a/src/components/layout/header/Actions/Docker/DockerModule.tsx b/src/modules/Docker/DockerModule.tsx similarity index 96% rename from src/components/layout/header/Actions/Docker/DockerModule.tsx rename to src/modules/Docker/DockerModule.tsx index 5a561dcc9..54704ec3c 100644 --- a/src/components/layout/header/Actions/Docker/DockerModule.tsx +++ b/src/modules/Docker/DockerModule.tsx @@ -5,7 +5,7 @@ import axios from 'axios'; import Docker from 'dockerode'; import { useTranslation } from 'next-i18next'; import { useEffect, useState } from 'react'; -import { useConfigContext } from '../../../../../config/provider'; +import { useConfigContext } from '../../config/provider'; import ContainerActionBar from './ContainerActionBar'; import DockerTable from './DockerTable'; @@ -60,6 +60,7 @@ export default function DockerMenuButton(props: any) { opened={opened} onClose={() => setOpened(false)} padding="xl" + position="right" size="full" title={} > diff --git a/src/components/layout/header/Actions/Docker/DockerTable.tsx b/src/modules/Docker/DockerTable.tsx similarity index 99% rename from src/components/layout/header/Actions/Docker/DockerTable.tsx rename to src/modules/Docker/DockerTable.tsx index 6f525d49e..2925e3990 100644 --- a/src/components/layout/header/Actions/Docker/DockerTable.tsx +++ b/src/modules/Docker/DockerTable.tsx @@ -118,7 +118,6 @@ export default function DockerTable({ icon={} value={search} onChange={handleSearchChange} - disabled={usedContainers.length === 0} /> diff --git a/src/pages/api/configs/[slug].ts b/src/pages/api/configs/[slug].ts index 3d1ea5605..a4bc6b5f1 100644 --- a/src/pages/api/configs/[slug].ts +++ b/src/pages/api/configs/[slug].ts @@ -45,13 +45,21 @@ function Put(req: NextApiRequest, res: NextApiResponse) { (previousProperty) => previousProperty.field === property.field ); + if (property.value !== undefined && property.value !== null) { + Consola.info( + 'Detected credential change of private secret. Value will be overwritten in configuration' + ); + return { + field: property.field, + type: property.type, + value: property.value, + }; + } + return { field: property.field, type: property.type, - value: - property.value !== undefined || property.value === null - ? property.value - : previousProperty?.value, + value: previousProperty?.value, }; }), }, diff --git a/src/pages/api/modules/overseerr/[id].tsx b/src/pages/api/modules/overseerr/[id].tsx index 3b0240271..55d253400 100644 --- a/src/pages/api/modules/overseerr/[id].tsx +++ b/src/pages/api/modules/overseerr/[id].tsx @@ -3,7 +3,7 @@ import { getCookie } from 'cookies-next'; import axios from 'axios'; import Consola from 'consola'; import { getConfig } from '../../../../tools/config/getConfig'; -import { MediaType } from '../../../../modules/overseerr/SearchResult'; +import type { MediaType } from '../../../../modules/overseerr/SearchResult'; async function Get(req: NextApiRequest, res: NextApiResponse) { // Get the slug of the request diff --git a/src/pages/api/modules/ping.ts b/src/pages/api/modules/ping.ts index 2528a8776..270ed49f8 100644 --- a/src/pages/api/modules/ping.ts +++ b/src/pages/api/modules/ping.ts @@ -17,7 +17,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) { } else if (error.code === 'ECONNABORTED') { res.status(408).json('Request Timeout'); } else { - res.status(500).json('Server Error'); + res.status(error.response ? error.response.status : 500).json('Server Error'); } }); // // Make a request to the URL diff --git a/src/styles/global.scss b/src/styles/global.scss index f77f06bd0..0c1fe9d34 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -81,7 +81,7 @@ } .grid-stack > .grid-stack-item > .grid-stack-item-content { - overflow-y: hidden; + overflow-y: auto; } .grid-stack.grid-stack-animate { diff --git a/src/tools/acceptableStatusCodes.ts b/src/tools/acceptableStatusCodes.ts index 33885ec95..3d942fd8d 100644 --- a/src/tools/acceptableStatusCodes.ts +++ b/src/tools/acceptableStatusCodes.ts @@ -1,21 +1,21 @@ export const StatusCodes = [ - { value: '200', label: '200 - OK', group: 'Sucessful responses' }, - { value: '204', label: '204 - No Content', group: 'Sucessful responses' }, - { value: '301', label: '301 - Moved Permanently', group: 'Redirection responses' }, - { value: '302', label: '302 - Found / Moved Temporarily', group: 'Redirection responses' }, - { value: '304', label: '304 - Not Modified', group: 'Redirection responses' }, - { value: '307', label: '307 - Temporary Redirect', group: 'Redirection responses' }, - { value: '308', label: '308 - Permanent Redirect', group: 'Redirection responses' }, - { value: '400', label: '400 - Bad Request', group: 'Client error responses' }, - { value: '401', label: '401 - Unauthorized', group: 'Client error responses' }, - { value: '403', label: '403 - Forbidden', group: 'Client error responses' }, - { value: '404', label: '404 - Not Found', group: 'Client error responses' }, - { value: '405', label: '405 - Method Not Allowed', group: 'Client error responses' }, - { value: '408', label: '408 - Request Timeout', group: 'Client error responses' }, - { value: '410', label: '410 - Gone', group: 'Client error responses' }, - { value: '429', label: '429 - Too Many Requests', group: 'Client error responses' }, - { value: '500', label: '500 - Internal Server Error', group: 'Server error responses' }, - { value: '502', label: '502 - Bad Gateway', group: 'Server error responses' }, - { value: '503', label: '503 - Service Unavailable', group: 'Server error responses' }, - { value: '054', label: '504 - Gateway Timeout Error', group: 'Server error responses' }, + { value: 200, label: '200 - OK', group: 'Sucessful responses' }, + { value: 204, label: '204 - No Content', group: 'Sucessful responses' }, + { value: 301, label: '301 - Moved Permanently', group: 'Redirection responses' }, + { value: 302, label: '302 - Found / Moved Temporarily', group: 'Redirection responses' }, + { value: 304, label: '304 - Not Modified', group: 'Redirection responses' }, + { value: 307, label: '307 - Temporary Redirect', group: 'Redirection responses' }, + { value: 308, label: '308 - Permanent Redirect', group: 'Redirection responses' }, + { value: 400, label: '400 - Bad Request', group: 'Client error responses' }, + { value: 401, label: '401 - Unauthorized', group: 'Client error responses' }, + { value: 403, label: '403 - Forbidden', group: 'Client error responses' }, + { value: 404, label: '404 - Not Found', group: 'Client error responses' }, + { value: 405, label: '405 - Method Not Allowed', group: 'Client error responses' }, + { value: 408, label: '408 - Request Timeout', group: 'Client error responses' }, + { value: 410, label: '410 - Gone', group: 'Client error responses' }, + { value: 429, label: '429 - Too Many Requests', group: 'Client error responses' }, + { value: 500, label: '500 - Internal Server Error', group: 'Server error responses' }, + { value: 502, label: '502 - Bad Gateway', group: 'Server error responses' }, + { value: 503, label: '503 - Service Unavailable', group: 'Server error responses' }, + { value: 504, label: '504 - Gateway Timeout Error', group: 'Server error responses' }, ]; diff --git a/src/tools/translation-namespaces.ts b/src/tools/translation-namespaces.ts index dd33af58a..1715194db 100644 --- a/src/tools/translation-namespaces.ts +++ b/src/tools/translation-namespaces.ts @@ -6,6 +6,7 @@ export const dashboardNamespaces = [ 'layout/modals/change-position', 'layout/modals/about', 'layout/header/actions/toggle-edit-mode', + 'layout/mobile/drawer', 'settings/common', 'settings/general/theme-selector', 'settings/general/config-changer', diff --git a/src/tools/types.ts b/src/tools/types.ts index 83f5b24de..cb59c6f3a 100644 --- a/src/tools/types.ts +++ b/src/tools/types.ts @@ -102,6 +102,7 @@ export const portmap = [ { name: 'nzbget', value: '6789' }, ]; +//TODO: Fix this to be used in the docker add to homarr button export const MatchingImages: { image: string; type: ServiceType; diff --git a/src/types/app.ts b/src/types/app.ts index 0f97d65ad..7edeadee9 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -52,12 +52,14 @@ export type ConfigAppIntegrationType = Omit & }; export type AppIntegrationPropertyType = { - type: 'private' | 'public'; + type: AppIntegrationPropertyAccessabilityType; field: IntegrationField; value?: string | null; isDefined: boolean; }; +export type AppIntegrationPropertyAccessabilityType = 'private' | 'public'; + type ConfigAppIntegrationPropertyType = Omit; export type IntegrationField = 'apiKey' | 'password' | 'username'; diff --git a/src/widgets/bitTorrent/BitTorrentQueueItem.tsx b/src/widgets/bitTorrent/BitTorrentQueueItem.tsx index d5d9ecb89..3c2283a58 100644 --- a/src/widgets/bitTorrent/BitTorrentQueueItem.tsx +++ b/src/widgets/bitTorrent/BitTorrentQueueItem.tsx @@ -18,7 +18,7 @@ export const BitTorrrentQueueItem = ({ torrent }: BitTorrentQueueItemProps) => { return (
- + (downloadApps[0]?.id); - const { data, isFetching, isError } = useGetTorrentData({ appId: selectedAppId! }); + const { data, isError, isInitialLoading, dataUpdatedAt } = useGetTorrentData({ + appId: selectedAppId!, + refreshInterval: widget.properties.refreshInterval * 1000, + }); useEffect(() => { if (!selectedAppId && downloadApps.length) { @@ -92,9 +109,15 @@ function BitTorrentTile({ widget }: BitTorrentTileProps) { ); } - if (isFetching) { + if (isInitialLoading) { return ( - + {t('card.loading.title')} @@ -124,26 +147,35 @@ function BitTorrentTile({ widget }: BitTorrentTileProps) { return true; }; + const difference = new Date().getTime() - dataUpdatedAt; + const duration = dayjs.duration(difference, 'ms'); + const humanizedDuration = duration.humanize(); + return ( - - - - - - - {width > MIN_WIDTH_MOBILE && } - {width > MIN_WIDTH_MOBILE && } - {width > MIN_WIDTH_MOBILE && } - - - - - {data.filter(filter).map((item: NormalizedTorrent, index: number) => ( - - ))} - -
{t('card.table.header.name')}{t('card.table.header.size')}{t('card.table.header.download')}{t('card.table.header.upload')}{t('card.table.header.estimatedTimeOfArrival')}{t('card.table.header.progress')}
-
+ + + + + + + + {width > MIN_WIDTH_MOBILE && } + {width > MIN_WIDTH_MOBILE && } + {width > MIN_WIDTH_MOBILE && } + + + + + {data.filter(filter).map((item: NormalizedTorrent, index: number) => ( + + ))} + +
{t('card.table.header.name')}{t('card.table.header.size')}{t('card.table.header.download')}{t('card.table.header.upload')}{t('card.table.header.estimatedTimeOfArrival')}{t('card.table.header.progress')}
+
+ + Last updated {humanizedDuration} ago + +
); } diff --git a/src/widgets/dashDot/DashDotTile.tsx b/src/widgets/dashDot/DashDotTile.tsx index 531a25313..fcf8271c1 100644 --- a/src/widgets/dashDot/DashDotTile.tsx +++ b/src/widgets/dashDot/DashDotTile.tsx @@ -54,9 +54,11 @@ function DashDotTile({ widget }: DashDotTileProps) { const { classes } = useDashDotTileStyles(); const { t } = useTranslation('modules/dashdot'); - const dashDotUrl = widget?.properties.url; + const dashDotUrl = widget.properties.url; - const { data: info } = useDashDotInfo({ dashDotUrl }); + const { data: info } = useDashDotInfo({ + dashDotUrl, + }); const graphs = widget?.properties.graphs.map((g) => ({ id: g, @@ -112,6 +114,7 @@ function DashDotTile({ widget }: DashDotTileProps) { const useDashDotInfo = ({ dashDotUrl }: { dashDotUrl: string }) => { const { name: configName } = useConfigContext(); return useQuery({ + refetchInterval: 50000, queryKey: [ 'dashdot/info', { diff --git a/src/widgets/date/DateTile.tsx b/src/widgets/date/DateTile.tsx index 843dc2564..5e830a792 100644 --- a/src/widgets/date/DateTile.tsx +++ b/src/widgets/date/DateTile.tsx @@ -1,4 +1,4 @@ -import { Center, Stack, Text, Title } from '@mantine/core'; +import { Stack, Text, Title } from '@mantine/core'; import { IconClock } from '@tabler/icons'; import dayjs from 'dayjs'; import { useEffect, useRef, useState } from 'react'; diff --git a/src/widgets/helper.ts b/src/widgets/helper.ts index 1344584bb..238fd88a4 100644 --- a/src/widgets/helper.ts +++ b/src/widgets/helper.ts @@ -5,6 +5,4 @@ import { IWidgetDefinition } from './widgets'; // The options of IWidgetDefinition are so heavily typed that it even used 'true' as type export const defineWidget = >( options: TOptions -) => { - return options; -}; +) => options; diff --git a/src/widgets/useNet/UseNetTile.tsx b/src/widgets/useNet/UseNetTile.tsx index 61182c37e..494df5132 100644 --- a/src/widgets/useNet/UseNetTile.tsx +++ b/src/widgets/useNet/UseNetTile.tsx @@ -46,16 +46,20 @@ const definition = defineWidget({ }, }); -export type IWeatherWidget = IWidget; +export type IUsenetWidget = IWidget; -interface UseNetTileProps {} +interface UseNetTileProps { + widget: IUsenetWidget; +} -function UseNetTile({}: UseNetTileProps) { +function UseNetTile({ widget }: UseNetTileProps) { const { t } = useTranslation('modules/usenet'); const { config } = useConfigContext(); const downloadApps = config?.apps.filter((x) => x.integration && downloadAppTypes.includes(x.integration.type)) ?? []; + const { ref, width, height } = useElementSize(); + const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs; const [selectedAppId, setSelectedApp] = useState(downloadApps[0]?.id); const { data } = useGetUsenetInfo({ appId: selectedAppId! }); @@ -84,9 +88,6 @@ function UseNetTile({}: UseNetTileProps) { return null; } - const { ref, width, height } = useElementSize(); - const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs; - return ( diff --git a/src/widgets/useNet/UsenetHistoryList.tsx b/src/widgets/useNet/UsenetHistoryList.tsx index 119356dea..28ee10ccf 100644 --- a/src/widgets/useNet/UsenetHistoryList.tsx +++ b/src/widgets/useNet/UsenetHistoryList.tsx @@ -4,8 +4,8 @@ import { Code, Group, Pagination, - ScrollArea, Skeleton, + Stack, Table, Text, Title, @@ -28,7 +28,7 @@ interface UsenetHistoryListProps { appId: string; } -const PAGE_SIZE = 10; +const PAGE_SIZE = 13; export const UsenetHistoryList: FunctionComponent = ({ appId }) => { const [page, setPage] = useState(1); @@ -39,7 +39,7 @@ export const UsenetHistoryList: FunctionComponent = ({ a const { data, isLoading, isError, error } = useGetUsenetHistory({ limit: PAGE_SIZE, offset: (page - 1) * PAGE_SIZE, - appId: appId, + appId, }); const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE); @@ -81,50 +81,49 @@ export const UsenetHistoryList: FunctionComponent = ({ a } return ( - <> - - - - - - + +
{t('modules/usenet:history.header.name')}{t('modules/usenet:history.header.size')}
+ + + + + {durationBreakpoint < width ? ( + + ) : null} + + + + {data.items.map((history) => ( + + + {durationBreakpoint < width ? ( - + ) : null} - - - {data.items.map((history) => ( - - - - {durationBreakpoint < width ? ( - - ) : null} - - ))} - -
{t('modules/usenet:history.header.name')}{t('modules/usenet:history.header.size')}{t('modules/usenet:history.header.duration')}
+ + + {history.name} + + + + {humanFileSize(history.size)} + {t('modules/usenet:history.header.duration')} + {parseDuration(history.time, t)} +
- - - {history.name} - - - - {humanFileSize(history.size)} - - {parseDuration(history.time, t)} -
-
+ ))} + +
{totalPages > 1 && ( = ({ a onChange={setPage} /> )} - + ); }; diff --git a/src/widgets/useNet/UsenetQueueList.tsx b/src/widgets/useNet/UsenetQueueList.tsx index 3138048ee..5fec4f842 100644 --- a/src/widgets/useNet/UsenetQueueList.tsx +++ b/src/widgets/useNet/UsenetQueueList.tsx @@ -1,6 +1,7 @@ import { ActionIcon, Alert, + Button, Center, Code, Group, @@ -8,6 +9,7 @@ import { Progress, ScrollArea, Skeleton, + Stack, Table, Text, Title, @@ -30,7 +32,7 @@ interface UsenetQueueListProps { appId: string; } -const PAGE_SIZE = 10; +const PAGE_SIZE = 13; export const UsenetQueueList: FunctionComponent = ({ appId }) => { const theme = useMantineTheme(); @@ -38,13 +40,13 @@ export const UsenetQueueList: FunctionComponent = ({ appId const progressbarBreakpoint = theme.breakpoints.xs; const progressBreakpoint = 400; const sizeBreakpoint = 300; - const { ref, width, height } = useElementSize(); + const { ref, width } = useElementSize(); const [page, setPage] = useState(1); const { data, isLoading, isError, error } = useGetUsenetDownloads({ limit: PAGE_SIZE, offset: (page - 1) * PAGE_SIZE, - appId: appId, + appId, }); const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE); @@ -85,103 +87,102 @@ export const UsenetQueueList: FunctionComponent = ({ appId ); } + // TODO: Set ScollArea dynamic height based on the widget size return ( - <> - - - - - + +
- {t('queue.header.name')}
+ + + + {sizeBreakpoint < width ? ( + + ) : null} + + {progressBreakpoint < width ? ( + + ) : null} + + + + {data.items.map((nzb) => ( + + + {sizeBreakpoint < width ? ( - + ) : null} - + {progressBreakpoint < width ? ( - + ) : null} - - - {data.items.map((nzb) => ( - - - - {sizeBreakpoint < width ? ( - - ) : null} - - {progressBreakpoint < width ? ( - - ) : null} - - ))} - -
+ {t('queue.header.name')}{t('queue.header.size')}{t('queue.header.eta')} width ? 100 : 200 }}> + {t('queue.header.progress')} +
+ {nzb.state === 'paused' ? ( + + + + + + ) : ( + + + + + + )} + + + + {nzb.name} + + + {t('queue.header.size')} + {humanFileSize(nzb.size)} + {t('queue.header.eta')} + {nzb.eta <= 0 ? ( + + {t('queue.paused')} + + ) : ( + {dayjs.duration(nzb.eta, 's').format('H:mm:ss')} + )} + width ? 100 : 200 }}> - {t('queue.header.progress')} - + + {nzb.progress.toFixed(1)}% + + {width > progressbarBreakpoint ? ( + 0 ? theme.primaryColor : 'lightgrey'} + value={nzb.progress} + size="lg" + style={{ width: '100%' }} + /> + ) : null} +
- {nzb.state === 'paused' ? ( - - - - - - ) : ( - - - - - - )} - - - - {nzb.name} - - - - {humanFileSize(nzb.size)} - - {nzb.eta <= 0 ? ( - - {t('queue.paused')} - - ) : ( - {dayjs.duration(nzb.eta, 's').format('H:mm:ss')} - )} - - - {nzb.progress.toFixed(1)}% - - {width > progressbarBreakpoint ? ( - 0 ? theme.primaryColor : 'lightgrey'} - value={nzb.progress} - size="lg" - style={{ width: '100%' }} - /> - ) : null} -
-
+ ))} + + {totalPages > 1 && ( )} - + ); }; diff --git a/src/widgets/widgets.d.ts b/src/widgets/widgets.d.ts index 729ed2e07..19e41ed12 100644 --- a/src/widgets/widgets.d.ts +++ b/src/widgets/widgets.d.ts @@ -1,6 +1,5 @@ -import { IconSun, TablerIcon } from '@tabler/icons'; +import { TablerIcon } from '@tabler/icons'; import React from 'react'; -import { BaseTileProps } from '../components/Dashboard/Tiles/type'; // Type of widgets which are safed to config export type IWidget = { @@ -32,6 +31,7 @@ export type IWidgetOptionValue = | IMultiSelectOptionValue | ISwitchOptionValue | ITextInputOptionValue + | ISliderInputOptionValue | INumberInputOptionValue; // will show a multi-select with specified data @@ -56,7 +56,16 @@ export type ITextInputOptionValue = { // will show a number-input export type INumberInputOptionValue = { type: 'number'; - defaultValue: string; + defaultValue: number; +}; + +// will show a slider-input +export type ISliderInputOptionValue = { + type: 'slider'; + defaultValue: number; + min: number; + max: number; + step: number; }; // is used to type the widget definitions which will be used to display all widgets