Skip to content

Commit ce0f27b

Browse files
committed
v0.8.0 Docker πŸ‹ and Dash. βš™ integrations !
<!-- Small release message --> - ✨ Add support for lists in module option by @ajnart in #280 - πŸ”¨ Fix Readarr default port number by @Moohan in #287 - ✨ Add dash. Integration by @MauriceNino in #277 - ✨ Add Docker integration by @ajnart in #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 #287 * @MauriceNino made their first contribution in #277 **Full Changelog**: v0.7.2...v0.8.0
2 parents aab1492 + 5c1a171 commit ce0f27b

31 files changed

+1397
-255
lines changed

β€Ždata/constants.tsβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export const REPO_URL = 'ajnart/homarr';
2-
export const CURRENT_VERSION = 'v0.7.2';
2+
export const CURRENT_VERSION = 'v0.8.0';

β€Žnext.config.jsβ€Ž

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ module.exports = withBundleAnalyzer({
99
eslint: {
1010
ignoreDuringBuilds: true,
1111
},
12-
experimental: {
13-
outputStandalone: true,
14-
},
12+
output: 'standalone',
1513
basePath: env.BASE_URL,
1614
});

β€Žpackage.jsonβ€Ž

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "homarr",
3-
"version": "0.7.2",
3+
"version": "0.8.0",
44
"description": "Homarr - A homepage for your server.",
55
"repository": {
66
"type": "git",
@@ -43,11 +43,12 @@
4343
"@nivo/line": "^0.79.1",
4444
"@tabler/icons": "^1.68.0",
4545
"axios": "^0.27.2",
46-
"cookies-next": "^2.0.4",
46+
"cookies-next": "^2.1.1",
4747
"dayjs": "^1.11.3",
48+
"dockerode": "^3.3.2",
4849
"framer-motion": "^6.3.1",
4950
"js-file-download": "^0.4.12",
50-
"next": "12.1.6",
51+
"next": "^12.2.0",
5152
"prism-react-renderer": "^1.3.1",
5253
"react": "^17.0.1",
5354
"react-dom": "^17.0.1",
@@ -56,9 +57,10 @@
5657
},
5758
"devDependencies": {
5859
"@babel/core": "^7.17.8",
59-
"@next/bundle-analyzer": "^12.1.4",
60-
"@next/eslint-plugin-next": "^12.1.4",
60+
"@next/bundle-analyzer": "^12.2.0",
61+
"@next/eslint-plugin-next": "^12.2.0",
6162
"@storybook/react": "^6.5.4",
63+
"@types/dockerode": "^3.3.9",
6264
"@types/node": "^17.0.23",
6365
"@types/react": "17.0.43",
6466
"@types/uuid": "^8.3.4",

β€Žsrc/components/AppShelf/AddAppShelfItem.tsxβ€Ž

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
11
import {
2-
Modal,
2+
ActionIcon,
3+
Anchor,
4+
Button,
35
Center,
46
Group,
5-
TextInput,
67
Image,
7-
Button,
8-
Select,
98
LoadingOverlay,
10-
ActionIcon,
11-
Tooltip,
12-
Title,
13-
Anchor,
14-
Text,
15-
Tabs,
9+
Modal,
1610
MultiSelect,
1711
ScrollArea,
12+
Select,
1813
Switch,
14+
Tabs,
15+
Text,
16+
TextInput,
17+
Title,
18+
Tooltip,
1919
} from '@mantine/core';
2020
import { useForm } from '@mantine/form';
21-
import { useEffect, useState } from 'react';
21+
import { useDebouncedValue } from '@mantine/hooks';
2222
import { IconApps as Apps } from '@tabler/icons';
23+
import { useEffect, useState } from 'react';
2324
import { v4 as uuidv4 } from 'uuid';
24-
import { useDebouncedValue } from '@mantine/hooks';
2525
import { useConfig } from '../../tools/state';
2626
import { ServiceTypeList, StatusCodes } from '../../tools/types';
27+
import Tip from '../layout/Tip';
2728

2829
export function AddItemShelfButton(props: any) {
2930
const [opened, setOpened] = useState(false);
@@ -58,7 +59,8 @@ function MatchIcon(name: string, form: any) {
5859
fetch(
5960
`https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name
6061
.replace(/\s+/g, '-')
61-
.toLowerCase()}.png`
62+
.toLowerCase()
63+
.replace(/^dash\.$/, 'dashdot')}.png`
6264
).then((res) => {
6365
if (res.ok) {
6466
form.setFieldValue('icon', res.url);
@@ -81,9 +83,10 @@ function MatchPort(name: string, form: any) {
8183
{ name: 'sonarr', value: '8989' },
8284
{ name: 'radarr', value: '7878' },
8385
{ name: 'lidarr', value: '8686' },
84-
{ name: 'readarr', value: '8686' },
86+
{ name: 'readarr', value: '8787' },
8587
{ name: 'deluge', value: '8112' },
8688
{ name: 'transmission', value: '9091' },
89+
{ name: 'dash.', value: '3001' },
8790
];
8891
// Match name with portmap key
8992
const port = portmap.find((p) => p.name === name.toLowerCase());
@@ -92,6 +95,8 @@ function MatchPort(name: string, form: any) {
9295
}
9396
}
9497

98+
const DEFAULT_ICON = '/favicon.svg';
99+
95100
export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & any) {
96101
const { setOpened } = props;
97102
const { config, setConfig } = useConfig();
@@ -111,7 +116,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
111116
type: props.type ?? 'Other',
112117
category: props.category ?? undefined,
113118
name: props.name ?? '',
114-
icon: props.icon ?? '/favicon.svg',
119+
icon: props.icon ?? DEFAULT_ICON,
115120
url: props.url ?? '',
116121
apiKey: props.apiKey ?? (undefined as unknown as string),
117122
username: props.username ?? (undefined as unknown as string),
@@ -146,7 +151,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
146151

147152
const [debounced, cancel] = useDebouncedValue(form.values.name, 250);
148153
useEffect(() => {
149-
if (form.values.name !== debounced || props.name || props.type) return;
154+
if (form.values.name !== debounced || form.values.icon !== DEFAULT_ICON) return;
150155
MatchIcon(form.values.name, form);
151156
MatchService(form.values.name, form);
152157
MatchPort(form.values.name, form);
@@ -219,7 +224,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
219224
<TextInput
220225
required
221226
label="Icon URL"
222-
placeholder="/favicon.svg"
227+
placeholder={DEFAULT_ICON}
223228
{...form.getInputProps('icon')}
224229
/>
225230
<TextInput
@@ -273,15 +278,8 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
273278
}}
274279
error={form.errors.apiKey && 'Invalid API key'}
275280
/>
276-
<Text
277-
style={{
278-
alignSelf: 'center',
279-
fontSize: '0.75rem',
280-
textAlign: 'center',
281-
color: 'gray',
282-
}}
283-
>
284-
Tip: Get your API key{' '}
281+
<Tip>
282+
Get your API key{' '}
285283
<Anchor
286284
target="_blank"
287285
weight="bold"
@@ -290,7 +288,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
290288
>
291289
here.
292290
</Anchor>
293-
</Text>
291+
</Tip>
294292
</>
295293
)}
296294
{form.values.type === 'qBittorrent' && (

β€Žsrc/components/AppShelf/AppShelf.tsxβ€Ž

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ const AppShelf = (props: any) => {
152152
const noCategory = config.services.filter(
153153
(e) => e.category === undefined || e.category === null
154154
);
155+
const downloadEnabled = config.modules?.[DownloadsModule.title]?.enabled ?? false;
155156
// Create an item with 0: true, 1: true, 2: true... For each category
156157
return (
157158
// Return one item for each category
@@ -176,6 +177,7 @@ const AppShelf = (props: any) => {
176177
{item()}
177178
</Accordion.Item>
178179
) : null}
180+
{downloadEnabled ? (
179181
<Accordion.Item key="Downloads" label="Your downloads">
180182
<Paper
181183
p="lg"
@@ -191,6 +193,7 @@ const AppShelf = (props: any) => {
191193
<DownloadComponent />
192194
</Paper>
193195
</Accordion.Item>
196+
) : null}
194197
</Accordion>
195198
</Group>
196199
);
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { Button, Group, Modal, Title } from '@mantine/core';
2+
import { useBooleanToggle } from '@mantine/hooks';
3+
import { showNotification, updateNotification } from '@mantine/notifications';
4+
import {
5+
IconCheck,
6+
IconPlayerPlay,
7+
IconPlayerStop,
8+
IconPlus,
9+
IconRefresh,
10+
IconRotateClockwise,
11+
IconTrash,
12+
IconX,
13+
} from '@tabler/icons';
14+
import axios from 'axios';
15+
import Dockerode from 'dockerode';
16+
import { tryMatchService } from '../../tools/addToHomarr';
17+
import { useConfig } from '../../tools/state';
18+
import { AddAppShelfItemForm } from '../AppShelf/AddAppShelfItem';
19+
20+
function sendDockerCommand(action: string, containerId: string, containerName: string) {
21+
showNotification({
22+
id: containerId,
23+
loading: true,
24+
title: `${action}ing container ${containerName.substring(1)}`,
25+
message: undefined,
26+
autoClose: false,
27+
disallowClose: true,
28+
});
29+
axios.get(`/api/docker/container/${containerId}?action=${action}`).then((res) => {
30+
setTimeout(() => {
31+
if (res.data.success === true) {
32+
updateNotification({
33+
id: containerId,
34+
title: `Container ${containerName} ${action}ed`,
35+
message: `Your container was successfully ${action}ed`,
36+
icon: <IconCheck />,
37+
autoClose: 2000,
38+
});
39+
}
40+
if (res.data.success === false) {
41+
updateNotification({
42+
id: containerId,
43+
color: 'red',
44+
title: 'There was an error with your container.',
45+
message: undefined,
46+
icon: <IconX />,
47+
autoClose: 2000,
48+
});
49+
}
50+
}, 500);
51+
});
52+
}
53+
54+
export interface ContainerActionBarProps {
55+
selected: Dockerode.ContainerInfo[];
56+
reload: () => void;
57+
}
58+
59+
export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) {
60+
const { config, setConfig } = useConfig();
61+
const [opened, setOpened] = useBooleanToggle(false);
62+
return (
63+
<Group>
64+
<Modal
65+
size="xl"
66+
radius="md"
67+
opened={opened}
68+
onClose={() => setOpened(false)}
69+
title="Add service"
70+
>
71+
<AddAppShelfItemForm
72+
setOpened={setOpened}
73+
{...tryMatchService(selected.at(0))}
74+
message="Add service to homarr"
75+
/>
76+
</Modal>
77+
<Button
78+
leftIcon={<IconRotateClockwise />}
79+
onClick={() =>
80+
Promise.all(
81+
selected.map((container) =>
82+
sendDockerCommand('restart', container.Id, container.Names[0].substring(1))
83+
)
84+
).then(() => reload())
85+
}
86+
variant="light"
87+
color="orange"
88+
radius="md"
89+
>
90+
Restart
91+
</Button>
92+
<Button
93+
leftIcon={<IconPlayerStop />}
94+
onClick={() =>
95+
Promise.all(
96+
selected.map((container) => {
97+
if (
98+
container.State === 'stopped' ||
99+
container.State === 'created' ||
100+
container.State === 'exited'
101+
) {
102+
return showNotification({
103+
id: container.Id,
104+
title: `Failed to stop ${container.Names[0].substring(1)}`,
105+
message: "You can't stop a stopped container",
106+
autoClose: 1000,
107+
});
108+
}
109+
return sendDockerCommand('stop', container.Id, container.Names[0].substring(1));
110+
})
111+
).then(() => reload())
112+
}
113+
variant="light"
114+
color="red"
115+
radius="md"
116+
>
117+
Stop
118+
</Button>
119+
<Button
120+
leftIcon={<IconPlayerPlay />}
121+
onClick={() =>
122+
Promise.all(
123+
selected.map((container) =>
124+
sendDockerCommand('start', container.Id, container.Names[0].substring(1))
125+
)
126+
).then(() => reload())
127+
}
128+
variant="light"
129+
color="green"
130+
radius="md"
131+
>
132+
Start
133+
</Button>
134+
<Button leftIcon={<IconRefresh />} onClick={() => reload()} variant="light" radius="md">
135+
Refresh data
136+
</Button>
137+
<Button
138+
leftIcon={<IconPlus />}
139+
color="indigo"
140+
variant="light"
141+
radius="md"
142+
onClick={() => {
143+
if (selected.length !== 1) {
144+
showNotification({
145+
autoClose: 5000,
146+
title: <Title order={4}>Please only add one service at a time!</Title>,
147+
color: 'red',
148+
message: undefined,
149+
});
150+
} else {
151+
setOpened(true);
152+
}
153+
}}
154+
>
155+
Add to Homarr
156+
</Button>
157+
<Button
158+
leftIcon={<IconTrash />}
159+
color="red"
160+
variant="light"
161+
radius="md"
162+
onClick={() =>
163+
Promise.all(
164+
selected.map((container) => {
165+
if (container.State === 'running') {
166+
return showNotification({
167+
id: container.Id,
168+
title: `Failed to delete ${container.Names[0].substring(1)}`,
169+
message: "You can't delete a running container",
170+
autoClose: 1000,
171+
});
172+
}
173+
return sendDockerCommand('remove', container.Id, container.Names[0].substring(1));
174+
})
175+
).then(() => reload())
176+
}
177+
>
178+
Remove
179+
</Button>
180+
</Group>
181+
);
182+
}

0 commit comments

Comments
Β (0)