Compare commits

...

31 Commits

Author SHA1 Message Date
Thomas Camlong
ce0f27bb6e v0.8.0 Docker 🐋 and Dash. ⚙ integrations !
<!-- Small release message -->

-  Add support for lists in module option by @ajnart in https://github.com/ajnart/homarr/pull/280
- 🔨 Fix Readarr default port number by @Moohan in https://github.com/ajnart/homarr/pull/287
-  Add dash. Integration by @MauriceNino in https://github.com/ajnart/homarr/pull/277
-  Add Docker integration by @ajnart in https://github.com/ajnart/homarr/pull/289

- Dash. (Pronounced Dashdot) is another self-hosted service, made by @MauriceNino that provides a simple way to see stats about your PC in a sleek way
- Docker integration provides a simple way to start, stop, restart and delete containers. To get started, simply mount your docker socket by adding `-v /var/run/docker.sock:/var/run/docker.sock` to your Homarr container !

* @Moohan made their first contribution in https://github.com/ajnart/homarr/pull/287
* @MauriceNino made their first contribution in https://github.com/ajnart/homarr/pull/277

**Full Changelog**: https://github.com/ajnart/homarr/compare/v0.7.2...v0.8.0
2022-07-20 16:49:48 +02:00
Thomas Camlong
5c1a171832 🔀 Merge pull request #289 from ajnart/docker-integration
Add Docker integration 🚀
2022-07-20 15:22:40 +02:00
Thomas "ajnart" Camlong
fd8ab2f643 🔀 Backmerge dev with dash. integration 2022-07-20 15:15:07 +02:00
Thomas Camlong
c750eed5ef Merge branch 'dev' into docker-integration 2022-07-20 15:05:16 +02:00
Thomas "ajnart" Camlong
c446bf1a1f 📦 Update cookies-next package 2022-07-20 15:02:09 +02:00
Thomas Camlong
0fdfa55067 🔀 Merge pull request #277 from MauriceNino/feature/276
Add dash. Integration thanks to @MauriceNino !
2022-07-20 14:52:12 +02:00
Thomas "ajnart" Camlong
c313eacefd 🐛 Fix small bug with the network module 2022-07-20 14:47:51 +02:00
Thomas "ajnart" Camlong
649f7521bc 🔒 Add guard for Docker socket 2022-07-20 14:21:11 +02:00
Thomas "ajnart" Camlong
7065b06c82 💄 Format code 2022-07-20 14:09:47 +02:00
Thomas "ajnart" Camlong
c4e01e482e Add simple image name matching 2022-07-20 14:08:56 +02:00
Thomas Camlong
e56c4b6b56 🔀 Merge pull request #287 from Moohan/master
Fix Readarr default port number
2022-07-11 13:54:01 +02:00
James McMahon
ce38163c6d Fix Readarr default port number
Per https://wiki.servarr.com/readarr
2022-07-11 12:21:28 +01:00
Thomas Camlong
0406d6d5ee Add skeleton while the meto module is loading 2022-07-07 07:13:11 +00:00
Thomas Camlong
4b92c52ea8 Add "Add to homarr" feature and move code 2022-07-06 18:08:39 +02:00
Thomas Camlong
be770d282a ⬆️ Upgrade NextJS version 2022-07-06 18:08:03 +02:00
MauriceNino
0bf95483f9 fix: styles for dash. widget 2022-06-30 16:08:39 +02:00
MauriceNino
60b88389a6 fix: remove leftover console.log 2022-06-30 16:08:39 +02:00
MauriceNino
72832a5767 fix: move enabled options to multi-select 2022-06-30 16:08:39 +02:00
MauriceNino
eb0313f551 fix: transform dash. -> dashdot for icon find 2022-06-30 16:08:39 +02:00
MauriceNino
c0ecc3d4c6 fix: types 2022-06-30 16:08:38 +02:00
MauriceNino
da7b478d81 feat: add dash. integration 2022-06-30 16:08:38 +02:00
Thomas Camlong
2702c9a7cf 🔀 Merge pull request #280 from ajnart/ajnart/issue279
 Add support for lists in module option
2022-06-28 22:33:28 +02:00
Thomas Camlong
3bda6c2b76 🔥 Remove the popover TIP when using the searchbar 2022-06-28 19:09:02 +02:00
Thomas Camlong
1a66bfb8be add a <Tip/> component and use it 2022-06-28 19:08:18 +02:00
Thomas Camlong
41be0e6362 🐛 Fix default values for modules
The default value was not set correctly for modules. This has been fixed. It was also fixed in the Weather Module and the Date Module.
2022-06-28 12:12:39 +02:00
Thomas Camlong
e93a3a3b5f Add support for lists in module option
This feature allows a module maker to use a list as the different possible values for a module integration.
2022-06-28 11:27:23 +02:00
Thomas Camlong
9945ef892e 📱 Fix settings pannels height 2022-06-28 11:06:45 +02:00
Thomas Camlong
812de35149 🐛 Fix a bug where download module was always there 2022-06-28 10:34:25 +02:00
Thomas Camlong
035224b02b add start/stop/restart feature on containers 2022-06-27 23:38:54 +02:00
Thomas Camlong
72aba9d8cd 🚧 Work in progress on the Docker integration 2022-06-27 19:25:26 +02:00
ajnart
df7e833b84 🚧 Work in progress on docker integration 2022-06-27 08:03:40 +02:00
31 changed files with 1398 additions and 256 deletions

View File

@@ -1,2 +1,2 @@
export const REPO_URL = 'ajnart/homarr';
export const CURRENT_VERSION = 'v0.7.2';
export const CURRENT_VERSION = 'v0.8.0';

View File

@@ -9,8 +9,6 @@ module.exports = withBundleAnalyzer({
eslint: {
ignoreDuringBuilds: true,
},
experimental: {
outputStandalone: true,
},
output: 'standalone',
basePath: env.BASE_URL,
});

View File

