Merge pull request #535 from ajnart/new-update-indicator

New update indicator
This commit is contained in:
Thomas Camlong
2022-12-11 14:42:08 +09:00
committed by GitHub
10 changed files with 108 additions and 160 deletions

View File

@@ -61,6 +61,7 @@
"next": "12.2.0", "next": "12.2.0",
"next-i18next": "^11.3.0", "next-i18next": "^11.3.0",
"nzbget-api": "^0.0.3", "nzbget-api": "^0.0.3",
"ping": "^0.4.2",
"prism-react-renderer": "^1.3.5", "prism-react-renderer": "^1.3.5",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -75,6 +76,7 @@
"@next/eslint-plugin-next": "^12.1.4", "@next/eslint-plugin-next": "^12.1.4",
"@types/dockerode": "^3.3.9", "@types/dockerode": "^3.3.9",
"@types/node": "17.0.1", "@types/node": "17.0.1",
"@types/ping": "^0.4.1",
"@types/react": "17.0.1", "@types/react": "17.0.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.30.7", "@typescript-eslint/eslint-plugin": "^5.30.7",

View File

@@ -113,12 +113,6 @@
"advancedOptions": { "advancedOptions": {
"title": "Advanced options", "title": "Advanced options",
"form": { "form": {
"httpStatusCodes": {
"label": "HTTP Status Codes",
"placeholder": "Select valid status codes",
"clearButtonLabel": "Clear selection",
"nothingFound": "Nothing found"
},
"openServiceInNewTab": { "openServiceInNewTab": {
"label": "Open service in new tab" "label": "Open service in new tab"
}, },

View File

@@ -7,7 +7,6 @@ import {
Image, Image,
LoadingOverlay, LoadingOverlay,
Modal, Modal,
MultiSelect,
PasswordInput, PasswordInput,
Select, Select,
Space, Space,
@@ -25,7 +24,7 @@ import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { tryMatchPort, ServiceTypeList, StatusCodes, Config } from '../../tools/types'; import { tryMatchPort, ServiceTypeList, Config } from '../../tools/types';
import apiKeyPaths from './apiKeyPaths.json'; import apiKeyPaths from './apiKeyPaths.json';
import Tip from '../layout/Tip'; import Tip from '../layout/Tip';
@@ -121,7 +120,6 @@ export function AddAppShelfItemForm(props: AddAppShelfItemFormProps) {
password: props.password ?? undefined, password: props.password ?? undefined,
openedUrl: props.openedUrl ?? undefined, openedUrl: props.openedUrl ?? undefined,
ping: props.ping ?? true, ping: props.ping ?? true,
status: props.status ?? ['200'],
newTab: props.newTab ?? true, newTab: props.newTab ?? true,
}, },
validate: { validate: {
@@ -139,12 +137,6 @@ export function AddAppShelfItemForm(props: AddAppShelfItemFormProps) {
} }
return null; return null;
}, },
status: (value: string[]) => {
if (!value.length) {
return t('modal.form.validation.noStatusCodeSelected');
}
return null;
},
}, },
}); });
@@ -190,12 +182,6 @@ export function AddAppShelfItemForm(props: AddAppShelfItemFormProps) {
if (newForm.openedUrl === '') newForm.openedUrl = undefined; if (newForm.openedUrl === '') newForm.openedUrl = undefined;
if (newForm.category === null) newForm.category = undefined; if (newForm.category === null) newForm.category = undefined;
if (newForm.ping === true) newForm.ping = undefined; if (newForm.ping === true) newForm.ping = undefined;
if (
(newForm.status.length === 1 && newForm.status[0] === '200') ||
newForm.ping === false
) {
delete newForm.status;
}
// If service already exists, update it. // If service already exists, update it.
if (config.services && config.services.find((s) => s.id === newForm.id)) { if (config.services && config.services.find((s) => s.id === newForm.id)) {
setConfig({ setConfig({
@@ -451,26 +437,10 @@ export function AddAppShelfItemForm(props: AddAppShelfItemFormProps) {
<Space h="sm" /> <Space h="sm" />
<Stack> <Stack>
<Switch <Switch
label={t('modal.tabs.advancedOptions.form.ping.label')} label="Ping service"
defaultChecked={form.values.ping} defaultChecked={form.values.ping}
{...form.getInputProps('ping')} {...form.getInputProps('ping')}
/> />
{form.values.ping && (
<MultiSelect
required
label={t('modal.tabs.advancedOptions.form.httpStatusCodes.label')}
data={StatusCodes}
placeholder={t('modal.tabs.advancedOptions.form.httpStatusCodes.placeholder')}
clearButtonLabel={t(
'modal.tabs.advancedOptions.form.httpStatusCodes.clearButtonLabel'
)}
nothingFound={t('modal.tabs.advancedOptions.form.httpStatusCodes.nothingFound')}
defaultValue={['200']}
clearable
searchable
{...form.getInputProps('status')}
/>
)}
<Switch <Switch
label={t('modal.tabs.advancedOptions.form.openServiceInNewTab.label')} label={t('modal.tabs.advancedOptions.form.openServiceInNewTab.label')}
defaultChecked={form.values.newTab} defaultChecked={form.values.newTab}

View File

@@ -1,15 +1,30 @@
import { ActionIcon, Title, Tooltip, Drawer, Tabs, ScrollArea } from '@mantine/core'; import {
import { useHotkeys } from '@mantine/hooks'; ActionIcon,
import { useState } from 'react'; Title,
import { IconSettings } from '@tabler/icons'; Tooltip,
Drawer,
Tabs,
ScrollArea,
Indicator,
Alert,
Notification,
Anchor,
} from '@mantine/core';
import { useElementSize, useHotkeys, useViewportSize } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { IconInfoCircle, IconSettings } from '@tabler/icons';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import AdvancedSettings from './AdvancedSettings'; import AdvancedSettings from './AdvancedSettings';
import CommonSettings from './CommonSettings'; import CommonSettings from './CommonSettings';
import Credits from './Credits'; import Credits from './Credits';
import { CURRENT_VERSION, REPO_URL } from '../../../data/constants';
import Link from 'next/link';
import { NextLink } from '@mantine/next';
function SettingsMenu(props: any) { function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: string }) {
const { t } = useTranslation('settings/common'); const { t } = useTranslation('settings/common');
const { height, width } = useViewportSize();
return ( return (
<Tabs defaultValue="Common"> <Tabs defaultValue="Common">
@@ -18,13 +33,16 @@ function SettingsMenu(props: any) {
<Tabs.Tab value="Customizations">{t('tabs.customizations')}</Tabs.Tab> <Tabs.Tab value="Customizations">{t('tabs.customizations')}</Tabs.Tab>
</Tabs.List> </Tabs.List>
<Tabs.Panel data-autofocus value="Common"> <Tabs.Panel data-autofocus value="Common">
<ScrollArea style={{ height: '78vh' }} offsetScrollbars> <ScrollArea style={{ height: height - 100 }} offsetScrollbars>
{newVersionAvailable && <NewUpdateIndicator newVersionAvailable={newVersionAvailable} />}
<CommonSettings /> <CommonSettings />
<Credits />
</ScrollArea> </ScrollArea>
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="Customizations"> <Tabs.Panel value="Customizations">
<ScrollArea style={{ height: '78vh' }} offsetScrollbars> <ScrollArea style={{ height: height - 120 }} offsetScrollbars>
<AdvancedSettings /> <AdvancedSettings />
<Credits />
</ScrollArea> </ScrollArea>
</Tabs.Panel> </Tabs.Panel>
</Tabs> </Tabs>
@@ -34,6 +52,17 @@ function SettingsMenu(props: any) {
export function SettingsMenuButton(props: any) { export function SettingsMenuButton(props: any) {
useHotkeys([['ctrl+L', () => setOpened(!opened)]]); useHotkeys([['ctrl+L', () => setOpened(!opened)]]);
const { t } = useTranslation('settings/common'); const { t } = useTranslation('settings/common');
const [newVersionAvailable, setNewVersionAvailable] = useState<string>('');
useEffect(() => {
// Fetch Data here when component first mounted
fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => {
res.json().then((data) => {
if (data.tag_name > CURRENT_VERSION) {
setNewVersionAvailable(data.tag_name);
}
});
});
}, [CURRENT_VERSION]);
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
@@ -47,10 +76,10 @@ export function SettingsMenuButton(props: any) {
opened={props.opened || opened} opened={props.opened || opened}
onClose={() => setOpened(false)} onClose={() => setOpened(false)}
> >
<SettingsMenu /> <SettingsMenu newVersionAvailable={newVersionAvailable} />
<Credits />
</Drawer> </Drawer>
<Tooltip label={t('tooltip')}> <Tooltip label={t('tooltip')}>
<Indicator size={15} color="blue" withBorder processing disabled={!newVersionAvailable}>
<ActionIcon <ActionIcon
variant="default" variant="default"
radius="md" radius="md"
@@ -61,7 +90,33 @@ export function SettingsMenuButton(props: any) {
> >
<IconSettings /> <IconSettings />
</ActionIcon> </ActionIcon>
</Indicator>
</Tooltip> </Tooltip>
</> </>
); );
} }
function NewUpdateIndicator({ newVersionAvailable }: { newVersionAvailable: string }) {
return (
<Notification
mt={10}
icon={<IconInfoCircle size={25} />}
disallowClose
color="teal"
radius="md"
title="New update available"
hidden={newVersionAvailable === ''}
>
Version{' '}
<b>
<Anchor
target="_blank"
href={`https://github.com/ajnart/homarr/releases/tag/${newVersionAvailable}`}
>
{newVersionAvailable}
</Anchor>
</b>{' '}
is available ! Current version: {CURRENT_VERSION}
</Notification>
);
}

View File

@@ -1,74 +0,0 @@
import React, { useEffect } from 'react';
import { createStyles, Footer as FooterComponent } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconAlertCircle as AlertCircle } from '@tabler/icons';
import { CURRENT_VERSION, REPO_URL } from '../../../data/constants';
const useStyles = createStyles((theme) => ({
footer: {
borderTop: `1px solid ${
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2]
}`,
},
inner: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: `${theme.spacing.md}px ${theme.spacing.md}px`,
[theme.fn.smallerThan('sm')]: {
flexDirection: 'column',
},
},
links: {
[theme.fn.smallerThan('sm')]: {
marginTop: theme.spacing.lg,
marginBottom: theme.spacing.sm,
},
},
}));
interface FooterCenteredProps {
links: { link: string; label: string }[];
}
export function Footer({ links }: FooterCenteredProps) {
useEffect(() => {
// Fetch Data here when component first mounted
fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => {
res.json().then((data) => {
if (data.tag_name > CURRENT_VERSION) {
showNotification({
color: 'yellow',
autoClose: false,
title: 'New version available',
icon: <AlertCircle />,
message: `Version ${data.tag_name} is available, update now!`,
});
} else if (data.tag_name < CURRENT_VERSION) {
showNotification({
color: 'orange',
autoClose: 5000,
title: 'You are using a development version',
icon: <AlertCircle />,
message: 'This version of Homarr is still in development! Bugs are expected 🐛',
});
}
});
});
}, []);
return (
<FooterComponent
height="auto"
style={{
background: 'none',
border: 'none',
clear: 'both',
}}
children={undefined}
/>
);
}

View File

@@ -1,6 +1,5 @@
import { AppShell, createStyles } from '@mantine/core'; import { AppShell, createStyles } from '@mantine/core';
import { Header } from './header/Header'; import { Header } from './header/Header';
import { Footer } from './Footer';
import Aside from './Aside'; import Aside from './Aside';
import Navbar from './Navbar'; import Navbar from './Navbar';
import { HeaderConfig } from './header/HeaderConfig'; import { HeaderConfig } from './header/HeaderConfig';
@@ -30,7 +29,6 @@ export default function Layout({ children, style }: any) {
header={<Header />} header={<Header />}
navbar={widgetPosition ? <Navbar /> : undefined} navbar={widgetPosition ? <Navbar /> : undefined}
aside={widgetPosition ? undefined : <Aside />} aside={widgetPosition ? undefined : <Aside />}
footer={<Footer links={[]} />}
> >
<HeaderConfig /> <HeaderConfig />
<Background /> <Background />

View File

@@ -62,7 +62,7 @@ export default function PingComponent(props: any) {
<motion.div <motion.div
style={{ position: 'absolute', bottom: 20, right: 20 }} style={{ position: 'absolute', bottom: 20, right: 20 }}
animate={{ animate={{
scale: isOnline === 'online' ? [1, 0.8, 1] : 1, scale: isOnline === 'online' ? [1, 0.7, 1] : 1,
}} }}
transition={{ repeat: Infinity, duration: 2.5, ease: 'easeInOut' }} transition={{ repeat: Infinity, duration: 2.5, ease: 'easeInOut' }}
> >
@@ -78,7 +78,7 @@ export default function PingComponent(props: any) {
} }
> >
<Indicator <Indicator
size={13} size={15}
color={isOnline === 'online' ? 'green' : isOnline === 'down' ? 'red' : 'yellow'} color={isOnline === 'online' ? 'green' : isOnline === 'down' ? 'red' : 'yellow'}
> >
{null} {null}

View File

@@ -1,5 +1,4 @@
import axios from 'axios'; import ping from 'ping';
import https from 'https';
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next';
async function Get(req: NextApiRequest, res: NextApiResponse) { async function Get(req: NextApiRequest, res: NextApiResponse) {

View File

@@ -35,28 +35,6 @@ interface ConfigModule {
}; };
} }
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' },
];
export const Targets = [ export const Targets = [
{ value: '_blank', label: 'New Tab' }, { value: '_blank', label: 'New Tab' },
{ value: '_top', label: 'Same Window' }, { value: '_top', label: 'Same Window' },

View File

@@ -2179,6 +2179,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/ping@npm:^0.4.1":
version: 0.4.1
resolution: "@types/ping@npm:0.4.1"
checksum: 9b94837fe66df70558c5a42b0e0c8371b4950ab56b96c42c8df809ff2cf52477dd0a7e01d2e6b38af8bb6683b3dcb54587960b96b4b1f3d40fdb529aea348ad0
languageName: node
linkType: hard
"@types/prettier@npm:^2.1.5": "@types/prettier@npm:^2.1.5":
version: 2.6.3 version: 2.6.3
resolution: "@types/prettier@npm:2.6.3" resolution: "@types/prettier@npm:2.6.3"
@@ -4843,6 +4850,7 @@ __metadata:
"@tanstack/react-query": ^4.2.1 "@tanstack/react-query": ^4.2.1
"@types/dockerode": ^3.3.9 "@types/dockerode": ^3.3.9
"@types/node": 17.0.1 "@types/node": 17.0.1
"@types/ping": ^0.4.1
"@types/react": 17.0.1 "@types/react": 17.0.1
"@types/uuid": ^8.3.4 "@types/uuid": ^8.3.4
"@typescript-eslint/eslint-plugin": ^5.30.7 "@typescript-eslint/eslint-plugin": ^5.30.7
@@ -4874,6 +4882,7 @@ __metadata:
next: 12.2.0 next: 12.2.0
next-i18next: ^11.3.0 next-i18next: ^11.3.0
nzbget-api: ^0.0.3 nzbget-api: ^0.0.3
ping: ^0.4.2
prettier: ^2.7.1 prettier: ^2.7.1
prism-react-renderer: ^1.3.5 prism-react-renderer: ^1.3.5
react: ^18.2.0 react: ^18.2.0
@@ -6897,6 +6906,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ping@npm:^0.4.2":
version: 0.4.2
resolution: "ping@npm:0.4.2"
dependencies:
q: 1.x
underscore: ^1.12.0
checksum: 43992c76fb3294734248753f2028d9fab3b919dbfae79a5ea6df7e81fc2d6d555dd0b195d6c3dbc5c89aa9dba1cd8eb58d5ecedad103ecfee64df516e5f3665b
languageName: node
linkType: hard
"pirates@npm:^4.0.4": "pirates@npm:^4.0.4":
version: 4.0.5 version: 4.0.5
resolution: "pirates@npm:4.0.5" resolution: "pirates@npm:4.0.5"
@@ -7064,7 +7083,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"q@npm:^1.4.1": "q@npm:1.x, q@npm:^1.4.1":
version: 1.5.1 version: 1.5.1
resolution: "q@npm:1.5.1" resolution: "q@npm:1.5.1"
checksum: 147baa93c805bc1200ed698bdf9c72e9e42c05f96d007e33a558b5fdfd63e5ea130e99313f28efc1783e90e6bdb4e48b67a36fcc026b7b09202437ae88a1fb12 checksum: 147baa93c805bc1200ed698bdf9c72e9e42c05f96d007e33a558b5fdfd63e5ea130e99313f28efc1783e90e6bdb4e48b67a36fcc026b7b09202437ae88a1fb12
@@ -8177,6 +8196,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"underscore@npm:^1.12.0":
version: 1.13.6
resolution: "underscore@npm:1.13.6"
checksum: d5cedd14a9d0d91dd38c1ce6169e4455bb931f0aaf354108e47bd46d3f2da7464d49b2171a5cf786d61963204a42d01ea1332a903b7342ad428deaafaf70ec36
languageName: node
linkType: hard
"unique-filename@npm:^1.1.1": "unique-filename@npm:^1.1.1":
version: 1.1.1 version: 1.1.1
resolution: "unique-filename@npm:1.1.1" resolution: "unique-filename@npm:1.1.1"