Compare commits

...

91 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
Thomas Camlong
aab1492934 🔖 v0.7.2 2022-06-25 17:53:20 +02:00
Thomas Camlong
1ae074db8f 🔥 Remove .docusaurus/ 2022-06-25 17:51:44 +02:00
Thomas Camlong
f21004e944 🔖 v0.7.2
Tag version v0.7.2
2022-06-25 17:47:39 +02:00
Thomas Camlong
7c421cc52f 🔀 Merge pull request #260 from walkxcode/dev
💄 Changes AppShelf category styling
2022-06-25 16:11:38 +02:00
Thomas Camlong
d8e407ab22 🔀 Merge pull request #257 from ajnart/fix-multiple-torrent-client
🐛 Fix itteration on the different types of services
2022-06-25 15:36:59 +02:00
Thomas Camlong
37565284e6 🔀 Merge pull request #271 from ajnart/searchBar
 Adds query placeholder and autoFocus (#267 #268)
2022-06-25 15:36:06 +02:00
Bjorn Lammers
b758df9f44 🔥 Fix indentation because I'm a perfectionist 2022-06-25 15:14:39 +02:00
Bjorn Lammers
a735ae47c5 🙈 Updates .dockerignore 2022-06-25 15:13:00 +02:00
WalkxCode
97d585dc17 Adds query placeholder and autoFocus (#267 #268) 2022-06-25 14:02:53 +02:00
Thomas Camlong
7f3db9add1 🐛 Fix adding a service doesn't fetch 2022-06-24 13:44:43 +02:00
Thomas Camlong
6d6964f086 🔀 Merge pull request #258 from ajnart/ajnart/issue256
🐛 Allow anything in the input for the form.
2022-06-24 13:39:18 +02:00
Thomas Camlong
2a4012f73a 🔀 Merge pull request #263 from ajnart/#261-discord
💬 Adds Discord Button (#261)
2022-06-24 12:44:23 +02:00
Bjorn Lammers
9385315f03 🔀 Merge pull request #265 from jelliuk/patch-1 2022-06-24 12:06:27 +02:00
James
ee824f0b27 Update README.md
Correct the link to Wiki/Integrations
2022-06-24 10:36:43 +01:00
Bjorn Lammers
792af504c7 💬 Adds Discord Button
#261
2022-06-22 13:19:44 +02:00
Bjorn Lammers
cd3c062a24 💄 Changes AppShelf category styling 2022-06-21 19:14:18 +00:00
ajnart
a5f477c19b 🚑 Hotfix to spread torrent pushing 2022-06-21 21:04:21 +02:00
ajnart
85164d79fc 🚑 Hotfix password and usernames 2022-06-21 20:35:40 +02:00
ajnart
7aedc4111f 🚑 Hotfix how the result from the services are awaited 2022-06-21 19:59:25 +02:00
ajnart
d1f89847f5 💄 Small UI fix for mobile 2022-06-21 19:38:32 +02:00
ajnart
57170847a1 🐛 Allow anything in the input for the form.
If it works, it works.
Fixes #256
2022-06-21 19:22:14 +02:00
ajnart
45de715390 🐛 Fix itteration on the different types of services 2022-06-21 19:16:29 +02:00
Thomas Camlong
c29d6f58dd 🔀 Merge pull request #252 from LarveyOfficial/fix-multiple-download-clients
🐛Allow multiple of the same torrent client +1
2022-06-21 16:22:18 +02:00
ajnart
f0bae49830 🚨 Lint and prettier fix 2022-06-21 16:21:40 +02:00
Larvey
c3ceae4dc6 Also fixed Torrent form fields 2022-06-20 17:26:13 -04:00
Larvey
d654fb39e5 🐛Allow multiple of the same torrent client
Allows multiple of the same type of torrent client
2022-06-20 17:10:54 -04:00
Thomas Camlong
7dc205fa66 🚀 v0.7.1 ! Bug fixes and QOL improvements
### Features
*  Changing deluge/qbittorent to use href instead of origin by @VinnyVynce in https://github.com/ajnart/homarr/pull/178
*  Transmission Integration by @ajnart in https://github.com/ajnart/homarr/pull/181
*  Add different URL for API calls by @ajnart in https://github.com/ajnart/homarr/pull/180
* Password / Login Page by @ajnart in https://github.com/ajnart/homarr/pull/179
*  Ability to toggle categories by @ajnart in https://github.com/ajnart/homarr/pull/177
*  Add settings to change title and icons by @Aimsucks in https://github.com/ajnart/homarr/pull/184
*  Color, shade, app opacity, and background customizations by @Aimsucks in https://github.com/ajnart/homarr/pull/188
*  Calendar indication about date and w-e with secondary color by @Darkham42 in https://github.com/ajnart/homarr/pull/193
*  More Information in Torrents Module by @LarveyOfficial in https://github.com/ajnart/homarr/pull/195
*  Could position widgets at left by @Darkham42 in https://github.com/ajnart/homarr/pull/197
*  Configure calendar widget to show Sunday first by @ajnart in https://github.com/ajnart/homarr/pull/224
* Service Addition Overhaul by @LarveyOfficial in https://github.com/ajnart/homarr/pull/229
*  Slidable service span in customizations  by @Aimsucks in https://github.com/ajnart/homarr/pull/222
### Tweaks
🔧 Make credentials non-required for torrents by @ajnart in https://github.com/ajnart/homarr/pull/223
### Bug fixes
* 🐛Fix completed torrents progress color by @LarveyOfficial in https://github.com/ajnart/homarr/pull/227
* 🐛Tiles could be moved accidentally on mobiles by @ajnart in https://github.com/ajnart/homarr/pull/226
* 🐛 Cannot open "New Tab URL" on mobile by @ajnart in https://github.com/ajnart/homarr/pull/225
* 🐛 Fix Calendar Item Duplication by @LarveyOfficial in https://github.com/ajnart/homarr/pull/249
* 🐛Fix URL for Radarr and other services by @LarveyOfficial in https://github.com/ajnart/homarr/pull/250
* 🐛 Fix Calendar not loading content when a service fails by @LarveyOfficial in https://github.com/ajnart/homarr/pull/230
* 🐛 Calendar current day for light theme by @Darkham42 in https://github.com/ajnart/homarr/pull/194
* 🐛 Fix for timezone issues by @LarveyOfficial in https://github.com/ajnart/homarr/pull/186
* 🐛 Fix Sonarr Incorrect Dates by @LarveyOfficial in https://github.com/ajnart/homarr/pull/189
* 🐛 Fix for origin URL not containing path in API request URL by @Aimsucks in https://github.com/ajnart/homarr/pull/221
2022-06-20 22:09:25 +02:00
ajnart
91a249d953 🔖 tag v0.7.1 2022-06-20 22:03:43 +02:00
Thomas Camlong
356afda9c7 Merge pull request #250 from LarveyOfficial/patch-2
🐛Fix URL for Radarr and other services
2022-06-20 20:14:17 +02:00
Larvey
35f02a2296 Fix URL for Radarr and other services 2022-06-20 14:02:33 -04:00
Thomas Camlong
16bcec0deb Merge pull request #249 from LarveyOfficial/patch-1
🐛 Fix Calendar Item Duplication
2022-06-20 19:45:47 +02:00
Larvey
16ec57081b Fix Calendar Item Duplication
- 6fd23cf6a0 changed how items for the calendar are acquired, making it so every series gets updated when a new one gets added, or one gets moved. This causes the entries in the calendar to duplicate due to old code being left in.
2022-06-20 13:32:31 -04:00
ajnart
690f09fcf3 🚑 Hotfix ServiceItems 2022-06-20 10:40:30 +02:00
Thomas Camlong
2f960169bb 🔀 Merge pull request #222 from Aimsucks/app-card-with-slider
 Slidable service span in customizations
2022-06-20 10:29:39 +02:00
ajnart
14a40d9f66 🔧 Tweak values and UI changes 2022-06-20 10:24:22 +02:00
Thomas Camlong
e5abd67f83 🔀 Merge pull request #221 from Aimsucks/issue-215-fix
🐛 Fix for origin URL not containing path in API request URL
2022-06-20 10:22:13 +02:00
Thomas Camlong
399ba7e2fc 🔀 Merge pull request #229 from LarveyOfficial/rework-AddAppShelfItem
Service Addition Overhaul
2022-06-20 10:20:01 +02:00
ajnart
7780ae3d7a ♻️ Re-implement changes 2022-06-20 10:17:49 +02:00
ajnart
80d3f16473 🔧 Tweak UI and change the name of the settings 2022-06-20 10:06:30 +02:00
Larvey
a8c0dfcd0c Fix Capitalization 2022-06-20 10:06:15 +02:00
Larvey
6ee7d6ec8d declutter config file 2022-06-20 10:06:15 +02:00
Larvey
544fae3808 Added Scrollbar 2022-06-20 10:05:55 +02:00
Larvey
4516dde1f4 Reworked AddAppShelfItem
Adds:
- Advanced Options tab
- Changed which ping status codes identify as "Online"
- Change if service opens in new tab or not

Fixes:
- Deluge and Transmission Password requirement
2022-06-20 10:01:36 +02:00
Thomas Camlong
a20c5f8d12 🔀 Merge pull request #223 from ajnart/transmission-login
🔧 Make credentials non-required for torrents
2022-06-20 09:08:10 +02:00
Thomas Camlong
60e5c0d165 🔀 Merge pull request #230 from LarveyOfficial/calendar-multi-content-fix
🐛 Fix Calendar not loading content when a service fails
2022-06-20 09:03:14 +02:00
Larvey
b7bf18250d Fix CalendarModule.tsx
Fix no content showing when 1 of the same service is down.
2022-06-16 21:02:30 -04:00
Thomas Camlong
93256b7a6a 🔀 Merge pull request #224 from ajnart/ajnart/issue217
 Configure calendar widget to show Sunday first #217
2022-06-15 06:52:50 +02:00
Thomas Camlong
47a4437a01 🔀 Merge pull request #225 from ajnart/ajnart/issue202
🐛 Fix cannot open "New Tab URL" on mobile
2022-06-15 06:52:16 +02:00
Thomas Camlong
92470c619e 🔀 Merge pull request #226 from ajnart/ajnart/issue214
🐛Tiles could be moved accidentally on mobiles
2022-06-15 06:51:18 +02:00
Thomas Camlong
7cb3dfbd16 🔀 Merge pull request #227 from LarveyOfficial/fix-progress-color
🐛Fix completed torrents progress color
2022-06-15 06:51:06 +02:00
ajnart
d69e4f41a1 🐛 Fix required username 2022-06-15 06:47:53 +02:00
Larvey
4980254e89 Update DownloadsModule.tsx 2022-06-14 21:14:26 -04:00
ajnart
5133286e04 🐛Tiles could be moved accidentally on mobiles
Fixes #214
2022-06-14 22:45:31 +02:00
ajnart
ca2713a12c 🐛 Cannot open "New Tab URL" on mobile
Fixes #202
2022-06-14 22:11:55 +02:00
ajnart
4981823c37 🔧 Tweak values on the slider 2022-06-14 22:09:30 +02:00
ajnart
5d31e414f0 Configure calendar widget to show Sunday first
Fixes #217
2022-06-14 20:50:11 +02:00
ajnart
8ec2b9d0cd 🔧 Make credentials non-required for torrents
Fixes #201
2022-06-14 20:33:26 +02:00
Aimsucks
bd920dfc86 App card width slider
Added a slider to determine the XL app card width
2022-06-14 17:45:22 +00:00
Aimsucks
b5540a9958 🐛 Origin URL in calendar to includes path
Changed the origin variable in the calendar module to use the entire URL instead of just the origin domain
2022-06-14 17:01:30 +00:00
Thomas Camlong
778988de58 🚀 v0.7.0 : Theming, Password protection, Autocompletion, Transmission, Mobile responsiveness! This is a big upgrade 👀 2022-06-13 10:15:39 +02:00
44 changed files with 1830 additions and 468 deletions

View File

@@ -2,5 +2,8 @@ Dockerfile
.dockerignore .dockerignore
node_modules node_modules
npm-debug.log npm-debug.log
README.md *.md
.git .git
.github
LICENSE
docs/

View File

@@ -2,7 +2,7 @@ FROM node:16-alpine
WORKDIR /app WORKDIR /app
ENV NODE_ENV production ENV NODE_ENV production
COPY /next.config.js ./ COPY /next.config.js ./
COPY /public ./public COPY /public ./public
COPY /package.json ./package.json COPY /package.json ./package.json
# Automatically leverage output traces to reduce image size. https://nextjs.org/docs/advanced-features/output-file-tracing # Automatically leverage output traces to reduce image size. https://nextjs.org/docs/advanced-features/output-file-tracing
COPY /.next/standalone ./ COPY /.next/standalone ./

View File

@@ -33,7 +33,7 @@ Homarr is a simple and lightweight homepage for your server, that helps you easi
It integrates with the services you use to display information on the homepage (E.g. Show upcoming Sonarr/Radarr releases). It integrates with the services you use to display information on the homepage (E.g. Show upcoming Sonarr/Radarr releases).
For a full list of integrations look at: [wiki/integrations](#). For a full list of integrations look at: [wiki/integrations](https://github.com/ajnart/homarr/wiki/Integrations)
If you have any questions about Homarr or want to share information with us, please go to one of the following places: If you have any questions about Homarr or want to share information with us, please go to one of the following places:
@@ -198,7 +198,4 @@ SOFTWARE.
<i>Thank you for visiting! <b>For more information <a href="https://github.com/ajnart/homarr/wiki">read the wiki!</a></b></i> <i>Thank you for visiting! <b>For more information <a href="https://github.com/ajnart/homarr/wiki">read the wiki!</a></b></i>
<br/> <br/>
<br/> <br/>
<a href="https://trackgit.com">
<img src="https://us-central1-trackgit-analytics.cloudfunctions.net/token/ping/l3khzc3a3pexzw5w5whl" alt="trackgit-views" />
</a>
</p> </p>

View File

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

View File

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

View File

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

View File

@@ -1,25 +1,30 @@
import { import {
Modal, ActionIcon,
Anchor,
Button,
Center, Center,
Group, Group,
TextInput,
Image, Image,
Button,
Select,
LoadingOverlay, LoadingOverlay,
ActionIcon, Modal,
Tooltip, MultiSelect,
Title, ScrollArea,
Anchor, Select,
Switch,
Tabs,
Text, Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { useForm } from '@mantine/form'; 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 { 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 { useConfig } from '../../tools/state';
import { ServiceTypeList } from '../../tools/types'; import { ServiceTypeList, StatusCodes } from '../../tools/types';
import Tip from '../layout/Tip';
export function AddItemShelfButton(props: any) { export function AddItemShelfButton(props: any) {
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
@@ -54,7 +59,8 @@ function MatchIcon(name: string, form: any) {
fetch( fetch(
`https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name `https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name
.replace(/\s+/g, '-') .replace(/\s+/g, '-')
.toLowerCase()}.png` .toLowerCase()
.replace(/^dash\.$/, 'dashdot')}.png`
).then((res) => { ).then((res) => {
if (res.ok) { if (res.ok) {
form.setFieldValue('icon', res.url); form.setFieldValue('icon', res.url);
@@ -77,9 +83,10 @@ function MatchPort(name: string, form: any) {
{ name: 'sonarr', value: '8989' }, { name: 'sonarr', value: '8989' },
{ name: 'radarr', value: '7878' }, { name: 'radarr', value: '7878' },
{ name: 'lidarr', value: '8686' }, { name: 'lidarr', value: '8686' },
{ name: 'readarr', value: '8686' }, { name: 'readarr', value: '8787' },
{ name: 'deluge', value: '8112' }, { name: 'deluge', value: '8112' },
{ name: 'transmission', value: '9091' }, { name: 'transmission', value: '9091' },
{ name: 'dash.', value: '3001' },
]; ];
// Match name with portmap key // Match name with portmap key
const port = portmap.find((p) => p.name === name.toLowerCase()); const port = portmap.find((p) => p.name === name.toLowerCase());
@@ -88,6 +95,8 @@ function MatchPort(name: string, form: any) {
} }
} }
const DEFAULT_ICON = '/favicon.svg';
export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & any) { export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & any) {
const { setOpened } = props; const { setOpened } = props;
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
@@ -107,23 +116,21 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
type: props.type ?? 'Other', type: props.type ?? 'Other',
category: props.category ?? undefined, category: props.category ?? undefined,
name: props.name ?? '', name: props.name ?? '',
icon: props.icon ?? '/favicon.svg', icon: props.icon ?? DEFAULT_ICON,
url: props.url ?? '', url: props.url ?? '',
apiKey: props.apiKey ?? (undefined as unknown as string), apiKey: props.apiKey ?? (undefined as unknown as string),
username: props.username ?? (undefined as unknown as string), username: props.username ?? (undefined as unknown as string),
password: props.password ?? (undefined as unknown as string), password: props.password ?? (undefined as unknown as string),
openedUrl: props.openedUrl ?? (undefined as unknown as string), openedUrl: props.openedUrl ?? (undefined as unknown as string),
status: props.status ?? ['200'],
newTab: props.newTab ?? true,
}, },
validate: { validate: {
apiKey: () => null, apiKey: () => null,
// Validate icon with a regex // Validate icon with a regex
icon: (value: string) => { icon: (value: string) =>
// Regex to match everything that ends with and icon extension // Disable matching to allow any values
if (!value.match(/\.(png|jpg|jpeg|gif|svg)$/)) { null,
return 'Please enter a valid icon URL';
}
return null;
},
// Validate url with a regex http/https // Validate url with a regex http/https
url: (value: string) => { url: (value: string) => {
try { try {
@@ -133,12 +140,18 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
} }
return null; return null;
}, },
status: (value: string[]) => {
if (!value.length) {
return 'Please select a status code';
}
return null;
},
}, },
}); });
const [debounced, cancel] = useDebouncedValue(form.values.name, 250); const [debounced, cancel] = useDebouncedValue(form.values.name, 250);
useEffect(() => { 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); MatchIcon(form.values.name, form);
MatchService(form.values.name, form); MatchService(form.values.name, form);
MatchPort(form.values.name, form); MatchPort(form.values.name, form);
@@ -167,6 +180,12 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
</Center> </Center>
<form <form
onSubmit={form.onSubmit(() => { onSubmit={form.onSubmit(() => {
if (JSON.stringify(form.values.status) === JSON.stringify(['200'])) {
form.values.status = undefined;
}
if (form.values.newTab === true) {
form.values.newTab = undefined;
}
// If service already exists, update it. // If service already exists, update it.
if (config.services && config.services.find((s) => s.id === form.values.id)) { if (config.services && config.services.find((s) => s.id === form.values.id)) {
setConfig({ setConfig({
@@ -191,131 +210,171 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
form.reset(); form.reset();
})} })}
> >
<Group direction="column" grow> <Tabs grow>
<TextInput <Tabs.Tab label="Options">
required <ScrollArea style={{ height: 500 }} scrollbarSize={4}>
label="Service name" <Group direction="column" grow>
placeholder="Plex" <TextInput
{...form.getInputProps('name')} required
/> label="Service name"
placeholder="Plex"
{...form.getInputProps('name')}
/>
<TextInput <TextInput
required required
label="Icon URL" label="Icon URL"
placeholder="/favicon.svg" placeholder={DEFAULT_ICON}
{...form.getInputProps('icon')} {...form.getInputProps('icon')}
/> />
<TextInput <TextInput
required required
label="Service URL" label="Service URL"
placeholder="http://localhost:7575" placeholder="http://localhost:7575"
{...form.getInputProps('url')} {...form.getInputProps('url')}
/> />
<TextInput <TextInput
label="New tab URL" label="On Click URL"
placeholder="http://sonarr.example.com" placeholder="http://sonarr.example.com"
{...form.getInputProps('openedUrl')} {...form.getInputProps('openedUrl')}
/> />
<Select <Select
label="Service type" label="Service type"
defaultValue="Other" defaultValue="Other"
placeholder="Pick one" placeholder="Pick one"
required required
searchable searchable
data={ServiceTypeList} data={ServiceTypeList}
{...form.getInputProps('type')} {...form.getInputProps('type')}
/> />
<Select <Select
label="Category" label="Category"
data={categoryList} data={categoryList}
placeholder="Select a category or create a new one" placeholder="Select a category or create a new one"
nothingFound="Nothing found" nothingFound="Nothing found"
searchable searchable
clearable clearable
creatable creatable
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
}} }}
getCreateLabel={(query) => `+ Create "${query}"`} getCreateLabel={(query) => `+ Create "${query}"`}
onCreate={(query) => {}} onCreate={(query) => {}}
{...form.getInputProps('category')} {...form.getInputProps('category')}
/> />
<LoadingOverlay visible={isLoading} /> <LoadingOverlay visible={isLoading} />
{(form.values.type === 'Sonarr' || {(form.values.type === 'Sonarr' ||
form.values.type === 'Radarr' || form.values.type === 'Radarr' ||
form.values.type === 'Lidarr' || form.values.type === 'Lidarr' ||
form.values.type === 'Readarr') && ( form.values.type === 'Readarr') && (
<> <>
<TextInput <TextInput
required
label="API key"
placeholder="Your API key"
value={form.values.apiKey}
onChange={(event) => {
form.setFieldValue('apiKey', event.currentTarget.value);
}}
error={form.errors.apiKey && 'Invalid API key'}
/>
<Tip>
Get your API key{' '}
<Anchor
target="_blank"
weight="bold"
style={{ fontStyle: 'inherit', fontSize: 'inherit' }}
href={`${hostname}/settings/general`}
>
here.
</Anchor>
</Tip>
</>
)}
{form.values.type === 'qBittorrent' && (
<>
<TextInput
required
label="Username"
placeholder="admin"
value={form.values.username}
onChange={(event) => {
form.setFieldValue('username', event.currentTarget.value);
}}
error={form.errors.username && 'Invalid username'}
/>
<TextInput
required
label="Password"
placeholder="adminadmin"
value={form.values.password}
onChange={(event) => {
form.setFieldValue('password', event.currentTarget.value);
}}
error={form.errors.password && 'Invalid password'}
/>
</>
)}
{form.values.type === 'Deluge' && (
<>
<TextInput
label="Password"
placeholder="password"
value={form.values.password}
onChange={(event) => {
form.setFieldValue('password', event.currentTarget.value);
}}
error={form.errors.password && 'Invalid password'}
/>
</>
)}
{form.values.type === 'Transmission' && (
<>
<TextInput
label="Username"
placeholder="admin"
value={form.values.username}
onChange={(event) => {
form.setFieldValue('username', event.currentTarget.value);
}}
error={form.errors.username && 'Invalid username'}
/>
<TextInput
label="Password"
placeholder="adminadmin"
value={form.values.password}
onChange={(event) => {
form.setFieldValue('password', event.currentTarget.value);
}}
error={form.errors.password && 'Invalid password'}
/>
</>
)}
</Group>
</ScrollArea>
</Tabs.Tab>
<Tabs.Tab label="Advanced Options">
<Group direction="column" grow>
<MultiSelect
required required
label="API key" label="HTTP Status Codes"
placeholder="Your API key" data={StatusCodes}
value={form.values.apiKey} placeholder="Select valid status codes"
onChange={(event) => { clearButtonLabel="Clear selection"
form.setFieldValue('apiKey', event.currentTarget.value); nothingFound="Nothing found"
}} defaultValue={['200']}
error={form.errors.apiKey && 'Invalid API key'} clearable
searchable
{...form.getInputProps('status')}
/> />
<Text <Switch
style={{ label="Open service in new tab"
alignSelf: 'center', defaultChecked={form.values.newTab}
fontSize: '0.75rem', {...form.getInputProps('newTab')}
textAlign: 'center',
color: 'gray',
}}
>
Tip: Get your API key{' '}
<Anchor
target="_blank"
weight="bold"
style={{ fontStyle: 'inherit', fontSize: 'inherit' }}
href={`${hostname}/settings/general`}
>
here.
</Anchor>
</Text>
</>
)}
{form.values.type === 'qBittorrent' && (
<>
<TextInput
required
label="Username"
placeholder="admin"
value={form.values.username}
onChange={(event) => {
form.setFieldValue('username', event.currentTarget.value);
}}
error={form.errors.username && 'Invalid username'}
/> />
<TextInput </Group>
required </Tabs.Tab>
label="Password" </Tabs>
placeholder="adminadmin"
value={form.values.password}
onChange={(event) => {
form.setFieldValue('password', event.currentTarget.value);
}}
error={form.errors.password && 'Invalid password'}
/>
</>
)}
{(form.values.type === 'Deluge' || form.values.type === 'Transmission') && (
<>
<TextInput
required
label="Password"
placeholder="password"
value={form.values.password}
onChange={(event) => {
form.setFieldValue('password', event.currentTarget.value);
}}
error={form.errors.password && 'Invalid password'}
/>
</>
)}
</Group>
<Group grow position="center" mt="xl"> <Group grow position="center" mt="xl">
<Button type="submit">{props.message ?? 'Add service'}</Button> <Button type="submit">{props.message ?? 'Add service'}</Button>
</Group> </Group>

View File

@@ -20,15 +20,30 @@ import DownloadComponent from '../modules/downloads/DownloadsModule';
const useStyles = createStyles((theme, _params) => ({ const useStyles = createStyles((theme, _params) => ({
item: { item: {
borderBottom: 0,
overflow: 'hidden', overflow: 'hidden',
border: '1px solid transparent', borderLeft: '3px solid transparent',
borderRadius: theme.radius.lg, borderRight: '3px solid transparent',
borderBottom: '3px solid transparent',
borderRadius: '20px',
borderColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
marginTop: theme.spacing.md, marginTop: theme.spacing.md,
}, },
itemOpened: { control: {
borderColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[3], backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
borderRadius: theme.spacing.md,
'&:hover': {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
},
},
content: {
margin: theme.spacing.md,
},
label: {
overflow: 'visible',
}, },
})); }));
@@ -44,11 +59,16 @@ const AppShelf = (props: any) => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const sensors = useSensors( const sensors = useSensors(
useSensor(TouchSensor, {}), useSensor(TouchSensor, {
activationConstraint: {
delay: 500,
tolerance: 5,
},
}),
useSensor(MouseSensor, { useSensor(MouseSensor, {
// Require the mouse to move by 10 pixels before activating // Require the mouse to move by 10 pixels before activating
activationConstraint: { activationConstraint: {
delay: 250, delay: 500,
tolerance: 5, tolerance: 5,
}, },
}) })
@@ -101,7 +121,14 @@ const AppShelf = (props: any) => {
<SortableContext items={config.services}> <SortableContext items={config.services}>
<Grid gutter="xl" align="center"> <Grid gutter="xl" align="center">
{filtered.map((service) => ( {filtered.map((service) => (
<Grid.Col key={service.id} span={6} xl={2} xs={4} sm={3} md={3}> <Grid.Col
key={service.id}
span={6}
xl={config.settings.appCardWidth || 2}
xs={4}
sm={3}
md={3}
>
<SortableAppShelfItem service={service} key={service.id} id={service.id} /> <SortableAppShelfItem service={service} key={service.id} id={service.id} />
</Grid.Col> </Grid.Col>
))} ))}
@@ -125,6 +152,7 @@ const AppShelf = (props: any) => {
const noCategory = config.services.filter( const noCategory = config.services.filter(
(e) => e.category === undefined || e.category === null (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 // Create an item with 0: true, 1: true, 2: true... For each category
return ( return (
// Return one item for each category // Return one item for each category
@@ -135,11 +163,6 @@ const AppShelf = (props: any) => {
order={2} order={2}
iconPosition="right" iconPosition="right"
multiple multiple
styles={{
item: {
borderRadius: '20px',
},
}}
initialState={toggledCategories} initialState={toggledCategories}
onChange={(idx) => settoggledCategories(idx)} onChange={(idx) => settoggledCategories(idx)}
> >
@@ -154,6 +177,7 @@ const AppShelf = (props: any) => {
{item()} {item()}
</Accordion.Item> </Accordion.Item>
) : null} ) : null}
{downloadEnabled ? (
<Accordion.Item key="Downloads" label="Your downloads"> <Accordion.Item key="Downloads" label="Your downloads">
<Paper <Paper
p="lg" p="lg"
@@ -169,6 +193,7 @@ const AppShelf = (props: any) => {
<DownloadComponent /> <DownloadComponent />
</Paper> </Paper>
</Accordion.Item> </Accordion.Item>
) : null}
</Accordion> </Accordion>
</Group> </Group>
); );

View File

@@ -83,8 +83,8 @@ export function AppShelfItem(props: any) {
> >
<Card.Section> <Card.Section>
<Anchor <Anchor
target="_blank" target={service.newTab === false ? '_top' : '_blank'}
href={service.url} href={service.openedUrl ? service.openedUrl : service.url}
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }} style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
> >
<Text mt="sm" align="center" lineClamp={1} weight={550}> <Text mt="sm" align="center" lineClamp={1} weight={550}>
@@ -127,13 +127,14 @@ export function AppShelfItem(props: any) {
src={service.icon} src={service.icon}
fit="contain" fit="contain"
onClick={() => { onClick={() => {
if (service.openedUrl) window.open(service.openedUrl, '_blank'); if (service.openedUrl) {
else window.open(service.url); window.open(service.openedUrl, service.newTab === false ? '_top' : '_blank');
} else window.open(service.url, service.newTab === false ? '_top' : '_blank');
}} }}
/> />
</motion.i> </motion.i>
</AspectRatio> </AspectRatio>
<PingComponent url={service.url} /> <PingComponent url={service.url} status={service.status} />
</Card.Section> </Card.Section>
</Center> </Center>
</Card> </Card>

View File

@@ -20,20 +20,7 @@ export default function AppShelfMenu(props: any) {
onClose={() => setOpened(false)} onClose={() => setOpened(false)}
title="Modify a service" title="Modify a service"
> >
<AddAppShelfItemForm <AddAppShelfItemForm setOpened={setOpened} {...service} message="Save service" />
setOpened={setOpened}
name={service.name}
id={service.id}
category={service.category}
type={service.type}
url={service.url}
icon={service.icon}
apiKey={service.apiKey}
username={service.username}
password={service.password}
openedUrl={service.openedUrl}
message="Save service"
/>
</Modal> </Modal>
<Menu <Menu
position="right" position="right"

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

@@ -3,6 +3,7 @@ import { useForm } from '@mantine/form';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { ColorSelector } from './ColorSelector'; import { ColorSelector } from './ColorSelector';
import { OpacitySelector } from './OpacitySelector'; import { OpacitySelector } from './OpacitySelector';
import { AppCardWidthSelector } from './AppCardWidthSelector';
import { ShadeSelector } from './ShadeSelector'; import { ShadeSelector } from './ShadeSelector';
export default function TitleChanger() { export default function TitleChanger() {
@@ -36,7 +37,7 @@ export default function TitleChanger() {
}; };
return ( return (
<Group direction="column" grow> <Group direction="column" grow mb="lg">
<form onSubmit={form.onSubmit((values) => saveChanges(values))}> <form onSubmit={form.onSubmit((values) => saveChanges(values))}>
<Group grow direction="column"> <Group grow direction="column">
<TextInput label="Page title" placeholder="Homarr 🦞" {...form.getInputProps('title')} /> <TextInput label="Page title" placeholder="Homarr 🦞" {...form.getInputProps('title')} />
@@ -58,6 +59,7 @@ export default function TitleChanger() {
<ColorSelector type="secondary" /> <ColorSelector type="secondary" />
<ShadeSelector /> <ShadeSelector />
<OpacitySelector /> <OpacitySelector />
<AppCardWidthSelector />
</Group> </Group>
); );
} }

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { Group, Text, Slider } from '@mantine/core';
import { useConfig } from '../../tools/state';
export function AppCardWidthSelector() {
const { config, setConfig } = useConfig();
const setappCardWidth = (appCardWidth: number) => {
setConfig({
...config,
settings: {
...config.settings,
appCardWidth,
},
});
};
return (
<Group direction="column" spacing="xs" grow>
<Text>App Width</Text>
<Slider
label={null}
defaultValue={config.settings.appCardWidth}
step={0.2}
min={0.8}
max={2}
styles={{ markLabel: { fontSize: 'xx-small' } }}
onChange={(value) => setappCardWidth(value)}
/>
</Group>
);
}

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 { useState } from 'react';
import { IconBrandGithub as BrandGithub } from '@tabler/icons';
import { CURRENT_VERSION } from '../../../data/constants';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch'; import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
import { WidgetsPositionSwitch } from '../WidgetsPositionSwitch/WidgetsPositionSwitch'; import { WidgetsPositionSwitch } from '../WidgetsPositionSwitch/WidgetsPositionSwitch';
import ConfigChanger from '../Config/ConfigChanger'; import ConfigChanger from '../Config/ConfigChanger';
import SaveConfigComponent from '../Config/SaveConfig'; import SaveConfigComponent from '../Config/SaveConfig';
import ModuleEnabler from './ModuleEnabler'; import ModuleEnabler from './ModuleEnabler';
import Tip from '../layout/Tip';
export default function CommonSettings(args: any) { export default function CommonSettings(args: any) {
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
@@ -25,11 +24,16 @@ export default function CommonSettings(args: any) {
); );
return ( return (
<Group direction="column" grow> <Group direction="column" grow mb="lg">
<Group grow direction="column" spacing={0}> <Group grow direction="column" spacing={0}>
<Text>Search engine</Text> <Text>Search engine</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 <SegmentedControl
fullWidth fullWidth
mb="sm"
title="Search engine" title="Search engine"
value={ value={
// Match config.settings.searchUrl with a key in the matches array // Match config.settings.searchUrl with a key in the matches array
@@ -51,21 +55,24 @@ export default function CommonSettings(args: any) {
data={matches} data={matches}
/> />
{searchUrl === 'Custom' && ( {searchUrl === 'Custom' && (
<TextInput <>
label="Query URL" <Tip>%s can be used as a placeholder for the query.</Tip>
placeholder="Custom query url" <TextInput
value={customSearchUrl} label="Query URL"
onChange={(event) => { placeholder="Custom query URL"
setCustomSearchUrl(event.currentTarget.value); value={customSearchUrl}
setConfig({ onChange={(event) => {
...config, setCustomSearchUrl(event.currentTarget.value);
settings: { setConfig({
...config.settings, ...config,
searchUrl: event.currentTarget.value, settings: {
}, ...config.settings,
}); searchUrl: event.currentTarget.value,
}} },
/> });
}}
/>
</>
)} )}
</Group> </Group>
<ColorSchemeSwitch /> <ColorSchemeSwitch />
@@ -73,47 +80,7 @@ export default function CommonSettings(args: any) {
<ModuleEnabler /> <ModuleEnabler />
<ConfigChanger /> <ConfigChanger />
<SaveConfigComponent /> <SaveConfigComponent />
<Text <Tip>Upload your config file by dragging and dropping it onto the page!</Tip>
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>
<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>
</Group>
</Group> </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 { useHotkeys } from '@mantine/hooks';
import { useState } from 'react'; import { useState } from 'react';
import { IconSettings } from '@tabler/icons'; import { IconSettings } from '@tabler/icons';
import AdvancedSettings from './AdvancedSettings'; import AdvancedSettings from './AdvancedSettings';
import CommonSettings from './CommonSettings'; import CommonSettings from './CommonSettings';
import Credits from './Credits';
function SettingsMenu(props: any) { function SettingsMenu(props: any) {
return ( return (
<Tabs grow> <Tabs grow>
<Tabs.Tab data-autofocus label="Common"> <Tabs.Tab data-autofocus label="Common">
<CommonSettings /> <ScrollArea style={{ height: '78vh' }} offsetScrollbars>
<CommonSettings />
</ScrollArea>
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab label="Customizations"> <Tabs.Tab label="Customizations">
<AdvancedSettings /> <ScrollArea style={{ height: '78vh' }} offsetScrollbars>
<AdvancedSettings />
</ScrollArea>
</Tabs.Tab> </Tabs.Tab>
</Tabs> </Tabs>
); );
@@ -26,13 +31,14 @@ export function SettingsMenuButton(props: any) {
<> <>
<Drawer <Drawer
size="xl" size="xl"
padding="xl" padding="lg"
position="right" position="right"
title={<Title order={3}>Settings</Title>} title={<Title order={5}>Settings</Title>}
opened={props.opened || opened} opened={props.opened || opened}
onClose={() => setOpened(false)} onClose={() => setOpened(false)}
> >
<SettingsMenu /> <SettingsMenu />
<Credits />
</Drawer> </Drawer>
<ActionIcon <ActionIcon
variant="default" variant="default"

View File

@@ -1,23 +1,29 @@
import React from 'react';
import { import {
createStyles, ActionIcon,
Header as Head,
Group,
Box, Box,
Burger, Burger,
createStyles,
Drawer, Drawer,
Title, Group,
Header as Head,
ScrollArea, ScrollArea,
ActionIcon, Title,
Transition, Transition,
} from '@mantine/core'; } from '@mantine/core';
import { useBooleanToggle } from '@mantine/hooks'; import { useBooleanToggle } from '@mantine/hooks';
import { Logo } from './Logo';
import SearchBar from '../modules/search/SearchModule';
import { AddItemShelfButton } from '../AppShelf/AddAppShelfItem'; import { AddItemShelfButton } from '../AppShelf/AddAppShelfItem';
import { SettingsMenuButton } from '../Settings/SettingsMenu'; import {
CalendarModule,
DateModule,
TotalDownloadsModule,
WeatherModule,
DashdotModule,
} from '../modules';
import { ModuleWrapper } from '../modules/moduleWrapper'; 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; const HEADER_HEIGHT = 60;
@@ -47,6 +53,7 @@ export function Header(props: any) {
</Box> </Box>
<Group noWrap> <Group noWrap>
<SearchBar /> <SearchBar />
<DockerDrawer />
<SettingsMenuButton /> <SettingsMenuButton />
<AddItemShelfButton /> <AddItemShelfButton />
<ActionIcon className={classes.burger} variant="default" radius="md" size="xl"> <ActionIcon className={classes.burger} variant="default" radius="md" size="xl">
@@ -78,12 +85,13 @@ export function Header(props: any) {
> >
{(styles) => ( {(styles) => (
<div style={styles}> <div style={styles}>
<ScrollArea style={{ height: '90vh' }}> <ScrollArea offsetScrollbars style={{ height: '90vh' }}>
<Group my="sm" grow direction="column" style={{ width: 300 }}> <Group my="sm" grow direction="column" style={{ width: 300 }}>
<ModuleWrapper module={CalendarModule} /> <ModuleWrapper module={CalendarModule} />
<ModuleWrapper module={TotalDownloadsModule} /> <ModuleWrapper module={TotalDownloadsModule} />
<ModuleWrapper module={WeatherModule} /> <ModuleWrapper module={WeatherModule} />
<ModuleWrapper module={DateModule} /> <ModuleWrapper module={DateModule} />
<ModuleWrapper module={DashdotModule} />
</Group> </Group>
</ScrollArea> </ScrollArea>
</div> </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 { Group } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks'; import { useMediaQuery } from '@mantine/hooks';
import { CalendarModule, DateModule, TotalDownloadsModule, WeatherModule } from '../modules'; import { CalendarModule, DateModule, TotalDownloadsModule, WeatherModule } from '../modules';
import { DashdotModule } from '../modules/dash.';
import { ModuleWrapper } from '../modules/moduleWrapper'; import { ModuleWrapper } from '../modules/moduleWrapper';
export default function Widgets(props: any) { export default function Widgets(props: any) {
@@ -14,6 +15,7 @@ export default function Widgets(props: any) {
<ModuleWrapper module={TotalDownloadsModule} /> <ModuleWrapper module={TotalDownloadsModule} />
<ModuleWrapper module={WeatherModule} /> <ModuleWrapper module={WeatherModule} />
<ModuleWrapper module={DateModule} /> <ModuleWrapper module={DateModule} />
<ModuleWrapper module={DashdotModule} />
</Group> </Group>
)} )}
</> </>

View File

@@ -29,6 +29,12 @@ export const CalendarModule: IModule = {
'A calendar module for displaying upcoming releases. It interacts with the Sonarr and Radarr API.', 'A calendar module for displaying upcoming releases. It interacts with the Sonarr and Radarr API.',
icon: CalendarIcon, icon: CalendarIcon,
component: CalendarComponent, component: CalendarComponent,
options: {
sundaystart: {
name: 'Start the week on Sunday',
value: false,
},
},
}; };
export default function CalendarComponent(props: any) { export default function CalendarComponent(props: any) {
@@ -62,50 +68,69 @@ export default function CalendarComponent(props: any) {
useEffect(() => { useEffect(() => {
// Create each Sonarr service and get the medias // Create each Sonarr service and get the medias
const currentSonarrMedias: any[] = [...sonarrMedias]; const currentSonarrMedias: any[] = [];
Promise.all( Promise.all(
sonarrServices.map((service) => sonarrServices.map((service) =>
getMedias(service, 'sonarr').then((res) => { getMedias(service, 'sonarr')
currentSonarrMedias.push(...res.data); .then((res) => {
}) currentSonarrMedias.push(...res.data);
})
.catch(() => {
currentSonarrMedias.push([]);
})
) )
).then(() => { ).then(() => {
setSonarrMedias(currentSonarrMedias); setSonarrMedias(currentSonarrMedias);
}); });
const currentRadarrMedias: any[] = [...radarrMedias]; const currentRadarrMedias: any[] = [];
Promise.all( Promise.all(
radarrServices.map((service) => radarrServices.map((service) =>
getMedias(service, 'radarr').then((res) => { getMedias(service, 'radarr')
currentRadarrMedias.push(...res.data); .then((res) => {
}) currentRadarrMedias.push(...res.data);
})
.catch(() => {
currentRadarrMedias.push([]);
})
) )
).then(() => { ).then(() => {
setRadarrMedias(currentRadarrMedias); setRadarrMedias(currentRadarrMedias);
}); });
const currentLidarrMedias: any[] = [...lidarrMedias]; const currentLidarrMedias: any[] = [];
Promise.all( Promise.all(
lidarrServices.map((service) => lidarrServices.map((service) =>
getMedias(service, 'lidarr').then((res) => { getMedias(service, 'lidarr')
currentLidarrMedias.push(...res.data); .then((res) => {
}) currentLidarrMedias.push(...res.data);
})
.catch(() => {
currentLidarrMedias.push([]);
})
) )
).then(() => { ).then(() => {
setLidarrMedias(currentLidarrMedias); setLidarrMedias(currentLidarrMedias);
}); });
const currentReadarrMedias: any[] = [...readarrMedias]; const currentReadarrMedias: any[] = [];
Promise.all( Promise.all(
readarrServices.map((service) => readarrServices.map((service) =>
getMedias(service, 'readarr').then((res) => { getMedias(service, 'readarr')
currentReadarrMedias.push(...res.data); .then((res) => {
}) currentReadarrMedias.push(...res.data);
})
.catch(() => {
currentReadarrMedias.push([]);
})
) )
).then(() => { ).then(() => {
setReadarrMedias(currentReadarrMedias); setReadarrMedias(currentReadarrMedias);
}); });
}, [config.services]); }, [config.services]);
const weekStartsAtSunday =
(config?.modules?.[CalendarModule.title]?.options?.sundaystart?.value as boolean) ?? false;
return ( return (
<Calendar <Calendar
firstDayOfWeek={weekStartsAtSunday ? 'sunday' : 'monday'}
onChange={(day: any) => {}} onChange={(day: any) => {}}
dayStyle={(date) => dayStyle={(date) =>
date.getDay() === today.getDay() && date.getDate() === today.getDate() date.getDay() === today.getDay() && date.getDate() === today.getDate()
@@ -115,6 +140,13 @@ export default function CalendarComponent(props: any) {
} }
: {} : {}
} }
styles={{
calendarHeader: {
marginRight: 15,
marginLeft: 15,
},
}}
allowLevelChange={false}
dayClassName={(date, modifiers) => cx({ [classes.weekend]: modifiers.weekend })} dayClassName={(date, modifiers) => cx({ [classes.weekend]: modifiers.weekend })}
renderDay={(renderdate) => ( renderDay={(renderdate) => (
<DayComponent <DayComponent

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 [date, setDate] = useState(new Date());
const setSafeInterval = useSetSafeInterval(); const setSafeInterval = useSetSafeInterval();
const { config } = useConfig(); 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'; const formatString = isFullTime ? 'HH:mm' : 'h:mm A';
// Change date on minute change // Change date on minute change
// Note: Using 10 000ms instead of 1000ms to chill a little :) // Note: Using 10 000ms instead of 1000ms to chill a little :)

View File

@@ -59,7 +59,7 @@ export default function DownloadComponent() {
setIsLoading(false); setIsLoading(false);
}); });
}, 5000); }, 5000);
}, [config.services]); }, []);
if (downloadServices.length === 0) { if (downloadServices.length === 0) {
return ( return (
@@ -158,7 +158,7 @@ export default function DownloadComponent() {
<Progress <Progress
radius="lg" radius="lg"
color={ color={
torrent.state === 'paused' ? 'yellow' : torrent.progress === 1 ? 'green' : 'blue' torrent.progress === 1 ? 'green' : torrent.state === 'paused' ? 'yellow' : 'blue'
} }
value={torrent.progress * 100} value={torrent.progress * 100}
size="lg" size="lg"

View File

@@ -1,6 +1,7 @@
export * from './date';
export * from './calendar'; export * from './calendar';
export * from './search'; export * from './dash.';
export * from './ping'; export * from './date';
export * from './weather';
export * from './downloads'; 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 { useConfig } from '../../tools/state';
import { IModule } from './modules'; import { IModule } from './modules';
function getItems(module: IModule) { function getItems(module: IModule) {
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
const enabledModules = config.modules ?? {};
const items: JSX.Element[] = []; const items: JSX.Element[] = [];
if (module.options) { if (module.options) {
const keys = Object.keys(module.options); const keys = Object.keys(module.options);
@@ -15,6 +23,38 @@ function getItems(module: IModule) {
types.forEach((type, index) => { types.forEach((type, index) => {
const optionName = `${module.title}.${keys[index]}`; const optionName = `${module.title}.${keys[index]}`;
const moduleInConfig = config.modules?.[module.title]; 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') { if (type === 'string') {
items.push( items.push(
<form <form
@@ -44,7 +84,11 @@ function getItems(module: IModule) {
id={optionName} id={optionName}
name={optionName} name={optionName}
label={values[index].name} 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) => {}} onChange={(e) => {}}
/> />
@@ -59,7 +103,9 @@ function getItems(module: IModule) {
<Switch <Switch
defaultChecked={ defaultChecked={
// Set default checked to the value of the option if it exists // 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]} key={keys[index]}
onClick={(e) => { onClick={(e) => {
@@ -120,8 +166,8 @@ export function ModuleWrapper(props: any) {
styles={{ styles={{
root: { root: {
position: 'absolute', position: 'absolute',
top: 15, top: 12,
right: 15, right: 12,
}, },
}} }}
/> />

View File

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

View File

@@ -11,6 +11,8 @@ const service: serviceItem = {
name: 'YouTube', name: 'YouTube',
icon: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/youtube.png', icon: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/youtube.png',
url: 'https://youtube.com/', url: 'https://youtube.com/',
status: ['200'],
newTab: false,
}; };
export const Default = (args: any) => <PingComponent service={service} />; export const Default = (args: any) => <PingComponent service={service} />;

View File

@@ -1,5 +1,5 @@
import { Indicator, Tooltip } from '@mantine/core'; import { Indicator, Tooltip } from '@mantine/core';
import axios from 'axios'; import axios, { AxiosResponse } from 'axios';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { IconPlug as Plug } from '@tabler/icons'; import { IconPlug as Plug } from '@tabler/icons';
@@ -19,18 +19,37 @@ export default function PingComponent(props: any) {
const { url }: { url: string } = props; const { url }: { url: string } = props;
const [isOnline, setOnline] = useState<State>('loading'); const [isOnline, setOnline] = useState<State>('loading');
const [response, setResponse] = useState(500);
const exists = config.modules?.[PingModule.title]?.enabled ?? false; const exists = config.modules?.[PingModule.title]?.enabled ?? false;
function statusCheck(response: AxiosResponse) {
const { status }: { status: string[] } = props;
//Default Status
let acceptableStatus = ['200'];
if (status !== undefined && status.length) {
acceptableStatus = status;
}
// Checks if reported status is in acceptable status array
if (acceptableStatus.indexOf(response.status.toString()) >= 0) {
setOnline('online');
setResponse(response.status);
} else {
setOnline('down');
setResponse(response.status);
}
}
useEffect(() => { useEffect(() => {
if (!exists) { if (!exists) {
return; return;
} }
axios axios
.get('/api/modules/ping', { params: { url } }) .get('/api/modules/ping', { params: { url } })
.then(() => { .then((response) => {
setOnline('online'); statusCheck(response);
}) })
.catch(() => { .catch((error) => {
setOnline('down'); statusCheck(error.response);
}); });
}, [config.modules?.[PingModule.title]?.enabled]); }, [config.modules?.[PingModule.title]?.enabled]);
if (!exists) { if (!exists) {
@@ -40,7 +59,13 @@ export default function PingComponent(props: any) {
<Tooltip <Tooltip
radius="lg" radius="lg"
style={{ position: 'absolute', bottom: 20, right: 20 }} style={{ position: 'absolute', bottom: 20, right: 20 }}
label={isOnline === 'loading' ? 'Loading...' : isOnline === 'online' ? 'Online' : 'Offline'} label={
isOnline === 'loading'
? 'Loading...'
: isOnline === 'online'
? `Online - ${response}`
: `Offline - ${response}`
}
> >
<motion.div <motion.div
animate={{ animate={{

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 { useDebouncedValue, useForm, useHotkeys } from '@mantine/hooks';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { import {
@@ -96,44 +96,32 @@ export default function SearchBar(props: any) {
} else if (isTorrent) { } else if (isTorrent) {
window.open(`https://bitsearch.to/search?q=${query.substring(3)}`); window.open(`https://bitsearch.to/search?q=${query.substring(3)}`);
} else { } else {
window.open(`${queryUrl}${values.query}`); window.open(
`${
queryUrl.includes('%s')
? queryUrl.replace('%s', values.query)
: queryUrl + values.query
}`
);
} }
}, 20); }, 20);
})} })}
> >
<Popover <Autocomplete
opened={opened} autoFocus
position="bottom" variant="filled"
placement="start" data={autocompleteData}
width={260} icon={icon}
withArrow ref={textInput}
rightSectionWidth={90}
rightSection={rightSection}
radius="md" radius="md"
trapFocus={false} size="md"
transition="pop-bottom-right" styles={{ rightSection: { pointerEvents: 'none' } }}
onFocusCapture={() => setOpened(true)} placeholder="Search the web..."
onBlurCapture={() => setOpened(false)} {...props}
target={ {...form.getInputProps('query')}
<Autocomplete />
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>
</form> </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 axios from 'axios';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
@@ -29,7 +29,7 @@ export const WeatherModule: IModule = {
}, },
location: { location: {
name: 'Current location', name: 'Current location',
value: '', value: 'Paris',
}, },
}, },
}; };
@@ -135,7 +135,7 @@ export default function WeatherComponent(props: any) {
const { config } = useConfig(); const { config } = useConfig();
const [weather, setWeather] = useState({} as WeatherResponse); const [weather, setWeather] = useState({} as WeatherResponse);
const cityInput: string = 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 = const isFahrenheit: boolean =
(config?.modules?.[WeatherModule.title]?.options?.freedomunit?.value as boolean) ?? false; (config?.modules?.[WeatherModule.title]?.options?.freedomunit?.value as boolean) ?? false;
@@ -157,7 +157,18 @@ export default function WeatherComponent(props: any) {
}); });
}, [cityInput]); }, [cityInput]);
if (!weather.current_weather) { 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 { function usePerferedUnit(value: number): string {
return isFahrenheit ? `${(value * (9 / 5) + 32).toFixed(1)}°F` : `${value.toFixed(1)}°C`; 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'; import { NextFetchEvent, NextRequest, NextResponse } from 'next/server';
export function middleware(req: NextRequest, ev: NextFetchEvent) { 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(); const url = req.nextUrl.clone();
if ( if (
!ok && !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',
});
};

View File

@@ -44,7 +44,10 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
}); });
} }
// Get the origin URL // Get the origin URL
const { origin } = new URL(service.url); let { href: origin } = new URL(service.url);
if (origin.endsWith('/')) {
origin = origin.slice(0, -1);
}
const pined = `${origin}${url?.url}?apiKey=${service.apiKey}&end=${nextMonth}&start=${lastMonth}`; const pined = `${origin}${url?.url}?apiKey=${service.apiKey}&end=${nextMonth}&start=${lastMonth}`;
const data = await axios.get( const data = await axios.get(
`${origin}${url?.url}?apiKey=${service.apiKey}&end=${nextMonth}&start=${lastMonth}` `${origin}${url?.url}?apiKey=${service.apiKey}&end=${nextMonth}&start=${lastMonth}`

View File

@@ -7,53 +7,52 @@ import { Config } from '../../../tools/types';
async function Post(req: NextApiRequest, res: NextApiResponse) { async function Post(req: NextApiRequest, res: NextApiResponse) {
// Get the type of service from the request url // Get the type of service from the request url
const torrents: NormalizedTorrent[] = [];
const { config }: { config: Config } = req.body; const { config }: { config: Config } = req.body;
const qBittorrentService = config.services const qBittorrentServices = config.services.filter((service) => service.type === 'qBittorrent');
.filter((service) => service.type === 'qBittorrent') const delugeServices = config.services.filter((service) => service.type === 'Deluge');
.at(0); const transmissionServices = config.services.filter((service) => service.type === 'Transmission');
const delugeService = config.services.filter((service) => service.type === 'Deluge').at(0);
const transmissionService = config.services const torrents: NormalizedTorrent[] = [];
.filter((service) => service.type === 'Transmission')
.at(0); if (!qBittorrentServices && !delugeServices && !transmissionServices) {
if (!qBittorrentService && !delugeService && !transmissionService) {
return res.status(500).json({ return res.status(500).json({
statusCode: 500, statusCode: 500,
message: 'Missing service', message: 'Missing services',
}); });
} }
if (qBittorrentService) { await Promise.all(
torrents.push( qBittorrentServices.map((service) =>
...( new QBittorrent({
await new QBittorrent({ baseUrl: service.url,
baseUrl: qBittorrentService.url, username: service.username,
username: qBittorrentService.username, password: service.password,
password: qBittorrentService.password, })
}).getAllData() .getAllData()
).torrents .then((e) => torrents.push(...e.torrents))
); )
} );
if (delugeService) { await Promise.all(
torrents.push( delugeServices.map((service) =>
...( new Deluge({
await new Deluge({ baseUrl: service.url,
baseUrl: delugeService.url, password: 'password' in service ? service.password : '',
password: delugeService.password, })
}).getAllData() .getAllData()
).torrents .then((e) => torrents.push(...e.torrents))
); )
} );
if (transmissionService) { // Map transmissionServices
torrents.push( await Promise.all(
...( transmissionServices.map((service) =>
await new Transmission({ new Transmission({
baseUrl: transmissionService.url, baseUrl: service.url,
username: transmissionService.username, username: 'username' in service ? service.username : '',
password: transmissionService.password, password: 'password' in service ? service.password : '',
}).getAllData() })
).torrents .getAllData()
); .then((e) => torrents.push(...e.torrents))
} )
);
res.status(200).json(torrents); res.status(200).json(torrents);
} }

View File

@@ -7,10 +7,14 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
await axios await axios
.get(url as string) .get(url as string)
.then((response) => { .then((response) => {
res.status(200).json(response.data); res.status(response.status).json(response.statusText);
}) })
.catch((error) => { .catch((error) => {
res.status(500).json(error); if (error.response) {
res.status(error.response.status).json(error.response.statusText);
} else {
res.status(500).json('Server Error');
}
}); });
// // Make a request to the URL // // Make a request to the URL
// const response = await axios.get(url); // const response = await axios.get(url);

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

@@ -12,6 +12,7 @@ export interface Settings {
background?: string; background?: string;
appOpacity?: number; appOpacity?: number;
widgetPosition?: string; widgetPosition?: string;
appCardWidth?: number;
} }
export interface Config { export interface Config {
@@ -31,9 +32,36 @@ 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: '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 = [
{ value: '_blank', label: 'New Tab' },
{ value: '_top', label: 'Same Window' },
];
export const ServiceTypeList = [ export const ServiceTypeList = [
'Other', 'Other',
'Emby', 'Emby',
'Dash.',
'Deluge', 'Deluge',
'Lidarr', 'Lidarr',
'Plex', 'Plex',
@@ -46,6 +74,7 @@ export const ServiceTypeList = [
export type ServiceType = export type ServiceType =
| 'Other' | 'Other'
| 'Emby' | 'Emby'
| 'Dash.'
| 'Deluge' | 'Deluge'
| 'Lidarr' | 'Lidarr'
| 'Plex' | 'Plex'
@@ -55,6 +84,12 @@ export type ServiceType =
| 'qBittorrent' | 'qBittorrent'
| 'Transmission'; | '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 { export interface serviceItem {
id: string; id: string;
name: string; name: string;
@@ -66,4 +101,6 @@ export interface serviceItem {
password?: string; password?: string;
username?: string; username?: string;
openedUrl?: string; openedUrl?: string;
newTab?: boolean;
status?: string[];
} }

380
yarn.lock
View File

@@ -2409,111 +2409,118 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@next/bundle-analyzer@npm:^12.1.4": "@next/bundle-analyzer@npm:^12.2.0":
version: 12.1.6 version: 12.2.0
resolution: "@next/bundle-analyzer@npm:12.1.6" resolution: "@next/bundle-analyzer@npm:12.2.0"
dependencies: dependencies:
webpack-bundle-analyzer: 4.3.0 webpack-bundle-analyzer: 4.3.0
checksum: cf37be49d45d706aea95df489656341bec64783e567067d15036b25330d7a69204987b2c402277f201b9bf943de588323b120fd8096bb3d6846a054bbb2cdc7e checksum: e08770ed2f7bfa4fb38c29d58d1e3ad198fa7e9a8c061ea5e15950dd10576bed0b5b8c19266e18503af1d211a0d8d450b5fed4926f6863135b38e585d6fd1980
languageName: node languageName: node
linkType: hard linkType: hard
"@next/env@npm:12.1.6": "@next/env@npm:12.2.0":
version: 12.1.6 version: 12.2.0
resolution: "@next/env@npm:12.1.6" resolution: "@next/env@npm:12.2.0"
checksum: e6a4f189f0d653d13dc7ad510f6c9d2cf690bfd9e07c554bd501b840f8dabc3da5adcab874b0bc01aab86c3647cff4fb84692e3c3b28125af26f0b05cd4c7fcf checksum: 5fb317bdb5eb2d5df12ff55e335368792dba21874c5ece3cabf8cd312cec911a1d54ecf368e69dc08640b0244669b8a98c86cd035c7874b17640602e67c1b9d9
languageName: node languageName: node
linkType: hard linkType: hard
"@next/eslint-plugin-next@npm:^12.1.4": "@next/eslint-plugin-next@npm:^12.2.0":
version: 12.1.6 version: 12.2.0
resolution: "@next/eslint-plugin-next@npm:12.1.6" resolution: "@next/eslint-plugin-next@npm:12.2.0"
dependencies: dependencies:
glob: 7.1.7 glob: 7.1.7
checksum: 33dcaf71f299d3c8a0744cad512369f92d7a355f3c0d57f2496e888e4242080c49226ec2c59ba2efac04b3a1df51c36019b853b4177df082ca4621a1713a2229 checksum: 2e33b9af79af680fd873d74e91bed397930a91802c1d7a293db757227ebc431d3d856de69477dc178dec8b531635ea69d79b188293024f1371afe6c348dbe647
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-android-arm-eabi@npm:12.1.6": "@next/swc-android-arm-eabi@npm:12.2.0":
version: 12.1.6 version: 12.2.0
resolution: "@next/swc-android-arm-eabi@npm:12.1.6" resolution: "@next/swc-android-arm-eabi@npm:12.2.0"
conditions: os=android & cpu=arm conditions: os=android & cpu=arm
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-android-arm64@npm:12.1.6": "@next/swc-android-arm64@npm:12.2.0":
version: 12.1.6 version: 12.2.0
resolution: "@next/swc-android-arm64@npm:12.1.6" resolution: "@next/swc-android-arm64@npm:12.2.0"
conditions: os=android & cpu=arm64 conditions: os=android & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-darwin-arm64@npm:12.1.6": "@next/swc-darwin-arm64@npm:12.2.0":
version: 12.1.6 version: 12.2.0
resolution: "@next/swc-darwin-arm64@npm:12.1.6" resolution: "@next/swc-darwin-arm64@npm:12.2.0"
conditions: os=darwin & cpu=arm64 conditions: os=darwin & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-darwin-x64@npm:12.1.6": "@next/swc-darwin-x64@npm:12.2.0":
version: 12.1.6 version: 12.2.0
resolution: "@next/swc-darwin-x64@npm:12.1.6" resolution: "@next/swc-darwin-x64@npm:12.2.0"
conditions: os=darwin & cpu=x64 conditions: os=darwin & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-linux-arm-gnueabihf@npm:12.1.6": "@next/swc-freebsd-x64@npm:12.2.0":
version: 12.1.6 version: 12.2.0
resolution: "@next/swc-linux-arm-gnueabihf@npm:12.1.6" 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 conditions: os=linux & cpu=arm
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-linux-arm64-gnu@npm:12.1.6": "@next/swc-linux-arm64-gnu@npm:12.2.0":
version: 12.1.6 version: 12.2.0
resolution: "@next/swc-linux-arm64-gnu@npm:12.1.6" resolution: "@next/swc-linux-arm64-gnu@npm:12.2.0"
conditions: os=linux & cpu=arm64 & libc=glibc conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-linux-arm64-musl@npm:12.1.6": "@next/swc-linux-arm64-musl@npm:12.2.0":
version: 12.1.6 version: 12.2.0
resolution: "@next/swc-linux-arm64-musl@npm:12.1.6" resolution: "@next/swc-linux-arm64-musl@npm:12.2.0"
conditions: os=linux & cpu=arm64 & libc=musl conditions: os=linux & cpu=arm64 & libc=musl
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-linux-x64-gnu@npm:12.1.6": "@next/swc-linux-x64-gnu@npm:12.2.0":
version: 12.1.6 version: 12.2.0
resolution: "@next/swc-linux-x64-gnu@npm:12.1.6" resolution: "@next/swc-linux-x64-gnu@npm:12.2.0"
conditions: os=linux & cpu=x64 & libc=glibc conditions: os=linux & cpu=x64 & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-linux-x64-musl@npm:12.1.6": "@next/swc-linux-x64-musl@npm:12.2.0":
version: 12.1.6 version: 12.2.0
resolution: "@next/swc-linux-x64-musl@npm:12.1.6" resolution: "@next/swc-linux-x64-musl@npm:12.2.0"
conditions: os=linux & cpu=x64 & libc=musl conditions: os=linux & cpu=x64 & libc=musl
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-win32-arm64-msvc@npm:12.1.6": "@next/swc-win32-arm64-msvc@npm:12.2.0":
version: 12.1.6 version: 12.2.0
resolution: "@next/swc-win32-arm64-msvc@npm:12.1.6" resolution: "@next/swc-win32-arm64-msvc@npm:12.2.0"
conditions: os=win32 & cpu=arm64 conditions: os=win32 & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-win32-ia32-msvc@npm:12.1.6": "@next/swc-win32-ia32-msvc@npm:12.2.0":
version: 12.1.6 version: 12.2.0
resolution: "@next/swc-win32-ia32-msvc@npm:12.1.6" resolution: "@next/swc-win32-ia32-msvc@npm:12.2.0"
conditions: os=win32 & cpu=ia32 conditions: os=win32 & cpu=ia32
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-win32-x64-msvc@npm:12.1.6": "@next/swc-win32-x64-msvc@npm:12.2.0":
version: 12.1.6 version: 12.2.0
resolution: "@next/swc-win32-x64-msvc@npm:12.1.6" resolution: "@next/swc-win32-x64-msvc@npm:12.2.0"
conditions: os=win32 & cpu=x64 conditions: os=win32 & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
@@ -3783,6 +3790,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@szmarczak/http-timer@npm:^5.0.1":
version: 5.0.1 version: 5.0.1
resolution: "@szmarczak/http-timer@npm:5.0.1" resolution: "@szmarczak/http-timer@npm:5.0.1"
@@ -3874,6 +3890,26 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@types/eslint-scope@npm:^3.7.3":
version: 3.7.3 version: 3.7.3
resolution: "@types/eslint-scope@npm:3.7.3" resolution: "@types/eslint-scope@npm:3.7.3"
@@ -4151,6 +4187,25 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@types/stack-utils@npm:^2.0.0":
version: 2.0.1 version: 2.0.1
resolution: "@types/stack-utils@npm:2.0.1" resolution: "@types/stack-utils@npm:2.0.1"
@@ -5196,6 +5251,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "assert@npm:^1.1.1":
version: 1.5.0 version: 1.5.0
resolution: "assert@npm:1.5.0" resolution: "assert@npm:1.5.0"
@@ -5519,7 +5583,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"base64-js@npm:^1.0.2": "base64-js@npm:^1.0.2, base64-js@npm:^1.3.1":
version: 1.5.1 version: 1.5.1
resolution: "base64-js@npm:1.5.1" resolution: "base64-js@npm:1.5.1"
checksum: 669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 checksum: 669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005
@@ -5541,6 +5605,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "better-opn@npm:^2.1.1":
version: 2.1.1 version: 2.1.1
resolution: "better-opn@npm:2.1.1" resolution: "better-opn@npm:2.1.1"
@@ -5587,6 +5660,17 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "bluebird@npm:^3.5.5":
version: 3.7.2 version: 3.7.2
resolution: "bluebird@npm:3.7.2" resolution: "bluebird@npm:3.7.2"
@@ -5842,6 +5926,23 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "builtin-status-codes@npm:^3.0.0":
version: 3.0.0 version: 3.0.0
resolution: "builtin-status-codes@npm:3.0.0" resolution: "builtin-status-codes@npm:3.0.0"
@@ -6578,14 +6679,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"cookies-next@npm:^2.0.4": "cookies-next@npm:^2.1.1":
version: 2.0.4 version: 2.1.1
resolution: "cookies-next@npm:2.0.4" resolution: "cookies-next@npm:2.1.1"
dependencies: dependencies:
"@types/cookie": ^0.4.1 "@types/cookie": ^0.4.1
"@types/node": ^16.10.2 "@types/node": ^16.10.2
cookie: ^0.4.0 cookie: ^0.4.0
checksum: fc25b4215f2d7092d72f8591c9bc8b30f3ea866fca76e536e31825899c3f05eefb97cdb4152c565429cab38d20f2f937d08aea76a43d3cdd3ca36e24a347fe00 checksum: c5fc2c72cf2d46d6fa804e5690b5038bab3d5c7e741a8472079bfbd6920010802962f7512d999ea430ebcbfc7c89c38e16f423479e4df7cb0bb782cc1a7f9004
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6679,6 +6780,17 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "cpy@npm:^8.1.2":
version: 8.1.2 version: 8.1.2
resolution: "cpy@npm:8.1.2" resolution: "cpy@npm:8.1.2"
@@ -7243,6 +7355,28 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "doctrine@npm:^2.1.0":
version: 2.1.0 version: 2.1.0
resolution: "doctrine@npm:2.1.0" resolution: "doctrine@npm:2.1.0"
@@ -7466,7 +7600,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 1.4.4
resolution: "end-of-stream@npm:1.4.4" resolution: "end-of-stream@npm:1.4.4"
dependencies: dependencies:
@@ -8716,6 +8850,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "fs-extra@npm:^10.1.0":
version: 10.1.0 version: 10.1.0
resolution: "fs-extra@npm:10.1.0" resolution: "fs-extra@npm:10.1.0"
@@ -9440,20 +9581,22 @@ __metadata:
"@mantine/next": ^4.2.8 "@mantine/next": ^4.2.8
"@mantine/notifications": ^4.2.8 "@mantine/notifications": ^4.2.8
"@mantine/prism": ^4.2.8 "@mantine/prism": ^4.2.8
"@next/bundle-analyzer": ^12.1.4 "@next/bundle-analyzer": ^12.2.0
"@next/eslint-plugin-next": ^12.1.4 "@next/eslint-plugin-next": ^12.2.0
"@nivo/core": ^0.79.0 "@nivo/core": ^0.79.0
"@nivo/line": ^0.79.1 "@nivo/line": ^0.79.1
"@storybook/react": ^6.5.4 "@storybook/react": ^6.5.4
"@tabler/icons": ^1.68.0 "@tabler/icons": ^1.68.0
"@types/dockerode": ^3.3.9
"@types/node": ^17.0.23 "@types/node": ^17.0.23
"@types/react": 17.0.43 "@types/react": 17.0.43
"@types/uuid": ^8.3.4 "@types/uuid": ^8.3.4
"@typescript-eslint/eslint-plugin": ^5.16.0 "@typescript-eslint/eslint-plugin": ^5.16.0
"@typescript-eslint/parser": ^5.16.0 "@typescript-eslint/parser": ^5.16.0
axios: ^0.27.2 axios: ^0.27.2
cookies-next: ^2.0.4 cookies-next: ^2.1.1
dayjs: ^1.11.3 dayjs: ^1.11.3
dockerode: ^3.3.2
eslint: ^8.11.0 eslint: ^8.11.0
eslint-config-airbnb: ^19.0.4 eslint-config-airbnb: ^19.0.4
eslint-config-airbnb-typescript: ^16.1.0 eslint-config-airbnb-typescript: ^16.1.0
@@ -9469,7 +9612,7 @@ __metadata:
framer-motion: ^6.3.1 framer-motion: ^6.3.1
jest: ^28.1.0 jest: ^28.1.0
js-file-download: ^0.4.12 js-file-download: ^0.4.12
next: 12.1.6 next: ^12.2.0
prettier: ^2.6.2 prettier: ^2.6.2
prism-react-renderer: ^1.3.1 prism-react-renderer: ^1.3.1
react: ^17.0.1 react: ^17.0.1
@@ -9704,7 +9847,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ieee754@npm:^1.1.4": "ieee754@npm:^1.1.13, ieee754@npm:^1.1.4":
version: 1.2.1 version: 1.2.1
resolution: "ieee754@npm:1.2.1" resolution: "ieee754@npm:1.2.1"
checksum: 5144c0c9815e54ada181d80a0b810221a253562422e7c6c3a60b1901154184f49326ec239d618c416c1c5945a2e197107aee8d986a3dd836b53dffefd99b5e7e checksum: 5144c0c9815e54ada181d80a0b810221a253562422e7c6c3a60b1901154184f49326ec239d618c416c1c5945a2e197107aee8d986a3dd836b53dffefd99b5e7e
@@ -11841,6 +11984,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "mkdirp@npm:^0.5.1, mkdirp@npm:^0.5.3":
version: 0.5.6 version: 0.5.6
resolution: "mkdirp@npm:0.5.6" resolution: "mkdirp@npm:0.5.6"
@@ -11920,7 +12070,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 2.16.0
resolution: "nan@npm:2.16.0" resolution: "nan@npm:2.16.0"
dependencies: dependencies:
@@ -11985,26 +12135,29 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"next@npm:12.1.6": "next@npm:^12.2.0":
version: 12.1.6 version: 12.2.0
resolution: "next@npm:12.1.6" resolution: "next@npm:12.2.0"
dependencies: dependencies:
"@next/env": 12.1.6 "@next/env": 12.2.0
"@next/swc-android-arm-eabi": 12.1.6 "@next/swc-android-arm-eabi": 12.2.0
"@next/swc-android-arm64": 12.1.6 "@next/swc-android-arm64": 12.2.0
"@next/swc-darwin-arm64": 12.1.6 "@next/swc-darwin-arm64": 12.2.0
"@next/swc-darwin-x64": 12.1.6 "@next/swc-darwin-x64": 12.2.0
"@next/swc-linux-arm-gnueabihf": 12.1.6 "@next/swc-freebsd-x64": 12.2.0
"@next/swc-linux-arm64-gnu": 12.1.6 "@next/swc-linux-arm-gnueabihf": 12.2.0
"@next/swc-linux-arm64-musl": 12.1.6 "@next/swc-linux-arm64-gnu": 12.2.0
"@next/swc-linux-x64-gnu": 12.1.6 "@next/swc-linux-arm64-musl": 12.2.0
"@next/swc-linux-x64-musl": 12.1.6 "@next/swc-linux-x64-gnu": 12.2.0
"@next/swc-win32-arm64-msvc": 12.1.6 "@next/swc-linux-x64-musl": 12.2.0
"@next/swc-win32-ia32-msvc": 12.1.6 "@next/swc-win32-arm64-msvc": 12.2.0
"@next/swc-win32-x64-msvc": 12.1.6 "@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 caniuse-lite: ^1.0.30001332
postcss: 8.4.5 postcss: 8.4.5
styled-jsx: 5.0.2 styled-jsx: 5.0.2
use-sync-external-store: 1.1.0
peerDependencies: peerDependencies:
fibers: ">= 3.1.0" fibers: ">= 3.1.0"
node-sass: ^6.0.0 || ^7.0.0 node-sass: ^6.0.0 || ^7.0.0
@@ -12020,6 +12173,8 @@ __metadata:
optional: true optional: true
"@next/swc-darwin-x64": "@next/swc-darwin-x64":
optional: true optional: true
"@next/swc-freebsd-x64":
optional: true
"@next/swc-linux-arm-gnueabihf": "@next/swc-linux-arm-gnueabihf":
optional: true optional: true
"@next/swc-linux-arm64-gnu": "@next/swc-linux-arm64-gnu":
@@ -12045,7 +12200,7 @@ __metadata:
optional: true optional: true
bin: bin:
next: dist/bin/next next: dist/bin/next
checksum: 670d544fd47670c29681d10824e6da625e9d4a048e564c8d9cb80d37f33c9ff9b5ca0a53e6d84d8d618b1fe7c9bb4e6b45040cb7e57a5c46b232a8f914425dc1 checksum: 38456c33935122ac1581367e4982034be23269039a8470a66443d710334336f8f3fb587f25d172d138d84cf18c01d3a76627fb610c2e2e57bd1692277c23fa2b
languageName: node languageName: node
linkType: hard linkType: hard
@@ -13695,7 +13850,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 3.6.0
resolution: "readable-stream@npm:3.6.0" resolution: "readable-stream@npm:3.6.0"
dependencies: dependencies:
@@ -14178,7 +14333,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 2.1.2
resolution: "safer-buffer@npm:2.1.2" resolution: "safer-buffer@npm:2.1.2"
checksum: cab8f25ae6f1434abee8d80023d7e72b598cf1327164ddab31003c51215526801e40b66c5e65d658a0af1e9d6478cadcb4c745f4bd6751f97d8644786c0978b0 checksum: cab8f25ae6f1434abee8d80023d7e72b598cf1327164ddab31003c51215526801e40b66c5e65d658a0af1e9d6478cadcb4c745f4bd6751f97d8644786c0978b0
@@ -14659,6 +14814,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "split-string@npm:^3.0.1, split-string@npm:^3.0.2":
version: 3.1.0 version: 3.1.0
resolution: "split-string@npm:3.1.0" resolution: "split-string@npm:3.1.0"
@@ -14675,6 +14837,23 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "ssri@npm:^6.0.1":
version: 6.0.2 version: 6.0.2
resolution: "ssri@npm:6.0.2" resolution: "ssri@npm:6.0.2"
@@ -15125,6 +15304,31 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "tar@npm:^6.0.2, tar@npm:^6.1.11, tar@npm:^6.1.2":
version: 6.1.11 version: 6.1.11
resolution: "tar@npm:6.1.11" resolution: "tar@npm:6.1.11"
@@ -15479,7 +15683,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 2.4.0
resolution: "tslib@npm:2.4.0" resolution: "tslib@npm:2.4.0"
checksum: 8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113 checksum: 8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113
@@ -15504,6 +15708,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "type-check@npm:^0.4.0, type-check@npm:~0.4.0":
version: 0.4.0 version: 0.4.0
resolution: "type-check@npm:0.4.0" resolution: "type-check@npm:0.4.0"
@@ -15908,6 +16119,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "use@npm:^3.1.0":
version: 3.1.1 version: 3.1.1
resolution: "use@npm:3.1.1" resolution: "use@npm:3.1.1"