@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "0.7.2",
"version": "0.8.0",
"description": "Homarr - A homepage for your server.",
"repository": {
"type": "git",
@@ -43,11 +43,12 @@
"@nivo/line": "^0.79.1",
"@tabler/icons": "^1.68.0",
"axios": "^0.27.2",
"cookies-next": "^2.0.4",
"cookies-next": "^2.1.1",
"dayjs": "^1.11.3",
"dockerode": "^3.3.2",
"framer-motion": "^6.3.1",
"js-file-download": "^0.4.12",
"next": "12.1.6",
"next": "^12.2.0",
"prism-react-renderer": "^1.3.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
@@ -56,9 +57,10 @@
},
"devDependencies": {
"@babel/core": "^7.17.8",
"@next/bundle-analyzer": "^12.1.4",
"@next/eslint-plugin-next": "^12.1.4",
"@next/bundle-analyzer": "^12.2.0",
"@next/eslint-plugin-next": "^12.2.0",
"@storybook/react": "^6.5.4",
"@types/dockerode": "^3.3.9",
"@types/node": "^17.0.23",
"@types/react": "17.0.43",
"@types/uuid": "^8.3.4",

View File

@@ -1,29 +1,30 @@
import {
Modal,
ActionIcon,
Anchor,
Button,
Center,
Group,
TextInput,
Image,
Button,
Select,
LoadingOverlay,
ActionIcon,
Tooltip,
Title,
Anchor,
Text,
Tabs,
Modal,
MultiSelect,
ScrollArea,
Select,
Switch,
Tabs,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { useEffect, useState } from 'react';
import { IconApps as Apps } from '@tabler/icons';
import { v4 as uuidv4 } from 'uuid';
import { useDebouncedValue } from '@mantine/hooks';
import { IconApps as Apps } from '@tabler/icons';
import { useEffect, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { useConfig } from '../../tools/state';
import { ServiceTypeList, StatusCodes } from '../../tools/types';
import Tip from '../layout/Tip';
export function AddItemShelfButton(props: any) {
const [opened, setOpened] = useState(false);
@@ -58,7 +59,8 @@ function MatchIcon(name: string, form: any) {
fetch(
`https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name
.replace(/\s+/g, '-')
.toLowerCase()}.png`
.toLowerCase()
.replace(/^dash\.$/, 'dashdot')}.png`
).then((res) => {
if (res.ok) {
form.setFieldValue('icon', res.url);
@@ -81,9 +83,10 @@ function MatchPort(name: string, form: any) {
{ name: 'sonarr', value: '8989' },
{ name: 'radarr', value: '7878' },
{ name: 'lidarr', value: '8686' },
{ name: 'readarr', value: '8686' },
{ name: 'readarr', value: '8787' },
{ name: 'deluge', value: '8112' },
{ name: 'transmission', value: '9091' },
{ name: 'dash.', value: '3001' },
];
// Match name with portmap key
const port = portmap.find((p) => p.name === name.toLowerCase());
@@ -92,6 +95,8 @@ function MatchPort(name: string, form: any) {
}
}
const DEFAULT_ICON = '/favicon.svg';
export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & any) {
const { setOpened } = props;
const { config, setConfig } = useConfig();
@@ -111,7 +116,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
type: props.type ?? 'Other',
category: props.category ?? undefined,
name: props.name ?? '',
icon: props.icon ?? '/favicon.svg',
icon: props.icon ?? DEFAULT_ICON,
url: props.url ?? '',
apiKey: props.apiKey ?? (undefined as unknown as string),
username: props.username ?? (undefined as unknown as string),
@@ -146,7 +151,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
const [debounced, cancel] = useDebouncedValue(form.values.name, 250);
useEffect(() => {
if (form.values.name !== debounced || props.name || props.type) return;
if (form.values.name !== debounced || form.values.icon !== DEFAULT_ICON) return;
MatchIcon(form.values.name, form);
MatchService(form.values.name, form);
MatchPort(form.values.name, form);
@@ -219,7 +224,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
<TextInput
required
label="Icon URL"
placeholder="/favicon.svg"
placeholder={DEFAULT_ICON}
{...form.getInputProps('icon')}
/>
<TextInput
@@ -273,15 +278,8 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
}}
error={form.errors.apiKey && 'Invalid API key'}
/>
<Text
style={{
alignSelf: 'center',
fontSize: '0.75rem',
textAlign: 'center',
color: 'gray',
}}
>
Tip: Get your API key{' '}
<Tip>
Get your API key{' '}
<Anchor
target="_blank"
weight="bold"
@@ -290,7 +288,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
>
here.
</Anchor>
</Text>
</Tip>
</>
)}
{form.values.type === 'qBittorrent' && (

View File

@@ -152,6 +152,7 @@ const AppShelf = (props: any) => {
const noCategory = config.services.filter(
(e) => e.category === undefined || e.category === null
);
const downloadEnabled = config.modules?.[DownloadsModule.title]?.enabled ?? false;
// Create an item with 0: true, 1: true, 2: true... For each category
return (
// Return one item for each category
@@ -176,6 +177,7 @@ const AppShelf = (props: any) => {
{item()}
</Accordion.Item>
) : null}
{downloadEnabled ? (
<Accordion.Item key="Downloads" label="Your downloads">
<Paper
p="lg"
@@ -191,6 +193,7 @@ const AppShelf = (props: any) => {
<DownloadComponent />
</Paper>
</Accordion.Item>
) : null}
</Accordion>
</Group>
);

View File

@@ -0,0 +1,182 @@
import { Button, Group, Modal, Title } from '@mantine/core';
import { useBooleanToggle } from '@mantine/hooks';
import { showNotification, updateNotification } from '@mantine/notifications';
import {
IconCheck,
IconPlayerPlay,
IconPlayerStop,
IconPlus,
IconRefresh,
IconRotateClockwise,
IconTrash,
IconX,
} from '@tabler/icons';
import axios from 'axios';
import Dockerode from 'dockerode';
import { tryMatchService } from '../../tools/addToHomarr';
import { useConfig } from '../../tools/state';
import { AddAppShelfItemForm } from '../AppShelf/AddAppShelfItem';
function sendDockerCommand(action: string, containerId: string, containerName: string) {
showNotification({
id: containerId,
loading: true,
title: `${action}ing container ${containerName.substring(1)}`,
message: undefined,
autoClose: false,
disallowClose: true,
});
axios.get(`/api/docker/container/${containerId}?action=${action}`).then((res) => {
setTimeout(() => {
if (res.data.success === true) {
updateNotification({
id: containerId,
title: `Container ${containerName} ${action}ed`,
message: `Your container was successfully ${action}ed`,
icon: <IconCheck />,
autoClose: 2000,
});
}
if (res.data.success === false) {
updateNotification({
id: containerId,
color: 'red',
title: 'There was an error with your container.',
message: undefined,
icon: <IconX />,
autoClose: 2000,
});
}
}, 500);
});
}
export interface ContainerActionBarProps {
selected: Dockerode.ContainerInfo[];
reload: () => void;
}
export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) {
const { config, setConfig } = useConfig();
const [opened, setOpened] = useBooleanToggle(false);
return (
<Group>
<Modal
size="xl"
radius="md"
opened={opened}
onClose={() => setOpened(false)}
title="Add service"
>
<AddAppShelfItemForm
setOpened={setOpened}
{...tryMatchService(selected.at(0))}
message="Add service to homarr"
/>
</Modal>
<Button
leftIcon={<IconRotateClockwise />}
onClick={() =>
Promise.all(
selected.map((container) =>
sendDockerCommand('restart', container.Id, container.Names[0].substring(1))
)
).then(() => reload())
}
variant="light"
color="orange"
radius="md"
>
Restart
</Button>
<Button
leftIcon={<IconPlayerStop />}
onClick={() =>
Promise.all(
selected.map((container) => {
if (
container.State === 'stopped' ||
container.State === 'created' ||
container.State === 'exited'
) {
return showNotification({
id: container.Id,
title: `Failed to stop ${container.Names[0].substring(1)}`,
message: "You can't stop a stopped container",
autoClose: 1000,
});
}
return sendDockerCommand('stop', container.Id, container.Names[0].substring(1));
})
).then(() => reload())
}
variant="light"
color="red"
radius="md"
>
Stop
</Button>
<Button
leftIcon={<IconPlayerPlay />}
onClick={() =>
Promise.all(
selected.map((container) =>
sendDockerCommand('start', container.Id, container.Names[0].substring(1))
)
).then(() => reload())
}
variant="light"
color="green"
radius="md"
>
Start
</Button>
<Button leftIcon={<IconRefresh />} onClick={() => reload()} variant="light" radius="md">
Refresh data
</Button>
<Button
leftIcon={<IconPlus />}
color="indigo"
variant="light"
radius="md"
onClick={() => {
if (selected.length !== 1) {
showNotification({
autoClose: 5000,
title: <Title order={4}>Please only add one service at a time!</Title>,
color: 'red',
message: undefined,
});
} else {
setOpened(true);
}
}}
>
Add to Homarr
</Button>
<Button
leftIcon={<IconTrash />}
color="red"
variant="light"
radius="md"
onClick={() =>
Promise.all(
selected.map((container) => {
if (container.State === 'running') {
return showNotification({
id: container.Id,
title: `Failed to delete ${container.Names[0].substring(1)}`,
message: "You can't delete a running container",
autoClose: 1000,
});
}
return sendDockerCommand('remove', container.Id, container.Names[0].substring(1));
})
).then(() => reload())
}
>
Remove
</Button>
</Group>
);
}

View File

@@ -0,0 +1,49 @@
import { Badge, BadgeVariant, MantineSize } from '@mantine/core';
import Dockerode from 'dockerode';
export interface ContainerStateProps {
state: Dockerode.ContainerInfo['State'];
}
export default function ContainerState(props: ContainerStateProps) {
const { state } = props;
const options: {
size: MantineSize;
radius: MantineSize;
variant: BadgeVariant;
} = {
size: 'md',
radius: 'md',
variant: 'outline',
};
switch (state) {
case 'running': {
return (
<Badge color="green" {...options}>
Running
</Badge>
);
}
case 'created': {
return (
<Badge color="cyan" {...options}>
Created
</Badge>
);
}
case 'exited': {
return (
<Badge color="red" {...options}>
Stopped
</Badge>
);
}
default: {
return (
<Badge color="purple" {...options}>
Unknown
</Badge>
);
}
}
}

View File

@@ -0,0 +1,53 @@
import { ActionIcon, Drawer, Group, LoadingOverlay } from '@mantine/core';
import { IconBrandDocker } from '@tabler/icons';
import axios from 'axios';
import { useEffect, useState } from 'react';
import Docker from 'dockerode';
import ContainerActionBar from './ContainerActionBar';
import DockerTable from './DockerTable';
export default function DockerDrawer(props: any) {
const [opened, setOpened] = useState(false);
const [containers, setContainers] = useState<Docker.ContainerInfo[]>([]);
const [selection, setSelection] = useState<Docker.ContainerInfo[]>([]);
const [visible, setVisible] = useState(false);
function reload() {
setVisible(true);
setTimeout(() => {
axios.get('/api/docker/containers').then((res) => {
setContainers(res.data);
setSelection([]);
setVisible(false);
});
}, 300);
}
useEffect(() => {
reload();
}, []);
// Check if the user has at least one container
if (containers.length < 1) return null;
return (
<>
<Drawer opened={opened} onClose={() => setOpened(false)} padding="xl" size="full">
<ContainerActionBar selected={selection} reload={reload} />
<div style={{ position: 'relative' }}>
<LoadingOverlay transitionDuration={500} visible={visible} />
<DockerTable containers={containers} selection={selection} setSelection={setSelection} />
</div>
</Drawer>
<Group position="center">
<ActionIcon
variant="default"
radius="md"
size="xl"
color="blue"
onClick={() => setOpened(true)}
>
<IconBrandDocker />
</ActionIcon>
</Group>
</>
);
}

View File

@@ -0,0 +1,91 @@
import { Menu, Text, useMantineTheme } from '@mantine/core';
import { showNotification, updateNotification } from '@mantine/notifications';
import {
IconCheck,
IconCodePlus,
IconPlayerPlay,
IconPlayerStop,
IconRotateClockwise,
IconX,
} from '@tabler/icons';
import axios from 'axios';
import Dockerode from 'dockerode';
function sendNotification(action: string, containerId: string, containerName: string) {
showNotification({
id: 'load-data',
loading: true,
title: `${action}ing container ${containerName}`,
message: 'Your password is being checked...',
autoClose: false,
disallowClose: true,
});
axios.get(`/api/docker/container/${containerId}?action=${action}`).then((res) => {
setTimeout(() => {
if (res.data.success === true) {
updateNotification({
id: 'load-data',
title: 'Container restarted',
message: 'Your container was successfully restarted',
icon: <IconCheck />,
autoClose: 2000,
});
}
if (res.data.success === false) {
updateNotification({
id: 'load-data',
color: 'red',
title: 'There was an error restarting your container.',
message: 'Your container has encountered issues while restarting.',
icon: <IconX />,
autoClose: 2000,
});
}
}, 500);
});
}
function restart(container: Dockerode.ContainerInfo) {
sendNotification('restart', container.Id, container.Names[0]);
}
function stop(container: Dockerode.ContainerInfo) {
console.log('stoping container', container.Id);
}
function start(container: Dockerode.ContainerInfo) {
console.log('starting container', container.Id);
}
export default function DockerMenu(props: any) {
const { container }: { container: Dockerode.ContainerInfo } = props;
const theme = useMantineTheme();
if (container === undefined) {
return null;
}
return (
<Menu shadow="lg" radius="md">
<Menu.Label>Actions</Menu.Label>
<Menu.Item icon={<IconRotateClockwise color="orange" />} onClick={() => restart(container)}>
<Text>Restart</Text>
</Menu.Item>
{container.State === 'running' ? (
<Menu.Item icon={<IconPlayerStop color="red" />}>
<Text>Stop</Text>
</Menu.Item>
) : (
<Menu.Item icon={<IconPlayerPlay color="green" />}>
<Text>Start</Text>
</Menu.Item>
)}
{/* <Menu.Item icon={<IconDownload color="blue" />}>
<Text>Pull latest image </Text>
</Menu.Item>
<Menu.Item icon={<IconFileText color="grey" />}>
<Text>Logs</Text>
</Menu.Item> */}
<Menu.Label>Homarr</Menu.Label>
<Menu.Item icon={<IconCodePlus color={theme.primaryColor} />}>
<Text>Add to Homarr</Text>
</Menu.Item>
</Menu>
);
}

View File

@@ -0,0 +1,90 @@
import { Table, Checkbox, Group, Badge, createStyles } from '@mantine/core';
import Dockerode from 'dockerode';
import ContainerState from './ContainerState';
const useStyles = createStyles((theme) => ({
rowSelected: {
backgroundColor:
theme.colorScheme === 'dark'
? theme.fn.rgba(theme.colors[theme.primaryColor][7], 0.2)
: theme.colors[theme.primaryColor][0],
},
}));
export default function DockerTable({
containers,
selection,
setSelection,
}: {
setSelection: any;
containers: Dockerode.ContainerInfo[];
selection: Dockerode.ContainerInfo[];
}) {
const { classes, cx } = useStyles();
const toggleRow = (container: Dockerode.ContainerInfo) =>
setSelection((current: Dockerode.ContainerInfo[]) =>
current.includes(container) ? current.filter((c) => c !== container) : [...current, container]
);
const toggleAll = () =>
setSelection((current: any) =>
current.length === containers.length ? [] : containers.map((c) => c)
);
const rows = containers.map((element) => {
const selected = selection.includes(element);
return (
<tr key={element.Id} className={cx({ [classes.rowSelected]: selected })}>
<td>
<Checkbox
checked={selection.includes(element)}
onChange={() => toggleRow(element)}
transitionDuration={0}
/>
</td>
<td>{element.Names[0].replace('/', '')}</td>
<td>{element.Image}</td>
<td>
<Group>
{element.Ports.sort((a, b) => a.PrivatePort - b.PrivatePort)
.slice(-3)
.map((port) => (
<Badge key={port.PrivatePort} variant="outline">
{port.PrivatePort}:{port.PublicPort}
</Badge>
))}
{element.Ports.length > 3 && (
<Badge variant="filled">{element.Ports.length - 3} more</Badge>
)}
</Group>
</td>
<td>
<ContainerState state={element.State} />
</td>
</tr>
);
});
return (
<Table captionSide="bottom" highlightOnHover sx={{ minWidth: 800 }} verticalSpacing="sm">
<caption>your docker containers</caption>
<thead>
<tr>
<th style={{ width: 40 }}>
<Checkbox
onChange={toggleAll}
checked={selection.length === containers.length}
indeterminate={selection.length > 0 && selection.length !== containers.length}
transitionDuration={0}
/>
</th>
<th>Name</th>
<th>Image</th>
<th>Ports</th>
<th>State</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
);
}

View File

@@ -37,7 +37,7 @@ export default function TitleChanger() {
};
return (
<Group direction="column" grow>
<Group direction="column" grow mb="lg">
<form onSubmit={form.onSubmit((values) => saveChanges(values))}>
<Group grow direction="column">
<TextInput label="Page title" placeholder="Homarr 🦞" {...form.getInputProps('title')} />

View File

@@ -1,13 +1,12 @@
import { ActionIcon, Group, Text, SegmentedControl, TextInput, Anchor } from '@mantine/core';
import { Group, Text, SegmentedControl, TextInput } from '@mantine/core';
import { useState } from 'react';
import { IconBrandGithub as BrandGithub, IconBrandDiscord as BrandDiscord } from '@tabler/icons';
import { CURRENT_VERSION } from '../../../data/constants';
import { useConfig } from '../../tools/state';
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
import { WidgetsPositionSwitch } from '../WidgetsPositionSwitch/WidgetsPositionSwitch';
import ConfigChanger from '../Config/ConfigChanger';
import SaveConfigComponent from '../Config/SaveConfig';
import ModuleEnabler from './ModuleEnabler';
import Tip from '../layout/Tip';
export default function CommonSettings(args: any) {
const { config, setConfig } = useConfig();
@@ -25,20 +24,16 @@ export default function CommonSettings(args: any) {
);
return (
<Group direction="column" grow>
<Group direction="column" grow mb="lg">
<Group grow direction="column" spacing={0}>
<Text>Search engine</Text>
<Text
style={{
fontSize: '0.75rem',
color: 'gray',
marginBottom: '0.5rem',
}}
>
Tip: %s can be used as a placeholder for the query.
</Text>
<Tip>
Use the prefixes <b>!yt</b> and <b>!t</b> in front of your query to search on YouTube or
for a Torrent respectively.
</Tip>
<SegmentedControl
fullWidth
mb="sm"
title="Search engine"
value={
// Match config.settings.searchUrl with a key in the matches array
@@ -60,21 +55,24 @@ export default function CommonSettings(args: any) {
data={matches}
/>
{searchUrl === 'Custom' && (
<TextInput
label="Query URL"
placeholder="Custom query URL"
value={customSearchUrl}
onChange={(event) => {
setCustomSearchUrl(event.currentTarget.value);
setConfig({
...config,
settings: {
...config.settings,
searchUrl: event.currentTarget.value,
},
});
}}
/>
<>
<Tip>%s can be used as a placeholder for the query.</Tip>
<TextInput
label="Query URL"
placeholder="Custom query URL"
value={customSearchUrl}
onChange={(event) => {
setCustomSearchUrl(event.currentTarget.value);
setConfig({
...config,
settings: {
...config.settings,
searchUrl: event.currentTarget.value,
},
});
}}
/>
</>
)}
</Group>
<ColorSchemeSwitch />
@@ -82,52 +80,7 @@ export default function CommonSettings(args: any) {
<ModuleEnabler />
<ConfigChanger />
<SaveConfigComponent />
<Text
style={{
alignSelf: 'center',
fontSize: '0.75rem',
textAlign: 'center',
color: 'gray',
}}
>
Tip: You can upload your config file by dragging and dropping it onto the page!
</Text>
<Group position="center" direction="row" mr="xs">
<Group spacing={0}>
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg">
<BrandGithub size={18} />
</ActionIcon>
<Text
style={{
position: 'relative',
fontSize: '0.90rem',
color: 'gray',
}}
>
{CURRENT_VERSION}
</Text>
</Group>
<Group spacing={1}>
<Text
style={{
fontSize: '0.90rem',
textAlign: 'center',
color: 'gray',
}}
>
Made with by @
<Anchor
href="https://github.com/ajnart"
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
>
ajnart
</Anchor>
</Text>
<ActionIcon<'a'> component="a" href="https://discord.gg/aCsmEV5RgA" size="lg">
<BrandDiscord size={18} />
</ActionIcon>
</Group>
</Group>
<Tip>Upload your config file by dragging and dropping it onto the page!</Tip>
</Group>
);
}

View File

@@ -0,0 +1,44 @@
import { Group, ActionIcon, Anchor, Text } from '@mantine/core';
import { IconBrandDiscord, IconBrandGithub } from '@tabler/icons';
import { CURRENT_VERSION } from '../../../data/constants';
export default function Credits(props: any) {
return (
<Group position="center" direction="row" mr="xs">
<Group spacing={0}>
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg">
<IconBrandGithub size={18} />
</ActionIcon>
<Text
style={{
position: 'relative',
fontSize: '0.90rem',
color: 'gray',
}}
>
{CURRENT_VERSION}
</Text>
</Group>
<Group spacing={1}>
<Text
style={{
fontSize: '0.90rem',
textAlign: 'center',
color: 'gray',
}}
>
Made with by @
<Anchor
href="https://github.com/ajnart"
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
>
ajnart
</Anchor>
</Text>
<ActionIcon<'a'> component="a" href="https://discord.gg/aCsmEV5RgA" size="lg">
<IconBrandDiscord size={18} />
</ActionIcon>
</Group>
</Group>
);
}

View File

@@ -1,18 +1,23 @@
import { ActionIcon, Title, Tooltip, Drawer, Tabs } from '@mantine/core';
import { ActionIcon, Title, Tooltip, Drawer, Tabs, ScrollArea } from '@mantine/core';
import { useHotkeys } from '@mantine/hooks';
import { useState } from 'react';
import { IconSettings } from '@tabler/icons';
import AdvancedSettings from './AdvancedSettings';
import CommonSettings from './CommonSettings';
import Credits from './Credits';
function SettingsMenu(props: any) {
return (
<Tabs grow>
<Tabs.Tab data-autofocus label="Common">
<CommonSettings />
<ScrollArea style={{ height: '78vh' }} offsetScrollbars>
<CommonSettings />
</ScrollArea>
</Tabs.Tab>
<Tabs.Tab label="Customizations">
<AdvancedSettings />
<ScrollArea style={{ height: '78vh' }} offsetScrollbars>
<AdvancedSettings />
</ScrollArea>
</Tabs.Tab>
</Tabs>
);
@@ -26,13 +31,14 @@ export function SettingsMenuButton(props: any) {
<>
<Drawer
size="xl"
padding="xl"
padding="lg"
position="right"
title={<Title order={3}>Settings</Title>}
title={<Title order={5}>Settings</Title>}
opened={props.opened || opened}
onClose={() => setOpened(false)}
>
<SettingsMenu />
<Credits />
</Drawer>
<ActionIcon
variant="default"

View File

@@ -1,23 +1,29 @@
import React from 'react';
import {
createStyles,
Header as Head,
Group,
ActionIcon,
Box,
Burger,
createStyles,
Drawer,
Title,
Group,
Header as Head,
ScrollArea,
ActionIcon,
Title,
Transition,
} from '@mantine/core';
import { useBooleanToggle } from '@mantine/hooks';
import { Logo } from './Logo';
import SearchBar from '../modules/search/SearchModule';
import { AddItemShelfButton } from '../AppShelf/AddAppShelfItem';
import { SettingsMenuButton } from '../Settings/SettingsMenu';
import {
CalendarModule,
DateModule,
TotalDownloadsModule,
WeatherModule,
DashdotModule,
} from '../modules';
import { ModuleWrapper } from '../modules/moduleWrapper';
import { CalendarModule, TotalDownloadsModule, WeatherModule, DateModule } from '../modules';
import DockerDrawer from '../Docker/DockerDrawer';
import SearchBar from '../modules/search/SearchModule';
import { SettingsMenuButton } from '../Settings/SettingsMenu';
import { Logo } from './Logo';
const HEADER_HEIGHT = 60;
@@ -47,6 +53,7 @@ export function Header(props: any) {
</Box>
<Group noWrap>
<SearchBar />
<DockerDrawer />
<SettingsMenuButton />
<AddItemShelfButton />
<ActionIcon className={classes.burger} variant="default" radius="md" size="xl">
@@ -84,6 +91,7 @@ export function Header(props: any) {
<ModuleWrapper module={TotalDownloadsModule} />
<ModuleWrapper module={WeatherModule} />
<ModuleWrapper module={DateModule} />
<ModuleWrapper module={DashdotModule} />
</Group>
</ScrollArea>
</div>

View File

@@ -0,0 +1,19 @@
import { Text } from '@mantine/core';
interface TipProps {
children: React.ReactNode;
}
export default function Tip(props: TipProps) {
return (
<Text
style={{
fontSize: '0.75rem',
color: 'gray',
marginBottom: '0.5rem',
}}
>
Tip: {props.children}
</Text>
);
}

View File

@@ -1,6 +1,7 @@
import { Group } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import { CalendarModule, DateModule, TotalDownloadsModule, WeatherModule } from '../modules';
import { DashdotModule } from '../modules/dash.';
import { ModuleWrapper } from '../modules/moduleWrapper';
export default function Widgets(props: any) {
@@ -14,6 +15,7 @@ export default function Widgets(props: any) {
<ModuleWrapper module={TotalDownloadsModule} />
<ModuleWrapper module={WeatherModule} />
<ModuleWrapper module={DateModule} />
<ModuleWrapper module={DashdotModule} />
</Group>
)}
</>

View File

@@ -0,0 +1,233 @@
import { createStyles, useMantineColorScheme, useMantineTheme } from '@mantine/core';
import { IconCalendar as CalendarIcon } from '@tabler/icons';
import axios from 'axios';
import { useEffect, useState } from 'react';
import { useConfig } from '../../../tools/state';
import { serviceItem } from '../../../tools/types';
import { IModule } from '../modules';
const asModule = <T extends IModule>(t: T) => t;
export const DashdotModule = asModule({
title: 'Dash.',
description: 'A module for displaying the graphs of your running Dash. instance.',
icon: CalendarIcon,
component: DashdotComponent,
options: {
cpuMultiView: {
name: 'CPU Multi-Core View',
value: false,
},
storageMultiView: {
name: 'Storage Multi-Drive View',
value: false,
},
useCompactView: {
name: 'Use Compact View',
value: false,
},
graphs: {
name: 'Graphs',
value: ['CPU', 'RAM', 'Storage', 'Network'],
options: ['CPU', 'RAM', 'Storage', 'Network', 'GPU'],
},
},
});
const useStyles = createStyles((theme, _params) => ({
heading: {
marginTop: 0,
marginBottom: 10,
},
table: {
display: 'table',
},
tableRow: {
display: 'table-row',
},
tableLabel: {
display: 'table-cell',
paddingRight: 10,
},
tableValue: {
display: 'table-cell',
whiteSpace: 'pre-wrap',
paddingBottom: 5,
},
graphsContainer: {
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
rowGap: 10,
columnGap: 10,
},
iframe: {
flex: '1 0 auto',
maxWidth: '100%',
height: '140px',
borderRadius: theme.radius.lg,
},
}));
const bpsPrettyPrint = (bits?: number) =>
!bits
? '-'
: bits > 1000 * 1000 * 1000
? `${(bits / 1000 / 1000 / 1000).toFixed(1)} Gb/s`
: bits > 1000 * 1000
? `${(bits / 1000 / 1000).toFixed(1)} Mb/s`
: bits > 1000
? `${(bits / 1000).toFixed(1)} Kb/s`
: `${bits.toFixed(1)} b/s`;
const bytePrettyPrint = (byte: number): string =>
byte > 1024 * 1024 * 1024
? `${(byte / 1024 / 1024 / 1024).toFixed(1)} GiB`
: byte > 1024 * 1024
? `${(byte / 1024 / 1024).toFixed(1)} MiB`
: byte > 1024
? `${(byte / 1024).toFixed(1)} KiB`
: `${byte.toFixed(1)} B`;
const useJson = (service: serviceItem | undefined, url: string) => {
const [data, setData] = useState<any | undefined>();
const doRequest = async () => {
try {
const resp = await axios.get(url, { baseURL: service?.url });
setData(resp.data);
// eslint-disable-next-line no-empty
} catch (e) {}
};
useEffect(() => {
if (service?.url) {
doRequest();
}
}, [service?.url]);
return data;
};
export function DashdotComponent() {
const { config } = useConfig();
const theme = useMantineTheme();
const { classes } = useStyles();
const { colorScheme } = useMantineColorScheme();
const dashConfig = config.modules?.[DashdotModule.title]
.options as typeof DashdotModule['options'];
const isCompact = dashConfig?.useCompactView?.value ?? false;
const dashdotService = config.services.filter((service) => service.type === 'Dash.')[0];
const enabledGraphs = dashConfig?.graphs?.value ?? ['CPU', 'RAM', 'Storage', 'Network'];
const cpuEnabled = enabledGraphs.includes('CPU');
const storageEnabled = enabledGraphs.includes('Storage');
const ramEnabled = enabledGraphs.includes('RAM');
const networkEnabled = enabledGraphs.includes('Network');
const gpuEnabled = enabledGraphs.includes('GPU');
const info = useJson(dashdotService, '/info');
const storageLoad = useJson(dashdotService, '/load/storage');
const totalUsed =
(storageLoad?.layout as any[])?.reduce((acc, curr) => (curr.load ?? 0) + acc, 0) ?? 0;
const totalSize =
(info?.storage?.layout as any[])?.reduce((acc, curr) => (curr.size ?? 0) + acc, 0) ?? 0;
const graphs = [
{
name: 'CPU',
enabled: cpuEnabled,
params: {
multiView: dashConfig?.cpuMultiView?.value ?? false,
},
},
{
name: 'Storage',
enabled: storageEnabled && !isCompact,
params: {
multiView: dashConfig?.storageMultiView?.value ?? false,
},
},
{
name: 'RAM',
enabled: ramEnabled,
},
{
name: 'Network',
enabled: networkEnabled,
spanTwo: true,
},
{
name: 'GPU',
enabled: gpuEnabled,
spanTwo: true,
},
].filter((g) => g.enabled);
return (
<div>
<h2 className={classes.heading}>Dash.</h2>
{!dashdotService ? (
<p>No dash. service found. Please add one to your Homarr dashboard.</p>
) : !info ? (
<p>Cannot acquire information from dash. - are you running the latest version?</p>
) : (
<div className={classes.graphsContainer}>
<div className={classes.table}>
{storageEnabled && isCompact && (
<div className={classes.tableRow}>
<p className={classes.tableLabel}>Storage:</p>
<p className={classes.tableValue}>
{(totalUsed / (totalSize || 1)).toFixed(1)}%{'\n'}
{bytePrettyPrint(totalUsed)} / {bytePrettyPrint(totalSize)}
</p>
</div>
)}
{networkEnabled && (
<div className={classes.tableRow}>
<p className={classes.tableLabel}>Network:</p>
<p className={classes.tableValue}>
{bpsPrettyPrint(info?.network?.speedUp)} Up{'\n'}
{bpsPrettyPrint(info?.network?.speedDown)} Down
</p>
</div>
)}
</div>
{graphs.map((graph) => (
<iframe
className={classes.iframe}
style={
isCompact
? {
width: graph.spanTwo ? '100%' : 'calc(50% - 5px)',
}
: undefined
}
key={graph.name}
title={graph.name}
src={`${
dashdotService.url
}?singleGraphMode=true&graph=${graph.name.toLowerCase()}&theme=${colorScheme}&surface=${(colorScheme ===
'dark'
? theme.colors.dark[7]
: theme.colors.gray[0]
).substring(1)}${isCompact ? '&gap=10' : '&gap=5'}&innerRadius=${theme.radius.lg}${
graph.params
? `&${Object.entries(graph.params)
.map(([key, value]) => `${key}=${value.toString()}`)
.join('&')}`
: ''
}`}
frameBorder="0"
allowTransparency
/>
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1 @@
export { DashdotModule } from './DashdotModule';

View File

@@ -23,7 +23,7 @@ export default function DateComponent(props: any) {
const [date, setDate] = useState(new Date());
const setSafeInterval = useSetSafeInterval();
const { config } = useConfig();
const isFullTime = config?.modules?.[DateModule.title]?.options?.full?.value ?? false;
const isFullTime = config?.modules?.[DateModule.title]?.options?.full?.value ?? true;
const formatString = isFullTime ? 'HH:mm' : 'h:mm A';
// Change date on minute change
// Note: Using 10 000ms instead of 1000ms to chill a little :)

View File

@@ -1,6 +1,7 @@
export * from './date';
export * from './calendar';
export * from './search';
export * from './ping';
export * from './weather';
export * from './dash.';
export * from './date';
export * from './downloads';
export * from './ping';
export * from './search';
export * from './weather';

View File

@@ -1,10 +1,18 @@
import { Button, Card, Group, Menu, Switch, TextInput, useMantineColorScheme } from '@mantine/core';
import {
Button,
Card,
Group,
Menu,
MultiSelect,
Switch,
TextInput,
useMantineColorScheme,
} from '@mantine/core';
import { useConfig } from '../../tools/state';
import { IModule } from './modules';
function getItems(module: IModule) {
const { config, setConfig } = useConfig();
const enabledModules = config.modules ?? {};
const items: JSX.Element[] = [];
if (module.options) {
const keys = Object.keys(module.options);
@@ -15,6 +23,38 @@ function getItems(module: IModule) {
types.forEach((type, index) => {
const optionName = `${module.title}.${keys[index]}`;
const moduleInConfig = config.modules?.[module.title];
if (type === 'object') {
items.push(
<MultiSelect
label={module.options?.[keys[index]].name}
data={module.options?.[keys[index]].options ?? []}
defaultValue={
(moduleInConfig?.options?.[keys[index]]?.value as string[]) ??
(values[index].value as string[]) ??
[]
}
searchable
onChange={(value) => {
setConfig({
...config,
modules: {
...config.modules,
[module.title]: {
...moduleInConfig,
options: {
...moduleInConfig?.options,
[keys[index]]: {
...moduleInConfig?.options?.[keys[index]],
value,
},
},
},
},
});
}}
/>
);
}
if (type === 'string') {
items.push(
<form
@@ -44,7 +84,11 @@ function getItems(module: IModule) {
id={optionName}
name={optionName}
label={values[index].name}
defaultValue={(moduleInConfig?.options?.[keys[index]]?.value as string) ?? ''}
defaultValue={
(moduleInConfig?.options?.[keys[index]]?.value as string) ??
(values[index].value as string) ??
''
}
onChange={(e) => {}}
/>
@@ -59,7 +103,9 @@ function getItems(module: IModule) {
<Switch
defaultChecked={
// Set default checked to the value of the option if it exists
(moduleInConfig?.options?.[keys[index]]?.value as boolean) ?? false
(moduleInConfig?.options?.[keys[index]]?.value as boolean) ??
(values[index].value as boolean) ??
false
}
key={keys[index]}
onClick={(e) => {

View File

@@ -16,5 +16,6 @@ interface Option {
export interface OptionValues {
name: string;
value: boolean | string;
value: boolean | string | string[];
options?: string[];
}

View File

@@ -1,4 +1,4 @@
import { Kbd, createStyles, Text, Popover, Autocomplete } from '@mantine/core';
import { Kbd, createStyles, Text, Popover, Autocomplete, Tooltip } from '@mantine/core';
import { useDebouncedValue, useForm, useHotkeys } from '@mantine/hooks';
import { useEffect, useRef, useState } from 'react';
import {
@@ -107,40 +107,21 @@ export default function SearchBar(props: any) {
}, 20);
})}
>
<Popover
opened={opened}
position="bottom"
placement="start"
width={260}
withArrow
<Autocomplete
autoFocus
variant="filled"
data={autocompleteData}
icon={icon}
ref={textInput}
rightSectionWidth={90}
rightSection={rightSection}
radius="md"
trapFocus={false}
transition="pop-bottom-right"
onFocusCapture={() => setOpened(true)}
onBlurCapture={() => setOpened(false)}
target={
<Autocomplete
autoFocus
variant="filled"
data={autocompleteData}
icon={icon}
ref={textInput}
rightSectionWidth={90}
rightSection={rightSection}
radius="md"
size="md"
styles={{ rightSection: { pointerEvents: 'none' } }}
placeholder="Search the web..."
{...props}
{...form.getInputProps('query')}
/>
}
>
<Text>
Tip: Use the prefixes <b>!yt</b> and <b>!t</b> in front of your query to search on YouTube
or for a Torrent respectively.
</Text>
</Popover>
size="md"
styles={{ rightSection: { pointerEvents: 'none' } }}
placeholder="Search the web..."
{...props}
{...form.getInputProps('query')}
/>
</form>
);
}

View File

@@ -1,4 +1,4 @@
import { Group, Space, Title, Tooltip } from '@mantine/core';
import { Group, Space, Title, Tooltip, Skeleton } from '@mantine/core';
import axios from 'axios';
import { useEffect, useState } from 'react';
import {
@@ -29,7 +29,7 @@ export const WeatherModule: IModule = {
},
location: {
name: 'Current location',
value: '',
value: 'Paris',
},
},
};
@@ -135,7 +135,7 @@ export default function WeatherComponent(props: any) {
const { config } = useConfig();
const [weather, setWeather] = useState({} as WeatherResponse);
const cityInput: string =
(config?.modules?.[WeatherModule.title]?.options?.location?.value as string) ?? '';
(config?.modules?.[WeatherModule.title]?.options?.location?.value as string) ?? 'Paris';
const isFahrenheit: boolean =
(config?.modules?.[WeatherModule.title]?.options?.freedomunit?.value as boolean) ?? false;
@@ -157,7 +157,18 @@ export default function WeatherComponent(props: any) {
});
}, [cityInput]);
if (!weather.current_weather) {
return null;
return (
<>
<Skeleton height={40} width={100} mb="xl" />
<Group noWrap direction="row">
<Skeleton height={50} circle />
<Group>
<Skeleton height={25} width={70} mr="lg" />
<Skeleton height={25} width={70} />
</Group>
</Group>
</>
);
}
function usePerferedUnit(value: number): string {
return isFahrenheit ? `${(value * (9 / 5) + 32).toFixed(1)}°F` : `${value.toFixed(1)}°C`;

View File

@@ -1,7 +1,7 @@
import { NextFetchEvent, NextRequest, NextResponse } from 'next/server';
export function middleware(req: NextRequest, ev: NextFetchEvent) {
const ok = req.cookies.password === process.env.PASSWORD;
const ok = req.cookies.get('password') === process.env.PASSWORD;
const url = req.nextUrl.clone();
if (
!ok &&

View File

@@ -0,0 +1,66 @@
import { NextApiRequest, NextApiResponse } from 'next';
import Docker from 'dockerode';
const docker = new Docker();
async function Get(req: NextApiRequest, res: NextApiResponse) {
// Get the slug of the request
const { id } = req.query as { id: string };
const { action } = req.query;
// Get the action on the request (start, stop, restart)
if (action !== 'start' && action !== 'stop' && action !== 'restart' && action !== 'remove') {
return res.status(400).json({
statusCode: 400,
message: 'Invalid action',
});
}
if (!id) {
return res.status(400).json({
message: 'Missing ID',
});
}
// Get the container with the ID
const container = docker.getContainer(id);
// Get the container info
container.inspect((err, data) => {
if (err) {
res.status(500).json({
message: err,
});
}
});
try {
switch (action) {
case 'remove':
await container.remove();
break;
case 'start':
container.start();
break;
case 'stop':
container.stop();
break;
case 'restart':
container.restart();
break;
}
} catch (err) {
res.status(500).json({
message: err,
});
}
return res.status(200).json({
success: true,
});
}
export default async (req: NextApiRequest, res: NextApiResponse) => {
// Filter out if the reuqest is a Put or a GET
if (req.method === 'GET') {
return Get(req, res);
}
return res.status(405).json({
statusCode: 405,
message: 'Method not allowed',
});
};

View File

@@ -0,0 +1,21 @@
import { NextApiRequest, NextApiResponse } from 'next';
import Docker from 'dockerode';
const docker = new Docker();
async function Get(req: NextApiRequest, res: NextApiResponse) {
const containers = await docker.listContainers({ all: true });
return res.status(200).json(containers);
}
export default async (req: NextApiRequest, res: NextApiResponse) => {
// Filter out if the reuqest is a POST or a GET
if (req.method === 'GET') {
return Get(req, res);
}
return res.status(405).json({
statusCode: 405,
message: 'Method not allowed',
});
};

55
src/tools/addToHomarr.ts Normal file
View File

@@ -0,0 +1,55 @@
import Dockerode from 'dockerode';
import { Config, MatchingImages, ServiceType } from './types';
async function MatchIcon(name: string) {
const res = await fetch(
`https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name
.replace(/\s+/g, '-')
.toLowerCase()}.png`
);
return res.ok ? res.url : '/favicon.svg';
}
function tryMatchType(imageName: string): ServiceType {
// Try to find imageName inside MatchingImages
const match = MatchingImages.find(({ image }) => imageName.includes(image));
if (match) {
return match.type;
}
return 'Other';
}
export function tryMatchService(container: Dockerode.ContainerInfo | undefined) {
if (container === undefined) return {};
const name = container.Names[0].substring(1);
return {
name,
id: container.Id,
type: tryMatchType(container.Image),
url: `${container.Ports.at(0)?.IP}:${container.Ports.at(0)?.PublicPort}`,
icon: `https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name
.replace(/\s+/g, '-')
.toLowerCase()}.png`,
};
}
export default async function addToHomarr(
container: Dockerode.ContainerInfo,
config: Config,
setConfig: (newconfig: Config) => void
) {
setConfig({
...config,
services: [
...config.services,
{
name: container.Names[0].substring(1),
id: container.Id,
type: tryMatchType(container.Image),
url: `localhost:${container.Ports.at(0)?.PublicPort}`,
icon: await MatchIcon(container.Names[0].substring(1)),
},
],
});
}

View File

@@ -61,6 +61,7 @@ export const Targets = [
export const ServiceTypeList = [
'Other',
'Emby',
'Dash.',
'Deluge',
'Lidarr',
'Plex',
@@ -73,6 +74,7 @@ export const ServiceTypeList = [
export type ServiceType =
| 'Other'
| 'Emby'
| 'Dash.'
| 'Deluge'
| 'Lidarr'
| 'Plex'
@@ -82,6 +84,12 @@ export type ServiceType =
| 'qBittorrent'
| 'Transmission';
export const MatchingImages: { image: string; type: ServiceType }[] = [
{ image: 'lscr.io/linuxserver/radarr', type: 'Radarr' },
{ image: 'lscr.io/linuxserver/sonarr', type: 'Sonarr' },
{ image: 'lscr.io/linuxserver/qbittorrent', type: 'qBittorrent' },
];
export interface serviceItem {
id: string;
name: string;

380
yarn.lock
View File

@@ -2409,111 +2409,118 @@ __metadata:
languageName: node
linkType: hard
"@next/bundle-analyzer@npm:^12.1.4":
version: 12.1.6
resolution: "@next/bundle-analyzer@npm:12.1.6"
"@next/bundle-analyzer@npm:^12.2.0":
version: 12.2.0
resolution: "@next/bundle-analyzer@npm:12.2.0"
dependencies:
webpack-bundle-analyzer: 4.3.0
checksum: cf37be49d45d706aea95df489656341bec64783e567067d15036b25330d7a69204987b2c402277f201b9bf943de588323b120fd8096bb3d6846a054bbb2cdc7e
checksum: e08770ed2f7bfa4fb38c29d58d1e3ad198fa7e9a8c061ea5e15950dd10576bed0b5b8c19266e18503af1d211a0d8d450b5fed4926f6863135b38e585d6fd1980
languageName: node
linkType: hard
"@next/env@npm:12.1.6":
version: 12.1.6
resolution: "@next/env@npm:12.1.6"
checksum: e6a4f189f0d653d13dc7ad510f6c9d2cf690bfd9e07c554bd501b840f8dabc3da5adcab874b0bc01aab86c3647cff4fb84692e3c3b28125af26f0b05cd4c7fcf
"@next/env@npm:12.2.0":
version: 12.2.0
resolution: "@next/env@npm:12.2.0"
checksum: 5fb317bdb5eb2d5df12ff55e335368792dba21874c5ece3cabf8cd312cec911a1d54ecf368e69dc08640b0244669b8a98c86cd035c7874b17640602e67c1b9d9
languageName: node
linkType: hard
"@next/eslint-plugin-next@npm:^12.1.4":
version: 12.1.6
resolution: "@next/eslint-plugin-next@npm:12.1.6"
"@next/eslint-plugin-next@npm:^12.2.0":
version: 12.2.0
resolution: "@next/eslint-plugin-next@npm:12.2.0"
dependencies:
glob: 7.1.7
checksum: 33dcaf71f299d3c8a0744cad512369f92d7a355f3c0d57f2496e888e4242080c49226ec2c59ba2efac04b3a1df51c36019b853b4177df082ca4621a1713a2229
checksum: 2e33b9af79af680fd873d74e91bed397930a91802c1d7a293db757227ebc431d3d856de69477dc178dec8b531635ea69d79b188293024f1371afe6c348dbe647
languageName: node
linkType: hard
"@next/swc-android-arm-eabi@npm:12.1.6":
version: 12.1.6
resolution: "@next/swc-android-arm-eabi@npm:12.1.6"
"@next/swc-android-arm-eabi@npm:12.2.0":
version: 12.2.0
resolution: "@next/swc-android-arm-eabi@npm:12.2.0"
conditions: os=android & cpu=arm
languageName: node
linkType: hard
"@next/swc-android-arm64@npm:12.1.6":
version: 12.1.6
resolution: "@next/swc-android-arm64@npm:12.1.6"
"@next/swc-android-arm64@npm:12.2.0":
version: 12.2.0
resolution: "@next/swc-android-arm64@npm:12.2.0"
conditions: os=android & cpu=arm64
languageName: node
linkType: hard
"@next/swc-darwin-arm64@npm:12.1.6":
version: 12.1.6
resolution: "@next/swc-darwin-arm64@npm:12.1.6"
"@next/swc-darwin-arm64@npm:12.2.0":
version: 12.2.0
resolution: "@next/swc-darwin-arm64@npm:12.2.0"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@next/swc-darwin-x64@npm:12.1.6":
version: 12.1.6
resolution: "@next/swc-darwin-x64@npm:12.1.6"
"@next/swc-darwin-x64@npm:12.2.0":
version: 12.2.0
resolution: "@next/swc-darwin-x64@npm:12.2.0"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@next/swc-linux-arm-gnueabihf@npm:12.1.6":
version: 12.1.6
resolution: "@next/swc-linux-arm-gnueabihf@npm:12.1.6"
"@next/swc-freebsd-x64@npm:12.2.0":
version: 12.2.0
resolution: "@next/swc-freebsd-x64@npm:12.2.0"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
"@next/swc-linux-arm-gnueabihf@npm:12.2.0":
version: 12.2.0
resolution: "@next/swc-linux-arm-gnueabihf@npm:12.2.0"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
"@next/swc-linux-arm64-gnu@npm:12.1.6":
version: 12.1.6
resolution: "@next/swc-linux-arm64-gnu@npm:12.1.6"
"@next/swc-linux-arm64-gnu@npm:12.2.0":
version: 12.2.0
resolution: "@next/swc-linux-arm64-gnu@npm:12.2.0"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-arm64-musl@npm:12.1.6":
version: 12.1.6
resolution: "@next/swc-linux-arm64-musl@npm:12.1.6"
"@next/swc-linux-arm64-musl@npm:12.2.0":
version: 12.2.0
resolution: "@next/swc-linux-arm64-musl@npm:12.2.0"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@next/swc-linux-x64-gnu@npm:12.1.6":
version: 12.1.6
resolution: "@next/swc-linux-x64-gnu@npm:12.1.6"
"@next/swc-linux-x64-gnu@npm:12.2.0":
version: 12.2.0
resolution: "@next/swc-linux-x64-gnu@npm:12.2.0"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-x64-musl@npm:12.1.6":
version: 12.1.6
resolution: "@next/swc-linux-x64-musl@npm:12.1.6"
"@next/swc-linux-x64-musl@npm:12.2.0":
version: 12.2.0
resolution: "@next/swc-linux-x64-musl@npm:12.2.0"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@next/swc-win32-arm64-msvc@npm:12.1.6":
version: 12.1.6
resolution: "@next/swc-win32-arm64-msvc@npm:12.1.6"
"@next/swc-win32-arm64-msvc@npm:12.2.0":
version: 12.2.0
resolution: "@next/swc-win32-arm64-msvc@npm:12.2.0"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@next/swc-win32-ia32-msvc@npm:12.1.6":
version: 12.1.6
resolution: "@next/swc-win32-ia32-msvc@npm:12.1.6"
"@next/swc-win32-ia32-msvc@npm:12.2.0":
version: 12.2.0
resolution: "@next/swc-win32-ia32-msvc@npm:12.2.0"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@next/swc-win32-x64-msvc@npm:12.1.6":
version: 12.1.6
resolution: "@next/swc-win32-x64-msvc@npm:12.1.6"
"@next/swc-win32-x64-msvc@npm:12.2.0":
version: 12.2.0
resolution: "@next/swc-win32-x64-msvc@npm:12.2.0"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
@@ -3783,6 +3790,15 @@ __metadata:
languageName: node
linkType: hard
"@swc/helpers@npm:0.4.2":
version: 0.4.2
resolution: "@swc/helpers@npm:0.4.2"
dependencies:
tslib: ^2.4.0
checksum: 0b8c86ad03b17b8fe57dc4498e25dc294ea6bc42558a6b92d8fcd789351dac80199409bef38a2e3ac06aae0fedddfc0ab9c34409acbf74e55d1bbbd74f68b6b7
languageName: node
linkType: hard
"@szmarczak/http-timer@npm:^5.0.1":
version: 5.0.1
resolution: "@szmarczak/http-timer@npm:5.0.1"
@@ -3874,6 +3890,26 @@ __metadata:
languageName: node
linkType: hard
"@types/docker-modem@npm:*":
version: 3.0.2
resolution: "@types/docker-modem@npm:3.0.2"
dependencies:
"@types/node": "*"
"@types/ssh2": "*"
checksum: 1f23db30e6e9bdd4c6d6e43572fb7ac7251d106a1906a9f3faabac393897712a5a9cd5a471baedc0ac8055dab3f48eda331f41a1e2c7c6bbe3c7f433e039151c
languageName: node
linkType: hard
"@types/dockerode@npm:^3.3.9":
version: 3.3.9
resolution: "@types/dockerode@npm:3.3.9"
dependencies:
"@types/docker-modem": "*"
"@types/node": "*"
checksum: 3d03c68addb37c50e9557fff17171d26423aa18e544cb24e4caa81ebcec39ccc1cafed7adbfb8f4220d8ed23028d231717826bb77a786d425885c4f4cc37536d
languageName: node
linkType: hard
"@types/eslint-scope@npm:^3.7.3":
version: 3.7.3
resolution: "@types/eslint-scope@npm:3.7.3"
@@ -4151,6 +4187,25 @@ __metadata:
languageName: node
linkType: hard
"@types/ssh2-streams@npm:*":
version: 0.1.9
resolution: "@types/ssh2-streams@npm:0.1.9"
dependencies:
"@types/node": "*"
checksum: 190f3c235bf19787cd255f366d3ac9233875750095f3c73d15e72a1e67a826aed7e7c389603c5e89cb6420b87ff6dffc566f9174e546ddb7ff8c8dc2e8b00def
languageName: node
linkType: hard
"@types/ssh2@npm:*":
version: 0.5.52
resolution: "@types/ssh2@npm:0.5.52"
dependencies:
"@types/node": "*"
"@types/ssh2-streams": "*"
checksum: bc1c76ac727ad73ddd59ba849cf0ea3ed2e930439e7a363aff24f04f29b74f9b1976369b869dc9a018223c9fb8ad041c09a0f07aea8cf46a8c920049188cddae
languageName: node
linkType: hard
"@types/stack-utils@npm:^2.0.0":
version: 2.0.1
resolution: "@types/stack-utils@npm:2.0.1"
@@ -5196,6 +5251,15 @@ __metadata:
languageName: node
linkType: hard
"asn1@npm:^0.2.4":
version: 0.2.6
resolution: "asn1@npm:0.2.6"
dependencies:
safer-buffer: ~2.1.0
checksum: 39f2ae343b03c15ad4f238ba561e626602a3de8d94ae536c46a4a93e69578826305366dc09fbb9b56aec39b4982a463682f259c38e59f6fa380cd72cd61e493d
languageName: node
linkType: hard
"assert@npm:^1.1.1":
version: 1.5.0
resolution: "assert@npm:1.5.0"
@@ -5519,7 +5583,7 @@ __metadata:
languageName: node
linkType: hard
"base64-js@npm:^1.0.2":
"base64-js@npm:^1.0.2, base64-js@npm:^1.3.1":
version: 1.5.1
resolution: "base64-js@npm:1.5.1"
checksum: 669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005
@@ -5541,6 +5605,15 @@ __metadata:
languageName: node
linkType: hard
"bcrypt-pbkdf@npm:^1.0.2":
version: 1.0.2
resolution: "bcrypt-pbkdf@npm:1.0.2"
dependencies:
tweetnacl: ^0.14.3
checksum: 4edfc9fe7d07019609ccf797a2af28351736e9d012c8402a07120c4453a3b789a15f2ee1530dc49eee8f7eb9379331a8dd4b3766042b9e502f74a68e7f662291
languageName: node
linkType: hard
"better-opn@npm:^2.1.1":
version: 2.1.1
resolution: "better-opn@npm:2.1.1"
@@ -5587,6 +5660,17 @@ __metadata:
languageName: node
linkType: hard
"bl@npm:^4.0.3":
version: 4.1.0
resolution: "bl@npm:4.1.0"
dependencies:
buffer: ^5.5.0
inherits: ^2.0.4
readable-stream: ^3.4.0
checksum: 9e8521fa7e83aa9427c6f8ccdcba6e8167ef30cc9a22df26effcc5ab682ef91d2cbc23a239f945d099289e4bbcfae7a192e9c28c84c6202e710a0dfec3722662
languageName: node
linkType: hard
"bluebird@npm:^3.5.5":
version: 3.7.2
resolution: "bluebird@npm:3.7.2"
@@ -5842,6 +5926,23 @@ __metadata:
languageName: node
linkType: hard
"buffer@npm:^5.5.0":
version: 5.7.1
resolution: "buffer@npm:5.7.1"
dependencies:
base64-js: ^1.3.1
ieee754: ^1.1.13
checksum: e2cf8429e1c4c7b8cbd30834ac09bd61da46ce35f5c22a78e6c2f04497d6d25541b16881e30a019c6fd3154150650ccee27a308eff3e26229d788bbdeb08ab84
languageName: node
linkType: hard
"buildcheck@npm:0.0.3":
version: 0.0.3
resolution: "buildcheck@npm:0.0.3"
checksum: baf30605c56e80c2ca0502e40e18f2ebc7075bb4a861c941c0b36cd468b27957ed11a62248003ce99b9e5f91a7dfa859b30aad4fa50f0090c77a6f596ba20e6d
languageName: node
linkType: hard
"builtin-status-codes@npm:^3.0.0":
version: 3.0.0
resolution: "builtin-status-codes@npm:3.0.0"
@@ -6578,14 +6679,14 @@ __metadata:
languageName: node
linkType: hard
"cookies-next@npm:^2.0.4":
version: 2.0.4
resolution: "cookies-next@npm:2.0.4"
"cookies-next@npm:^2.1.1":
version: 2.1.1
resolution: "cookies-next@npm:2.1.1"
dependencies:
"@types/cookie": ^0.4.1
"@types/node": ^16.10.2
cookie: ^0.4.0
checksum: fc25b4215f2d7092d72f8591c9bc8b30f3ea866fca76e536e31825899c3f05eefb97cdb4152c565429cab38d20f2f937d08aea76a43d3cdd3ca36e24a347fe00
checksum: c5fc2c72cf2d46d6fa804e5690b5038bab3d5c7e741a8472079bfbd6920010802962f7512d999ea430ebcbfc7c89c38e16f423479e4df7cb0bb782cc1a7f9004
languageName: node
linkType: hard
@@ -6679,6 +6780,17 @@ __metadata:
languageName: node
linkType: hard
"cpu-features@npm:~0.0.4":
version: 0.0.4
resolution: "cpu-features@npm:0.0.4"
dependencies:
buildcheck: 0.0.3
nan: ^2.15.0
node-gyp: latest
checksum: a20d58e41e63182b34753dfe23bd1d967944ec13d84b70849b5d334fb4a558b7e71e7f955ed86c8e75dd65b5c5b882f1c494174d342cb6d8a062d77f79d39596
languageName: node
linkType: hard
"cpy@npm:^8.1.2":
version: 8.1.2
resolution: "cpy@npm:8.1.2"
@@ -7243,6 +7355,28 @@ __metadata:
languageName: node
linkType: hard
"docker-modem@npm:^3.0.0":
version: 3.0.5
resolution: "docker-modem@npm:3.0.5"
dependencies:
debug: ^4.1.1
readable-stream: ^3.5.0
split-ca: ^1.0.1
ssh2: ^1.4.0
checksum: 79027f8e719a77031790af628f9aa1d72607ec3769149de3a4b683930f2e4d113ae0e3a7345b32ff3b2289f886879f4fcf216afb17908178ba00f9661c4e0dd6
languageName: node
linkType: hard
"dockerode@npm:^3.3.2":
version: 3.3.2
resolution: "dockerode@npm:3.3.2"
dependencies:
docker-modem: ^3.0.0
tar-fs: ~2.0.1
checksum: 69b60547ed2e6156e6ec1df16fccea9150c935ee0b0517723b4d05a5d840a01d4cd945341390d24b7fa301383be64145d563d9319be56d487a5bcbf9f872ee59
languageName: node
linkType: hard
"doctrine@npm:^2.1.0":
version: 2.1.0
resolution: "doctrine@npm:2.1.0"
@@ -7466,7 +7600,7 @@ __metadata:
languageName: node
linkType: hard
"end-of-stream@npm:^1.0.0, end-of-stream@npm:^1.1.0":
"end-of-stream@npm:^1.0.0, end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1":
version: 1.4.4
resolution: "end-of-stream@npm:1.4.4"
dependencies:
@@ -8716,6 +8850,13 @@ __metadata:
languageName: node
linkType: hard
"fs-constants@npm:^1.0.0":
version: 1.0.0
resolution: "fs-constants@npm:1.0.0"
checksum: 18f5b718371816155849475ac36c7d0b24d39a11d91348cfcb308b4494824413e03572c403c86d3a260e049465518c4f0d5bd00f0371cdfcad6d4f30a85b350d
languageName: node
linkType: hard
"fs-extra@npm:^10.1.0":
version: 10.1.0
resolution: "fs-extra@npm:10.1.0"
@@ -9440,20 +9581,22 @@ __metadata:
"@mantine/next": ^4.2.8
"@mantine/notifications": ^4.2.8
"@mantine/prism": ^4.2.8
"@next/bundle-analyzer": ^12.1.4
"@next/eslint-plugin-next": ^12.1.4
"@next/bundle-analyzer": ^12.2.0
"@next/eslint-plugin-next": ^12.2.0
"@nivo/core": ^0.79.0
"@nivo/line": ^0.79.1
"@storybook/react": ^6.5.4
"@tabler/icons": ^1.68.0
"@types/dockerode": ^3.3.9
"@types/node": ^17.0.23
"@types/react": 17.0.43
"@types/uuid": ^8.3.4
"@typescript-eslint/eslint-plugin": ^5.16.0
"@typescript-eslint/parser": ^5.16.0
axios: ^0.27.2
cookies-next: ^2.0.4
cookies-next: ^2.1.1
dayjs: ^1.11.3
dockerode: ^3.3.2
eslint: ^8.11.0
eslint-config-airbnb: ^19.0.4
eslint-config-airbnb-typescript: ^16.1.0
@@ -9469,7 +9612,7 @@ __metadata:
framer-motion: ^6.3.1
jest: ^28.1.0
js-file-download: ^0.4.12
next: 12.1.6
next: ^12.2.0
prettier: ^2.6.2
prism-react-renderer: ^1.3.1
react: ^17.0.1
@@ -9704,7 +9847,7 @@ __metadata:
languageName: node
linkType: hard
"ieee754@npm:^1.1.4":
"ieee754@npm:^1.1.13, ieee754@npm:^1.1.4":
version: 1.2.1
resolution: "ieee754@npm:1.2.1"
checksum: 5144c0c9815e54ada181d80a0b810221a253562422e7c6c3a60b1901154184f49326ec239d618c416c1c5945a2e197107aee8d986a3dd836b53dffefd99b5e7e
@@ -11841,6 +11984,13 @@ __metadata:
languageName: node
linkType: hard
"mkdirp-classic@npm:^0.5.2":
version: 0.5.3
resolution: "mkdirp-classic@npm:0.5.3"
checksum: 3f4e088208270bbcc148d53b73e9a5bd9eef05ad2cbf3b3d0ff8795278d50dd1d11a8ef1875ff5aea3fa888931f95bfcb2ad5b7c1061cfefd6284d199e6776ac
languageName: node
linkType: hard
"mkdirp@npm:^0.5.1, mkdirp@npm:^0.5.3":
version: 0.5.6
resolution: "mkdirp@npm:0.5.6"
@@ -11920,7 +12070,7 @@ __metadata:
languageName: node
linkType: hard
"nan@npm:^2.12.1":
"nan@npm:^2.12.1, nan@npm:^2.15.0, nan@npm:^2.16.0":
version: 2.16.0
resolution: "nan@npm:2.16.0"
dependencies:
@@ -11985,26 +12135,29 @@ __metadata:
languageName: node
linkType: hard
"next@npm:12.1.6":
version: 12.1.6
resolution: "next@npm:12.1.6"
"next@npm:^12.2.0":
version: 12.2.0
resolution: "next@npm:12.2.0"
dependencies:
"@next/env": 12.1.6
"@next/swc-android-arm-eabi": 12.1.6
"@next/swc-android-arm64": 12.1.6
"@next/swc-darwin-arm64": 12.1.6
"@next/swc-darwin-x64": 12.1.6
"@next/swc-linux-arm-gnueabihf": 12.1.6
"@next/swc-linux-arm64-gnu": 12.1.6
"@next/swc-linux-arm64-musl": 12.1.6
"@next/swc-linux-x64-gnu": 12.1.6
"@next/swc-linux-x64-musl": 12.1.6
"@next/swc-win32-arm64-msvc": 12.1.6
"@next/swc-win32-ia32-msvc": 12.1.6
"@next/swc-win32-x64-msvc": 12.1.6
"@next/env": 12.2.0
"@next/swc-android-arm-eabi": 12.2.0
"@next/swc-android-arm64": 12.2.0
"@next/swc-darwin-arm64": 12.2.0
"@next/swc-darwin-x64": 12.2.0
"@next/swc-freebsd-x64": 12.2.0
"@next/swc-linux-arm-gnueabihf": 12.2.0
"@next/swc-linux-arm64-gnu": 12.2.0
"@next/swc-linux-arm64-musl": 12.2.0
"@next/swc-linux-x64-gnu": 12.2.0
"@next/swc-linux-x64-musl": 12.2.0
"@next/swc-win32-arm64-msvc": 12.2.0
"@next/swc-win32-ia32-msvc": 12.2.0
"@next/swc-win32-x64-msvc": 12.2.0
"@swc/helpers": 0.4.2
caniuse-lite: ^1.0.30001332
postcss: 8.4.5
styled-jsx: 5.0.2
use-sync-external-store: 1.1.0
peerDependencies:
fibers: ">= 3.1.0"
node-sass: ^6.0.0 || ^7.0.0
@@ -12020,6 +12173,8 @@ __metadata:
optional: true
"@next/swc-darwin-x64":
optional: true
"@next/swc-freebsd-x64":
optional: true
"@next/swc-linux-arm-gnueabihf":
optional: true
"@next/swc-linux-arm64-gnu":
@@ -12045,7 +12200,7 @@ __metadata:
optional: true
bin:
next: dist/bin/next
checksum: 670d544fd47670c29681d10824e6da625e9d4a048e564c8d9cb80d37f33c9ff9b5ca0a53e6d84d8d618b1fe7c9bb4e6b45040cb7e57a5c46b232a8f914425dc1
checksum: 38456c33935122ac1581367e4982034be23269039a8470a66443d710334336f8f3fb587f25d172d138d84cf18c01d3a76627fb610c2e2e57bd1692277c23fa2b
languageName: node
linkType: hard
@@ -13695,7 +13850,7 @@ __metadata:
languageName: node
linkType: hard
"readable-stream@npm:^3.6.0":
"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0":
version: 3.6.0
resolution: "readable-stream@npm:3.6.0"
dependencies:
@@ -14178,7 +14333,7 @@ __metadata:
languageName: node
linkType: hard
"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.1.0":
"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.1.0, safer-buffer@npm:~2.1.0":
version: 2.1.2
resolution: "safer-buffer@npm:2.1.2"
checksum: cab8f25ae6f1434abee8d80023d7e72b598cf1327164ddab31003c51215526801e40b66c5e65d658a0af1e9d6478cadcb4c745f4bd6751f97d8644786c0978b0
@@ -14659,6 +14814,13 @@ __metadata:
languageName: node
linkType: hard
"split-ca@npm:^1.0.1":
version: 1.0.1
resolution: "split-ca@npm:1.0.1"
checksum: 1e7409938a95ee843fe2593156a5735e6ee63772748ee448ea8477a5a3e3abde193c3325b3696e56a5aff07c7dcf6b1f6a2f2a036895b4f3afe96abb366d893f
languageName: node
linkType: hard
"split-string@npm:^3.0.1, split-string@npm:^3.0.2":
version: 3.1.0
resolution: "split-string@npm:3.1.0"
@@ -14675,6 +14837,23 @@ __metadata:
languageName: node
linkType: hard
"ssh2@npm:^1.4.0":
version: 1.11.0
resolution: "ssh2@npm:1.11.0"
dependencies:
asn1: ^0.2.4
bcrypt-pbkdf: ^1.0.2
cpu-features: ~0.0.4
nan: ^2.16.0
dependenciesMeta:
cpu-features:
optional: true
nan:
optional: true
checksum: e40cb9f171741a807c170dc555078aa8c49dc93dd36fc9c8be026fce1cfd31f0d37078d9b60a0f2cfb11d0e007ed5407376b72f8a0ef9f2cb89574632bbfb824
languageName: node
linkType: hard
"ssri@npm:^6.0.1":
version: 6.0.2
resolution: "ssri@npm:6.0.2"
@@ -15125,6 +15304,31 @@ __metadata:
languageName: node
linkType: hard
"tar-fs@npm:~2.0.1":
version: 2.0.1
resolution: "tar-fs@npm:2.0.1"
dependencies:
chownr: ^1.1.1
mkdirp-classic: ^0.5.2
pump: ^3.0.0
tar-stream: ^2.0.0
checksum: 26cd297ed2421bc8038ce1a4ca442296b53739f409847d495d46086e5713d8db27f2c03ba2f461d0f5ddbc790045628188a8544f8ae32cbb6238b279b68d0247
languageName: node
linkType: hard
"tar-stream@npm:^2.0.0":
version: 2.2.0
resolution: "tar-stream@npm:2.2.0"
dependencies:
bl: ^4.0.3
end-of-stream: ^1.4.1
fs-constants: ^1.0.0
inherits: ^2.0.3
readable-stream: ^3.1.1
checksum: 699831a8b97666ef50021c767f84924cfee21c142c2eb0e79c63254e140e6408d6d55a065a2992548e72b06de39237ef2b802b99e3ece93ca3904a37622a66f3
languageName: node
linkType: hard
"tar@npm:^6.0.2, tar@npm:^6.1.11, tar@npm:^6.1.2":
version: 6.1.11
resolution: "tar@npm:6.1.11"
@@ -15479,7 +15683,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0":
"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0":
version: 2.4.0
resolution: "tslib@npm:2.4.0"
checksum: 8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113
@@ -15504,6 +15708,13 @@ __metadata:
languageName: node
linkType: hard
"tweetnacl@npm:^0.14.3":
version: 0.14.5
resolution: "tweetnacl@npm:0.14.5"
checksum: 6061daba1724f59473d99a7bb82e13f211cdf6e31315510ae9656fefd4779851cb927adad90f3b488c8ed77c106adc0421ea8055f6f976ff21b27c5c4e918487
languageName: node
linkType: hard
"type-check@npm:^0.4.0, type-check@npm:~0.4.0":
version: 0.4.0
resolution: "type-check@npm:0.4.0"
@@ -15908,6 +16119,15 @@ __metadata:
languageName: node
linkType: hard
"use-sync-external-store@npm:1.1.0":
version: 1.1.0
resolution: "use-sync-external-store@npm:1.1.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 8993a0b642f91d7fcdbb02b7b3ac984bd3af4769686f38291fe7fcfe73dfb73d6c64d20dfb7e5e7fbf5a6da8f5392d6f8e5b00c243a04975595946e82c02b883
languageName: node
linkType: hard
"use@npm:^3.1.0":
version: 3.1.1
resolution: "use@npm:3.1.1"