mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-09 15:05:48 +01:00
🎨 Rename "services" to "apps" in entire project
This commit is contained in:
@@ -1,20 +1,196 @@
|
|||||||
{
|
{
|
||||||
"name": "default",
|
"schemaVersion": "1.0",
|
||||||
"services": [
|
"configProperties": {
|
||||||
|
"name": "default"
|
||||||
|
},
|
||||||
|
"categories": [],
|
||||||
|
"wrappers": [
|
||||||
{
|
{
|
||||||
"name": "example",
|
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e",
|
||||||
"id": "09c45847-8afc-4c1a-9697-f03192de948a",
|
"position": 1
|
||||||
"type": "Other",
|
|
||||||
"icon": "https://c.tenor.com/o656qFKDzeUAAAAC/rick-astley-never-gonna-give-you-up.gif",
|
|
||||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"apps": [
|
||||||
"searchUrl": "https://google.com/search?q="
|
{
|
||||||
|
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33a",
|
||||||
|
"name": "Documentation",
|
||||||
|
"url": "https://homarr.dev",
|
||||||
|
"behaviour": {
|
||||||
|
"onClickUrl": "https://homarr.dev",
|
||||||
|
"isOpeningInNewTab": true
|
||||||
},
|
},
|
||||||
"modules": {
|
"network": {
|
||||||
"Search Bar": {
|
"enabledStatusChecker": false,
|
||||||
"enabled": true
|
"okStatus": [
|
||||||
|
200
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"iconUrl": "/imgs/logo/logo.png"
|
||||||
|
},
|
||||||
|
"integration": {
|
||||||
|
"type": null,
|
||||||
|
"properties": []
|
||||||
|
},
|
||||||
|
"area": {
|
||||||
|
"type": "wrapper",
|
||||||
|
"properties": {
|
||||||
|
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shape": {
|
||||||
|
"location": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 3,
|
||||||
|
"height": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a330",
|
||||||
|
"name": "Support",
|
||||||
|
"url": "https://github.com/ajnart/homarr",
|
||||||
|
"behaviour": {
|
||||||
|
"onClickUrl": "https://github.com/ajnart/homarr",
|
||||||
|
"isOpeningInNewTab": true
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"enabledStatusChecker": false,
|
||||||
|
"okStatus": [
|
||||||
|
200
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/github.png"
|
||||||
|
},
|
||||||
|
"integration": {
|
||||||
|
"type": null,
|
||||||
|
"properties": []
|
||||||
|
},
|
||||||
|
"area": {
|
||||||
|
"type": "wrapper",
|
||||||
|
"properties": {
|
||||||
|
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shape": {
|
||||||
|
"location": {
|
||||||
|
"x": 3,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 3,
|
||||||
|
"height": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337",
|
||||||
|
"name": "Community",
|
||||||
|
"url": "https://discord.com/invite/aCsmEV5RgA",
|
||||||
|
"behaviour": {
|
||||||
|
"onClickUrl": "https://discord.com/invite/aCsmEV5RgA",
|
||||||
|
"isOpeningInNewTab": true
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"enabledStatusChecker": false,
|
||||||
|
"okStatus": [
|
||||||
|
200
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/discord.png"
|
||||||
|
},
|
||||||
|
"integration": {
|
||||||
|
"type": null,
|
||||||
|
"properties": []
|
||||||
|
},
|
||||||
|
"area": {
|
||||||
|
"type": "wrapper",
|
||||||
|
"properties": {
|
||||||
|
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shape": {
|
||||||
|
"location": {
|
||||||
|
"x": 6,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 3,
|
||||||
|
"height": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a990",
|
||||||
|
"name": "Donate",
|
||||||
|
"url": "https://homarr.dev/docs/community/donate",
|
||||||
|
"behaviour": {
|
||||||
|
"onClickUrl": "https://homarr.dev/docs/community/donate",
|
||||||
|
"isOpeningInNewTab": true
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"enabledStatusChecker": false,
|
||||||
|
"okStatus": [
|
||||||
|
200
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"iconUrl": "/imgs/logo/logo.png"
|
||||||
|
},
|
||||||
|
"integration": {
|
||||||
|
"type": null,
|
||||||
|
"properties": []
|
||||||
|
},
|
||||||
|
"area": {
|
||||||
|
"type": "wrapper",
|
||||||
|
"properties": {
|
||||||
|
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shape": {
|
||||||
|
"location": {
|
||||||
|
"x": 9,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 3,
|
||||||
|
"height": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"integrations": [],
|
||||||
|
"settings": {
|
||||||
|
"common": {
|
||||||
|
"searchEngine": {
|
||||||
|
"type": "google",
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"customization": {
|
||||||
|
"layout": {
|
||||||
|
"enabledLeftSidebar": false,
|
||||||
|
"enabledRightSidebar": false,
|
||||||
|
"enabledDocker": false,
|
||||||
|
"enabledPing": false,
|
||||||
|
"enabledSearchbar": true
|
||||||
|
},
|
||||||
|
"pageTitle": "Homarr",
|
||||||
|
"logoImageUrl": "/imgs/logo/logo.png",
|
||||||
|
"faviconUrl": "/imgs/logo/logo.png",
|
||||||
|
"backgroundImageUrl": "",
|
||||||
|
"customCss": "",
|
||||||
|
"colors": {
|
||||||
|
"primary": "red",
|
||||||
|
"secondary": "orange",
|
||||||
|
"shade": 5
|
||||||
|
},
|
||||||
|
"appOpacity": 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ export const AboutModal = ({ opened, closeModal }: AboutModalProps) => {
|
|||||||
>
|
>
|
||||||
<Text mb="lg">
|
<Text mb="lg">
|
||||||
Homarr is a simple and modern homepage for your server that helps you access all of your
|
Homarr is a simple and modern homepage for your server that helps you access all of your
|
||||||
services in one place. It integrates with the services you use to display useful information
|
apps in one place. It integrates with the apps you use to display useful information
|
||||||
or control them. It's easy to install and supports many different devices.
|
or control them. It's easy to install and supports many different devices.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { UsenetModule, TorrentsModule } from '../../modules';
|
|||||||
const AppShelf = (props: any) => {
|
const AppShelf = (props: any) => {
|
||||||
const { config, setConfig } = useConfig();
|
const { config, setConfig } = useConfig();
|
||||||
// Extract all the categories from the services in config
|
// Extract all the categories from the services in config
|
||||||
const categoryList = config.services.reduce((acc, cur) => {
|
const categoryList = config.apps.reduce((acc, cur) => {
|
||||||
if (cur.category && !acc.includes(cur.category)) {
|
if (cur.category && !acc.includes(cur.category)) {
|
||||||
acc.push(cur.category);
|
acc.push(cur.category);
|
||||||
}
|
}
|
||||||
@@ -67,9 +67,9 @@ const AppShelf = (props: any) => {
|
|||||||
|
|
||||||
if (active.id !== over.id) {
|
if (active.id !== over.id) {
|
||||||
const newConfig = { ...config };
|
const newConfig = { ...config };
|
||||||
const activeIndex = newConfig.services.findIndex((e) => e.id === active.id);
|
const activeIndex = newConfig.apps.findIndex((e) => e.id === active.id);
|
||||||
const overIndex = newConfig.services.findIndex((e) => e.id === over.id);
|
const overIndex = newConfig.apps.findIndex((e) => e.id === over.id);
|
||||||
newConfig.services = arrayMove(newConfig.services, activeIndex, overIndex);
|
newConfig.apps = arrayMove(newConfig.apps, activeIndex, overIndex);
|
||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,14 +78,14 @@ const AppShelf = (props: any) => {
|
|||||||
|
|
||||||
const getItems = (filter?: string) => {
|
const getItems = (filter?: string) => {
|
||||||
// If filter is not set, return all the services without a category or a null category
|
// If filter is not set, return all the services without a category or a null category
|
||||||
let filtered = config.services;
|
let filtered = config.apps;
|
||||||
const modules = Object.values(Modules).map((module) => module);
|
const modules = Object.values(Modules).map((module) => module);
|
||||||
|
|
||||||
if (!filter) {
|
if (!filter) {
|
||||||
filtered = config.services.filter((e) => !e.category || e.category === null);
|
filtered = config.apps.filter((e) => !e.category || e.category === null);
|
||||||
}
|
}
|
||||||
if (filter) {
|
if (filter) {
|
||||||
filtered = config.services.filter((e) => e.category === filter);
|
filtered = config.apps.filter((e) => e.category === filter);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<DndContext
|
<DndContext
|
||||||
@@ -94,7 +94,7 @@ const AppShelf = (props: any) => {
|
|||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
<SortableContext items={config.services}>
|
<SortableContext items={config.apps}>
|
||||||
<Grid gutter="lg" grow={config.settings.grow}>
|
<Grid gutter="lg" grow={config.settings.grow}>
|
||||||
{filtered.map((service) => (
|
{filtered.map((service) => (
|
||||||
<Grid.Col key={service.id} span="content">
|
<Grid.Col key={service.id} span="content">
|
||||||
@@ -112,7 +112,7 @@ const AppShelf = (props: any) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{activeId ? (
|
{activeId ? (
|
||||||
<AppShelfItem service={config.services.find((e) => e.id === activeId)} id={activeId} />
|
<AppShelfItem service={config.apps.find((e) => e.id === activeId)} id={activeId} />
|
||||||
) : null}
|
) : null}
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Avatar, Group, Text } from '@mantine/core';
|
|
||||||
|
|
||||||
interface smallServiceItem {
|
|
||||||
label: string;
|
|
||||||
icon?: string;
|
|
||||||
url?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SmallServiceItem(props: any) {
|
|
||||||
const { service }: { service: smallServiceItem } = props;
|
|
||||||
// TODO : Use Next/link
|
|
||||||
return (
|
|
||||||
<Group>
|
|
||||||
{service.icon && <Avatar src={service.icon} />}
|
|
||||||
<Text>{service.label}</Text>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,18 +2,18 @@ import { SelectItem } from '@mantine/core';
|
|||||||
import { closeModal, ContextModalProps } from '@mantine/modals';
|
import { closeModal, ContextModalProps } from '@mantine/modals';
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
import { ServiceType } from '../../../../types/service';
|
import { AppType } from '../../../../types/app';
|
||||||
import { ChangePositionModal } from './ChangePositionModal';
|
import { ChangePositionModal } from './ChangePositionModal';
|
||||||
|
|
||||||
type ChangeServicePositionModalInnerProps = {
|
type ChangeAppPositionModalInnerProps = {
|
||||||
service: ServiceType;
|
app: AppType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChangeServicePositionModal = ({
|
export const ChangeAppPositionModal = ({
|
||||||
id,
|
id,
|
||||||
context,
|
context,
|
||||||
innerProps,
|
innerProps,
|
||||||
}: ContextModalProps<ChangeServicePositionModalInnerProps>) => {
|
}: ContextModalProps<ChangeAppPositionModalInnerProps>) => {
|
||||||
const { name: configName } = useConfigContext();
|
const { name: configName } = useConfigContext();
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||||
|
|
||||||
@@ -24,9 +24,9 @@ export const ChangeServicePositionModal = ({
|
|||||||
|
|
||||||
updateConfig(configName, (previousConfig) => ({
|
updateConfig(configName, (previousConfig) => ({
|
||||||
...previousConfig,
|
...previousConfig,
|
||||||
services: [
|
apps: [
|
||||||
...previousConfig.services.filter((x) => x.id !== innerProps.service.id),
|
...previousConfig.apps.filter((x) => x.id !== innerProps.app.id),
|
||||||
{ ...innerProps.service, shape: { location: { x, y }, size: { width, height } } },
|
{ ...innerProps.app, shape: { location: { x, y }, size: { width, height } } },
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
context.closeModal(id);
|
context.closeModal(id);
|
||||||
@@ -45,10 +45,10 @@ export const ChangeServicePositionModal = ({
|
|||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
widthData={widthData}
|
widthData={widthData}
|
||||||
heightData={heightData}
|
heightData={heightData}
|
||||||
initialX={innerProps.service.shape.location.x}
|
initialX={innerProps.app.shape.location.x}
|
||||||
initialY={innerProps.service.shape.location.y}
|
initialY={innerProps.app.shape.location.y}
|
||||||
initialWidth={innerProps.service.shape.size.width}
|
initialWidth={innerProps.app.shape.size.width}
|
||||||
initialHeight={innerProps.service.shape.size.height}
|
initialHeight={innerProps.app.shape.size.height}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -13,32 +13,32 @@ import { useTranslation } from 'next-i18next';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
import { ServiceType } from '../../../../types/service';
|
import { AppType } from '../../../../types/app';
|
||||||
import { AppearanceTab } from './Tabs/AppereanceTab/AppereanceTab';
|
import { AppearanceTab } from './Tabs/AppereanceTab/AppereanceTab';
|
||||||
import { BehaviourTab } from './Tabs/BehaviourTab/BehaviourTab';
|
import { BehaviourTab } from './Tabs/BehaviourTab/BehaviourTab';
|
||||||
import { GeneralTab } from './Tabs/GeneralTab/GeneralTab';
|
import { GeneralTab } from './Tabs/GeneralTab/GeneralTab';
|
||||||
import { IntegrationTab } from './Tabs/IntegrationTab/IntegrationTab';
|
import { IntegrationTab } from './Tabs/IntegrationTab/IntegrationTab';
|
||||||
import { NetworkTab } from './Tabs/NetworkTab/NetworkTab';
|
import { NetworkTab } from './Tabs/NetworkTab/NetworkTab';
|
||||||
import { DebouncedServiceIcon } from './Tabs/Shared/DebouncedServiceIcon';
|
import { DebouncedAppIcon } from './Tabs/Shared/DebouncedAppIcon';
|
||||||
import { EditServiceModalTab } from './Tabs/type';
|
import { EditAppModalTab } from './Tabs/type';
|
||||||
|
|
||||||
const serviceUrlRegex =
|
const appUrlRegex =
|
||||||
'(https?://(?:www.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^\\s]{2,}|www.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^\\s]{2,}|https?://(?:www.|(?!www))[a-zA-Z0-9]+.[^\\s]{2,}|www.[a-zA-Z0-9]+.[^\\s]{2,})';
|
'(https?://(?:www.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^\\s]{2,}|www.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^\\s]{2,}|https?://(?:www.|(?!www))[a-zA-Z0-9]+.[^\\s]{2,}|www.[a-zA-Z0-9]+.[^\\s]{2,})';
|
||||||
|
|
||||||
export const EditServiceModal = ({
|
export const EditAppModal = ({
|
||||||
context,
|
context,
|
||||||
id,
|
id,
|
||||||
innerProps,
|
innerProps,
|
||||||
}: ContextModalProps<{ service: ServiceType; allowServiceNamePropagation: boolean }>) => {
|
}: ContextModalProps<{ app: AppType; allowAppNamePropagation: boolean }>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { name: configName, config } = useConfigContext();
|
const { name: configName, config } = useConfigContext();
|
||||||
const updateConfig = useConfigStore((store) => store.updateConfig);
|
const updateConfig = useConfigStore((store) => store.updateConfig);
|
||||||
const [allowServiceNamePropagation, setAllowServiceNamePropagation] = useState<boolean>(
|
const [allowAppNamePropagation, setAllowAppNamePropagation] = useState<boolean>(
|
||||||
innerProps.allowServiceNamePropagation
|
innerProps.allowAppNamePropagation
|
||||||
);
|
);
|
||||||
|
|
||||||
const form = useForm<ServiceType>({
|
const form = useForm<AppType>({
|
||||||
initialValues: innerProps.service,
|
initialValues: innerProps.app,
|
||||||
validate: {
|
validate: {
|
||||||
name: (name) => (!name ? 'Name is required' : null),
|
name: (name) => (!name ? 'Name is required' : null),
|
||||||
url: (url) => {
|
url: (url) => {
|
||||||
@@ -46,7 +46,7 @@ export const EditServiceModal = ({
|
|||||||
return 'Url is required';
|
return 'Url is required';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!url.match(serviceUrlRegex)) {
|
if (!url.match(appUrlRegex)) {
|
||||||
return 'Value is not a valid url';
|
return 'Value is not a valid url';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ export const EditServiceModal = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!url.match(serviceUrlRegex)) {
|
if (!url.match(appUrlRegex)) {
|
||||||
return 'Uri override is not a valid uri';
|
return 'Uri override is not a valid uri';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,21 +78,21 @@ export const EditServiceModal = ({
|
|||||||
validateInputOnChange: true,
|
validateInputOnChange: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (values: ServiceType) => {
|
const onSubmit = (values: AppType) => {
|
||||||
if (!configName) {
|
if (!configName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateConfig(configName, (previousConfig) => ({
|
updateConfig(configName, (previousConfig) => ({
|
||||||
...previousConfig,
|
...previousConfig,
|
||||||
services: [...previousConfig.services.filter((x) => x.id !== form.values.id), form.values],
|
apps: [...previousConfig.apps.filter((x) => x.id !== form.values.id), form.values],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// also close the parent modal
|
// also close the parent modal
|
||||||
context.closeAll();
|
context.closeAll();
|
||||||
};
|
};
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<EditServiceModalTab>('general');
|
const [activeTab, setActiveTab] = useState<EditAppModalTab>('general');
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
context.closeModal(id);
|
context.closeModal(id);
|
||||||
@@ -125,17 +125,17 @@ export const EditServiceModal = ({
|
|||||||
</Alert>
|
</Alert>
|
||||||
))}
|
))}
|
||||||
<Stack spacing={0} align="center" my="lg">
|
<Stack spacing={0} align="center" my="lg">
|
||||||
<DebouncedServiceIcon form={form} width={120} height={120} />
|
<DebouncedAppIcon form={form} width={120} height={120} />
|
||||||
|
|
||||||
<Text align="center" weight="bold" size="lg" mt="md">
|
<Text align="center" weight="bold" size="lg" mt="md">
|
||||||
{form.values.name ?? 'New Service'}
|
{form.values.name ?? 'New App'}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onTabChange={(tab) => setActiveTab(tab as EditServiceModalTab)}
|
onTabChange={(tab) => setActiveTab(tab as EditAppModalTab)}
|
||||||
defaultValue="general"
|
defaultValue="general"
|
||||||
>
|
>
|
||||||
<Tabs.List grow>
|
<Tabs.List grow>
|
||||||
@@ -181,8 +181,8 @@ export const EditServiceModal = ({
|
|||||||
<NetworkTab form={form} />
|
<NetworkTab form={form} />
|
||||||
<AppearanceTab
|
<AppearanceTab
|
||||||
form={form}
|
form={form}
|
||||||
disallowServiceNameProgagation={() => setAllowServiceNamePropagation(false)}
|
disallowAppNameProgagation={() => setAllowAppNamePropagation(false)}
|
||||||
allowServiceNamePropagation={allowServiceNamePropagation}
|
allowAppNamePropagation={allowAppNamePropagation}
|
||||||
/>
|
/>
|
||||||
<IntegrationTab form={form} />
|
<IntegrationTab form={form} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -199,9 +199,3 @@ export const EditServiceModal = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const useStyles = createStyles(() => ({
|
|
||||||
serviceImage: {
|
|
||||||
objectFit: 'contain',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
import { createStyles, Flex, Tabs, TextInput } from '@mantine/core';
|
import { createStyles, Flex, Tabs, TextInput } from '@mantine/core';
|
||||||
import { UseFormReturnType } from '@mantine/form';
|
import { UseFormReturnType } from '@mantine/form';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { ServiceType } from '../../../../../../types/service';
|
import { AppType } from '../../../../../../types/app';
|
||||||
import { DebouncedServiceIcon } from '../Shared/DebouncedServiceIcon';
|
import { DebouncedAppIcon } from '../Shared/DebouncedAppIcon';
|
||||||
import { IconSelector } from './IconSelector/IconSelector';
|
import { IconSelector } from './IconSelector/IconSelector';
|
||||||
|
|
||||||
interface AppearanceTabProps {
|
interface AppearanceTabProps {
|
||||||
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
form: UseFormReturnType<AppType, (values: AppType) => AppType>;
|
||||||
disallowServiceNameProgagation: () => void;
|
disallowAppNameProgagation: () => void;
|
||||||
allowServiceNamePropagation: boolean;
|
allowAppNamePropagation: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppearanceTab = ({
|
export const AppearanceTab = ({
|
||||||
form,
|
form,
|
||||||
disallowServiceNameProgagation,
|
disallowAppNameProgagation,
|
||||||
allowServiceNamePropagation,
|
allowAppNamePropagation,
|
||||||
}: AppearanceTabProps) => {
|
}: AppearanceTabProps) => {
|
||||||
const { t } = useTranslation('');
|
const { t } = useTranslation('');
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
@@ -25,9 +25,9 @@ export const AppearanceTab = ({
|
|||||||
<TextInput
|
<TextInput
|
||||||
defaultValue={form.values.appearance.iconUrl}
|
defaultValue={form.values.appearance.iconUrl}
|
||||||
className={classes.textInput}
|
className={classes.textInput}
|
||||||
icon={<DebouncedServiceIcon form={form} width={20} height={20} />}
|
icon={<DebouncedAppIcon form={form} width={20} height={20} />}
|
||||||
label="Service Icon"
|
label="App Icon"
|
||||||
description="Logo of your service displayed in your dashboard. Must return a body content containg an image"
|
description="Logo of your app displayed in your dashboard. Must return a body content containg an image"
|
||||||
variant="default"
|
variant="default"
|
||||||
withAsterisk
|
withAsterisk
|
||||||
required
|
required
|
||||||
@@ -40,9 +40,9 @@ export const AppearanceTab = ({
|
|||||||
iconUrl: item.url,
|
iconUrl: item.url,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
disallowServiceNameProgagation();
|
disallowAppNameProgagation();
|
||||||
}}
|
}}
|
||||||
allowServiceNamePropagation={allowServiceNamePropagation}
|
allowAppNamePropagation={allowAppNamePropagation}
|
||||||
form={form}
|
form={form}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -21,17 +21,17 @@ import { ICON_PICKER_SLICE_LIMIT } from '../../../../../../../../data/constants'
|
|||||||
import { useRepositoryIconsQuery } from '../../../../../../../tools/hooks/useRepositoryIconsQuery';
|
import { useRepositoryIconsQuery } from '../../../../../../../tools/hooks/useRepositoryIconsQuery';
|
||||||
import { IconSelectorItem } from '../../../../../../../types/iconSelector/iconSelectorItem';
|
import { IconSelectorItem } from '../../../../../../../types/iconSelector/iconSelectorItem';
|
||||||
import { WalkxcodeRepositoryIcon } from '../../../../../../../types/iconSelector/repositories/walkxcodeIconRepository';
|
import { WalkxcodeRepositoryIcon } from '../../../../../../../types/iconSelector/repositories/walkxcodeIconRepository';
|
||||||
import { ServiceType } from '../../../../../../../types/service';
|
import { AppType } from '../../../../../../../types/app';
|
||||||
|
|
||||||
interface IconSelectorProps {
|
interface IconSelectorProps {
|
||||||
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
form: UseFormReturnType<AppType, (values: AppType) => AppType>;
|
||||||
onChange: (icon: IconSelectorItem) => void;
|
onChange: (icon: IconSelectorItem) => void;
|
||||||
allowServiceNamePropagation: boolean;
|
allowAppNamePropagation: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IconSelector = ({
|
export const IconSelector = ({
|
||||||
onChange,
|
onChange,
|
||||||
allowServiceNamePropagation,
|
allowAppNamePropagation,
|
||||||
form,
|
form,
|
||||||
}: IconSelectorProps) => {
|
}: IconSelectorProps) => {
|
||||||
const { data, isLoading } = useRepositoryIconsQuery<WalkxcodeRepositoryIcon>({
|
const { data, isLoading } = useRepositoryIconsQuery<WalkxcodeRepositoryIcon>({
|
||||||
@@ -48,7 +48,7 @@ export const IconSelector = ({
|
|||||||
const [debouncedValue] = useDebouncedValue(form.values.name, 500);
|
const [debouncedValue] = useDebouncedValue(form.values.name, 500);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (allowServiceNamePropagation !== true) {
|
if (allowAppNamePropagation !== true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2,10 +2,10 @@ import { Tabs, TextInput, Switch, Text } from '@mantine/core';
|
|||||||
import { UseFormReturnType } from '@mantine/form';
|
import { UseFormReturnType } from '@mantine/form';
|
||||||
import { IconClick } from '@tabler/icons';
|
import { IconClick } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { ServiceType } from '../../../../../../types/service';
|
import { AppType } from '../../../../../../types/app';
|
||||||
|
|
||||||
interface BehaviourTabProps {
|
interface BehaviourTabProps {
|
||||||
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
form: UseFormReturnType<AppType, (values: AppType) => AppType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BehaviourTab = ({ form }: BehaviourTabProps) => {
|
export const BehaviourTab = ({ form }: BehaviourTabProps) => {
|
||||||
@@ -15,8 +15,8 @@ export const BehaviourTab = ({ form }: BehaviourTabProps) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
icon={<IconClick size={16} />}
|
icon={<IconClick size={16} />}
|
||||||
label="On click url"
|
label="On click url"
|
||||||
description="Overrides the service URL when clicking on the service"
|
description="Overrides the app URL when clicking on the app"
|
||||||
placeholder="URL that should be opened instead when clicking on the service"
|
placeholder="URL that should be opened instead when clicking on the app"
|
||||||
variant="default"
|
variant="default"
|
||||||
mb="md"
|
mb="md"
|
||||||
{...form.getInputProps('behaviour.onClickUrl')}
|
{...form.getInputProps('behaviour.onClickUrl')}
|
||||||
@@ -2,12 +2,12 @@ import { Tabs, Text, TextInput } from '@mantine/core';
|
|||||||
import { UseFormReturnType } from '@mantine/form';
|
import { UseFormReturnType } from '@mantine/form';
|
||||||
import { IconCursorText, IconLink } from '@tabler/icons';
|
import { IconCursorText, IconLink } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { ServiceType } from '../../../../../../types/service';
|
import { AppType } from '../../../../../../types/app';
|
||||||
import { EditServiceModalTab } from '../type';
|
import { EditAppModalTab } from '../type';
|
||||||
|
|
||||||
interface GeneralTabProps {
|
interface GeneralTabProps {
|
||||||
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
form: UseFormReturnType<AppType, (values: AppType) => AppType>;
|
||||||
openTab: (tab: EditServiceModalTab) => void;
|
openTab: (tab: EditAppModalTab) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
|
export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
|
||||||
@@ -16,9 +16,9 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
|
|||||||
<Tabs.Panel value="general" pt="lg">
|
<Tabs.Panel value="general" pt="lg">
|
||||||
<TextInput
|
<TextInput
|
||||||
icon={<IconCursorText size={16} />}
|
icon={<IconCursorText size={16} />}
|
||||||
label="Service name"
|
label="App name"
|
||||||
description="Used for displaying the service on the dashboard"
|
description="Used for displaying the app on the dashboard"
|
||||||
placeholder="My example service"
|
placeholder="My example app"
|
||||||
variant="default"
|
variant="default"
|
||||||
mb="md"
|
mb="md"
|
||||||
withAsterisk
|
withAsterisk
|
||||||
@@ -26,10 +26,10 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
|
|||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
icon={<IconLink size={16} />}
|
icon={<IconLink size={16} />}
|
||||||
label="Service url"
|
label="App url"
|
||||||
description={
|
description={
|
||||||
<Text>
|
<Text>
|
||||||
URL that will be opened when clicking on the service. Can be overwritten using
|
URL that will be opened when clicking on the app. Can be overwritten using
|
||||||
<Text
|
<Text
|
||||||
onClick={() => openTab('behaviour')}
|
onClick={() => openTab('behaviour')}
|
||||||
variant="link"
|
variant="link"
|
||||||
@@ -7,13 +7,13 @@ import {
|
|||||||
IntegrationField,
|
IntegrationField,
|
||||||
integrationFieldDefinitions,
|
integrationFieldDefinitions,
|
||||||
integrationFieldProperties,
|
integrationFieldProperties,
|
||||||
ServiceIntegrationPropertyType,
|
AppIntegrationPropertyType,
|
||||||
ServiceIntegrationType,
|
AppIntegrationType,
|
||||||
ServiceType,
|
AppType,
|
||||||
} from '../../../../../../../../types/service';
|
} from '../../../../../../../../types/app';
|
||||||
|
|
||||||
interface IntegrationSelectorProps {
|
interface IntegrationSelectorProps {
|
||||||
form: UseFormReturnType<ServiceType, (item: ServiceType) => ServiceType>;
|
form: UseFormReturnType<AppType, (item: AppType) => AppType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
|
export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
|
||||||
@@ -53,9 +53,9 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
|
|||||||
},
|
},
|
||||||
].filter((x) => Object.keys(integrationFieldProperties).includes(x.value));
|
].filter((x) => Object.keys(integrationFieldProperties).includes(x.value));
|
||||||
|
|
||||||
const getNewProperties = (value: string | null): ServiceIntegrationPropertyType[] => {
|
const getNewProperties = (value: string | null): AppIntegrationPropertyType[] => {
|
||||||
if (!value) return [];
|
if (!value) return [];
|
||||||
const integrationType = value as ServiceIntegrationType['type'];
|
const integrationType = value as AppIntegrationType['type'];
|
||||||
if (integrationType === null) {
|
if (integrationType === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
label="Integration configuration"
|
label="Integration configuration"
|
||||||
description="Treats this service as the selected integration and provides you with per-service configuration"
|
description="Treats this app as the selected integration and provides you with per-app configuration"
|
||||||
placeholder="Select your desired configuration"
|
placeholder="Select your desired configuration"
|
||||||
itemComponent={SelectItemComponent}
|
itemComponent={SelectItemComponent}
|
||||||
data={data}
|
data={data}
|
||||||
@@ -5,13 +5,13 @@ import {
|
|||||||
IntegrationField,
|
IntegrationField,
|
||||||
integrationFieldDefinitions,
|
integrationFieldDefinitions,
|
||||||
integrationFieldProperties,
|
integrationFieldProperties,
|
||||||
ServiceIntegrationPropertyType,
|
AppIntegrationPropertyType,
|
||||||
ServiceType,
|
AppType,
|
||||||
} from '../../../../../../../../types/service';
|
} from '../../../../../../../../types/app';
|
||||||
import { GenericSecretInput } from '../InputElements/GenericSecretInput';
|
import { GenericSecretInput } from '../InputElements/GenericSecretInput';
|
||||||
|
|
||||||
interface IntegrationOptionsRendererProps {
|
interface IntegrationOptionsRendererProps {
|
||||||
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
form: UseFormReturnType<AppType, (values: AppType) => AppType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererProps) => {
|
export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererProps) => {
|
||||||
@@ -34,7 +34,7 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP
|
|||||||
const type = Object.entries(integrationFieldDefinitions).find(
|
const type = Object.entries(integrationFieldDefinitions).find(
|
||||||
([k, v]) => k === property
|
([k, v]) => k === property
|
||||||
)![1].type;
|
)![1].type;
|
||||||
const newProperty: ServiceIntegrationPropertyType = {
|
const newProperty: AppIntegrationPropertyType = {
|
||||||
type,
|
type,
|
||||||
field: property as IntegrationField,
|
field: property as IntegrationField,
|
||||||
isDefined: false,
|
isDefined: false,
|
||||||
@@ -2,12 +2,12 @@ import { Alert, Divider, Tabs, Text } from '@mantine/core';
|
|||||||
import { UseFormReturnType } from '@mantine/form';
|
import { UseFormReturnType } from '@mantine/form';
|
||||||
import { IconAlertTriangle } from '@tabler/icons';
|
import { IconAlertTriangle } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { ServiceType } from '../../../../../../types/service';
|
import { AppType } from '../../../../../../types/app';
|
||||||
import { IntegrationSelector } from './Components/InputElements/IntegrationSelector';
|
import { IntegrationSelector } from './Components/InputElements/IntegrationSelector';
|
||||||
import { IntegrationOptionsRenderer } from './Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer';
|
import { IntegrationOptionsRenderer } from './Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer';
|
||||||
|
|
||||||
interface IntegrationTabProps {
|
interface IntegrationTabProps {
|
||||||
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
form: UseFormReturnType<AppType, (values: AppType) => AppType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IntegrationTab = ({ form }: IntegrationTabProps) => {
|
export const IntegrationTab = ({ form }: IntegrationTabProps) => {
|
||||||
@@ -2,10 +2,10 @@ import { Tabs, Switch, MultiSelect } from '@mantine/core';
|
|||||||
import { UseFormReturnType } from '@mantine/form';
|
import { UseFormReturnType } from '@mantine/form';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { StatusCodes } from '../../../../../../tools/acceptableStatusCodes';
|
import { StatusCodes } from '../../../../../../tools/acceptableStatusCodes';
|
||||||
import { ServiceType } from '../../../../../../types/service';
|
import { AppType } from '../../../../../../types/app';
|
||||||
|
|
||||||
interface NetworkTabProps {
|
interface NetworkTabProps {
|
||||||
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
form: UseFormReturnType<AppType, (values: AppType) => AppType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NetworkTab = ({ form }: NetworkTabProps) => {
|
export const NetworkTab = ({ form }: NetworkTabProps) => {
|
||||||
@@ -14,7 +14,7 @@ export const NetworkTab = ({ form }: NetworkTabProps) => {
|
|||||||
<Tabs.Panel value="network" pt="lg">
|
<Tabs.Panel value="network" pt="lg">
|
||||||
<Switch
|
<Switch
|
||||||
label="Enable status checker"
|
label="Enable status checker"
|
||||||
description="Sends a simple HTTP / HTTPS request to check if your service is online"
|
description="Sends a simple HTTP / HTTPS request to check if your app is online"
|
||||||
mb="md"
|
mb="md"
|
||||||
defaultChecked={form.values.network.enabledStatusChecker}
|
defaultChecked={form.values.network.enabledStatusChecker}
|
||||||
{...form.getInputProps('network.enabledStatusChecker')}
|
{...form.getInputProps('network.enabledStatusChecker')}
|
||||||
@@ -23,7 +23,7 @@ export const NetworkTab = ({ form }: NetworkTabProps) => {
|
|||||||
<MultiSelect
|
<MultiSelect
|
||||||
required
|
required
|
||||||
label="HTTP status codes"
|
label="HTTP status codes"
|
||||||
description="Determines what response codes are allowed for this service to be 'Online'"
|
description="Determines what response codes are allowed for this app to be 'Online'"
|
||||||
data={StatusCodes}
|
data={StatusCodes}
|
||||||
clearable
|
clearable
|
||||||
searchable
|
searchable
|
||||||
@@ -4,21 +4,21 @@ import Image from 'next/image';
|
|||||||
import { createStyles, Loader } from '@mantine/core';
|
import { createStyles, Loader } from '@mantine/core';
|
||||||
import { UseFormReturnType } from '@mantine/form';
|
import { UseFormReturnType } from '@mantine/form';
|
||||||
import { useDebouncedValue } from '@mantine/hooks';
|
import { useDebouncedValue } from '@mantine/hooks';
|
||||||
import { ServiceType } from '../../../../../../types/service';
|
import { AppType } from '../../../../../../types/app';
|
||||||
|
|
||||||
interface DebouncedServiceIconProps {
|
interface DebouncedAppIconProps {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
form: UseFormReturnType<AppType, (values: AppType) => AppType>;
|
||||||
debouncedWaitPeriod?: number;
|
debouncedWaitPeriod?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DebouncedServiceIcon = ({
|
export const DebouncedAppIcon = ({
|
||||||
form,
|
form,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
debouncedWaitPeriod = 1000,
|
debouncedWaitPeriod = 1000,
|
||||||
}: DebouncedServiceIconProps) => {
|
}: DebouncedAppIconProps) => {
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const [debouncedIconImageUrl] = useDebouncedValue(
|
const [debouncedIconImageUrl] = useDebouncedValue(
|
||||||
form.values.appearance.iconUrl,
|
form.values.appearance.iconUrl,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export type EditServiceModalTab =
|
export type EditAppModalTab =
|
||||||
| 'general'
|
| 'general'
|
||||||
| 'behaviour'
|
| 'behaviour'
|
||||||
| 'network'
|
| 'network'
|
||||||
@@ -23,7 +23,7 @@ export const AvailableIntegrationElements = ({
|
|||||||
<SelectorBackArrow onClickBack={onClickBack} />
|
<SelectorBackArrow onClickBack={onClickBack} />
|
||||||
|
|
||||||
<Text mb="md" color="dimmed">
|
<Text mb="md" color="dimmed">
|
||||||
Integrations interact with your services, to provide you with more control over your
|
Integrations interact with your apps, to provide you with more control over your
|
||||||
applications. They usually require a few configurations before use.
|
applications. They usually require a few configurations before use.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useTranslation } from 'next-i18next';
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { openContextModalGeneric } from '../../../../../../tools/mantineModalManagerExtensions';
|
import { openContextModalGeneric } from '../../../../../../tools/mantineModalManagerExtensions';
|
||||||
import { ServiceType } from '../../../../../../types/service';
|
import { AppType } from '../../../../../../types/app';
|
||||||
import { useStyles } from '../Shared/styles';
|
import { useStyles } from '../Shared/styles';
|
||||||
|
|
||||||
interface AvailableElementTypesProps {
|
interface AvailableElementTypesProps {
|
||||||
@@ -24,16 +24,16 @@ export const AvailableElementTypes = ({
|
|||||||
<Space h="lg" />
|
<Space h="lg" />
|
||||||
<Group spacing="md" grow>
|
<Group spacing="md" grow>
|
||||||
<ElementItem
|
<ElementItem
|
||||||
name="Service"
|
name="Apps"
|
||||||
icon={<IconBox size={40} strokeWidth={1.3} />}
|
icon={<IconBox size={40} strokeWidth={1.3} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
openContextModalGeneric<{ service: ServiceType; allowServiceNamePropagation: boolean }>(
|
openContextModalGeneric<{ app: AppType; allowAppNamePropagation: boolean }>(
|
||||||
{
|
{
|
||||||
modal: 'editService',
|
modal: 'editApp',
|
||||||
innerProps: {
|
innerProps: {
|
||||||
service: {
|
app: {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
name: 'Your service',
|
name: 'Your app',
|
||||||
url: 'https://homarr.dev',
|
url: 'https://homarr.dev',
|
||||||
appearance: {
|
appearance: {
|
||||||
iconUrl: '/imgs/logo/logo.png',
|
iconUrl: '/imgs/logo/logo.png',
|
||||||
@@ -67,7 +67,7 @@ export const AvailableElementTypes = ({
|
|||||||
properties: [],
|
properties: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
allowServiceNamePropagation: true,
|
allowAppNamePropagation: true,
|
||||||
},
|
},
|
||||||
size: 'xl',
|
size: 'xl',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const AvailableStaticTypes = ({ onClickBack }: AvailableStaticTypesProps)
|
|||||||
|
|
||||||
<Text mb="md" color="dimmed">
|
<Text mb="md" color="dimmed">
|
||||||
Static elements provide you additional control over your dashboard. They are static, because
|
Static elements provide you additional control over your dashboard. They are static, because
|
||||||
they don't integrate with any services and their content never changes.
|
they don't integrate with any apps and their content never changes.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ interface ServiceIconProps {
|
|||||||
service: string;
|
service: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServiceIcon = ({ size }: ServiceIconProps) => null;
|
export const AppIcon = ({ size }: ServiceIconProps) => null;
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
import { openContextModalGeneric } from '../../../../tools/mantineModalManagerExtensions';
|
import { openContextModalGeneric } from '../../../../tools/mantineModalManagerExtensions';
|
||||||
import { ServiceType } from '../../../../types/service';
|
import { AppType } from '../../../../types/app';
|
||||||
import { GenericTileMenu } from '../GenericTileMenu';
|
import { GenericTileMenu } from '../GenericTileMenu';
|
||||||
|
|
||||||
interface TileMenuProps {
|
interface TileMenuProps {
|
||||||
service: ServiceType;
|
app: AppType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServiceMenu = ({ service }: TileMenuProps) => {
|
export const AppMenu = ({ app }: TileMenuProps) => {
|
||||||
const handleClickEdit = () => {
|
const handleClickEdit = () => {
|
||||||
openContextModalGeneric<{ service: ServiceType; allowServiceNamePropagation: boolean }>({
|
openContextModalGeneric<{ app: AppType; allowAppNamePropagation: boolean }>({
|
||||||
modal: 'editService',
|
modal: 'editApp',
|
||||||
size: 'xl',
|
size: 'xl',
|
||||||
innerProps: {
|
innerProps: {
|
||||||
service,
|
app,
|
||||||
allowServiceNamePropagation: false,
|
allowAppNamePropagation: false,
|
||||||
},
|
},
|
||||||
styles: {
|
styles: {
|
||||||
root: {
|
root: {
|
||||||
@@ -25,9 +25,9 @@ export const ServiceMenu = ({ service }: TileMenuProps) => {
|
|||||||
|
|
||||||
const handleClickChangePosition = () => {
|
const handleClickChangePosition = () => {
|
||||||
openContextModalGeneric({
|
openContextModalGeneric({
|
||||||
modal: 'changeServicePositionModal',
|
modal: 'changeAppPositionModal',
|
||||||
innerProps: {
|
innerProps: {
|
||||||
service,
|
app,
|
||||||
},
|
},
|
||||||
styles: {
|
styles: {
|
||||||
root: {
|
root: {
|
||||||
@@ -3,23 +3,23 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { ServiceType } from '../../../../types/service';
|
import { AppType } from '../../../../types/app';
|
||||||
|
|
||||||
interface ServicePingProps {
|
interface AppPingProps {
|
||||||
service: ServiceType;
|
app: AppType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServicePing = ({ service }: ServicePingProps) => {
|
export const AppPing = ({ app }: AppPingProps) => {
|
||||||
const { t } = useTranslation('modules/ping');
|
const { t } = useTranslation('modules/ping');
|
||||||
const { config } = useConfigContext();
|
const { config } = useConfigContext();
|
||||||
const active =
|
const active =
|
||||||
(config?.settings.customization.layout.enabledPing && service.network.enabledStatusChecker) ??
|
(config?.settings.customization.layout.enabledPing && app.network.enabledStatusChecker) ??
|
||||||
false;
|
false;
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: [`ping/${service.id}`],
|
queryKey: [`ping/${app.id}`],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await fetch(`/api/modules/ping?url=${encodeURI(service.url)}`);
|
const response = await fetch(`/api/modules/ping?url=${encodeURI(app.url)}`);
|
||||||
const isOk = service.network.okStatus.includes(response.status);
|
const isOk = app.network.okStatus.includes(response.status);
|
||||||
return {
|
return {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
state: isOk ? 'online' : 'down',
|
state: isOk ? 'online' : 'down',
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
import { Card, Center, Text, UnstyledButton } from '@mantine/core';
|
import { Card, Center, Text, UnstyledButton } from '@mantine/core';
|
||||||
import { NextLink } from '@mantine/next';
|
import { NextLink } from '@mantine/next';
|
||||||
import { createStyles } from '@mantine/styles';
|
import { createStyles } from '@mantine/styles';
|
||||||
import { ServiceType } from '../../../../types/service';
|
import { AppType } from '../../../../types/app';
|
||||||
import { useCardStyles } from '../../../layout/useCardStyles';
|
import { useCardStyles } from '../../../layout/useCardStyles';
|
||||||
import { useEditModeStore } from '../../Views/useEditModeStore';
|
import { useEditModeStore } from '../../Views/useEditModeStore';
|
||||||
import { HomarrCardWrapper } from '../HomarrCardWrapper';
|
import { HomarrCardWrapper } from '../HomarrCardWrapper';
|
||||||
import { BaseTileProps } from '../type';
|
import { BaseTileProps } from '../type';
|
||||||
import { ServiceMenu } from './ServiceMenu';
|
import { AppMenu } from './AppMenu';
|
||||||
import { ServicePing } from './ServicePing';
|
import { AppPing } from './AppPing';
|
||||||
|
|
||||||
interface ServiceTileProps extends BaseTileProps {
|
interface AppTileProps extends BaseTileProps {
|
||||||
service: ServiceType;
|
app: AppType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServiceTile = ({ className, service }: ServiceTileProps) => {
|
export const AppTile = ({ className, app }: AppTileProps) => {
|
||||||
const isEditMode = useEditModeStore((x) => x.enabled);
|
const isEditMode = useEditModeStore((x) => x.enabled);
|
||||||
|
|
||||||
const { cx, classes } = useStyles();
|
const { cx, classes } = useStyles();
|
||||||
@@ -24,25 +24,25 @@ export const ServiceTile = ({ className, service }: ServiceTileProps) => {
|
|||||||
|
|
||||||
const inner = (
|
const inner = (
|
||||||
<>
|
<>
|
||||||
<Text align="center" weight={500} size="md" className={classes.serviceName}>
|
<Text align="center" weight={500} size="md" className={classes.appName}>
|
||||||
{service.name}
|
{app.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Center style={{ height: '75%', flex: 1 }}>
|
<Center style={{ height: '75%', flex: 1 }}>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img className={classes.image} src={service.appearance.iconUrl} alt="" />
|
<img className={classes.image} src={app.appearance.iconUrl} alt="" />
|
||||||
</Center>
|
</Center>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HomarrCardWrapper className={className}>
|
<HomarrCardWrapper className={className}>
|
||||||
{/* TODO: add service menu */}
|
{/* TODO: add app menu */}
|
||||||
|
|
||||||
<div style={{ position: 'absolute', top: 10, right: 10 }}>
|
<div style={{ position: 'absolute', top: 10, right: 10 }}>
|
||||||
<ServiceMenu service={service} />
|
<AppMenu app={app} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!service.url || isEditMode ? (
|
{!app.url || isEditMode ? (
|
||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
className={classes.button}
|
className={classes.button}
|
||||||
style={{ pointerEvents: isEditMode ? 'none' : 'auto' }}
|
style={{ pointerEvents: isEditMode ? 'none' : 'auto' }}
|
||||||
@@ -53,14 +53,14 @@ export const ServiceTile = ({ className, service }: ServiceTileProps) => {
|
|||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
style={{ pointerEvents: isEditMode ? 'none' : 'auto' }}
|
style={{ pointerEvents: isEditMode ? 'none' : 'auto' }}
|
||||||
component={NextLink}
|
component={NextLink}
|
||||||
href={service.url}
|
href={app.url}
|
||||||
target={service.behaviour.isOpeningNewTab ? '_blank' : '_self'}
|
target={app.behaviour.isOpeningNewTab ? '_blank' : '_self'}
|
||||||
className={cx(classes.button, classes.link)}
|
className={cx(classes.button, classes.link)}
|
||||||
>
|
>
|
||||||
{inner}
|
{inner}
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
)}
|
)}
|
||||||
<ServicePing service={service} />
|
<AppPing app={app} />
|
||||||
</HomarrCardWrapper>
|
</HomarrCardWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -72,8 +72,8 @@ const useStyles = createStyles((theme, _params, getRef) => ({
|
|||||||
maxWidth: '80%',
|
maxWidth: '80%',
|
||||||
transition: 'transform 100ms ease-in-out',
|
transition: 'transform 100ms ease-in-out',
|
||||||
},
|
},
|
||||||
serviceName: {
|
appName: {
|
||||||
ref: getRef('serviceName'),
|
ref: getRef('appName'),
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@@ -87,8 +87,8 @@ const useStyles = createStyles((theme, _params, getRef) => ({
|
|||||||
[`&:hover .${getRef('image')}`]: {
|
[`&:hover .${getRef('image')}`]: {
|
||||||
// TODO: add styles for image when hovering card
|
// TODO: add styles for image when hovering card
|
||||||
},
|
},
|
||||||
[`&:hover .${getRef('serviceName')}`]: {
|
[`&:hover .${getRef('appName')}`]: {
|
||||||
// TODO: add styles for service name when hovering card
|
// TODO: add styles for app name when hovering card
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -2,7 +2,7 @@ import { ReactNode, RefObject } from 'react';
|
|||||||
|
|
||||||
interface GridstackTileWrapperProps {
|
interface GridstackTileWrapperProps {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'service' | 'module';
|
type: 'app' | 'module';
|
||||||
x?: number;
|
x?: number;
|
||||||
y?: number;
|
y?: number;
|
||||||
width?: number;
|
width?: number;
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { DashDotTile } from '../../../widgets/dashDot/DashDotTile';
|
|||||||
import { UseNetTile } from '../../../widgets/useNet/UseNetTile';
|
import { UseNetTile } from '../../../widgets/useNet/UseNetTile';
|
||||||
import { WeatherTile } from '../../../widgets/weather/WeatherTile';
|
import { WeatherTile } from '../../../widgets/weather/WeatherTile';
|
||||||
import { EmptyTile } from './EmptyTile';
|
import { EmptyTile } from './EmptyTile';
|
||||||
import { ServiceTile } from './Service/ServiceTile';
|
import { AppTile } from './Apps/AppTile';
|
||||||
|
|
||||||
// TODO: just remove and use service (later app) directly. For widgets the the definition should contain min/max width/height
|
// TODO: just remove and use app (later app) directly. For widgets the the definition should contain min/max width/height
|
||||||
type TileDefinitionProps = {
|
type TileDefinitionProps = {
|
||||||
[key in keyof IntegrationsType | 'service']: {
|
[key in keyof IntegrationsType | 'app']: {
|
||||||
minWidth?: number;
|
minWidth?: number;
|
||||||
minHeight?: number;
|
minHeight?: number;
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
@@ -20,8 +20,8 @@ type TileDefinitionProps = {
|
|||||||
|
|
||||||
// TODO: change components for other modules
|
// TODO: change components for other modules
|
||||||
export const Tiles: TileDefinitionProps = {
|
export const Tiles: TileDefinitionProps = {
|
||||||
service: {
|
app: {
|
||||||
component: ServiceTile,
|
component: AppTile,
|
||||||
minWidth: 2,
|
minWidth: 2,
|
||||||
maxWidth: 12,
|
maxWidth: 12,
|
||||||
minHeight: 2,
|
minHeight: 2,
|
||||||
|
|||||||
@@ -27,19 +27,19 @@ export const DashboardCategory = ({ category }: DashboardCategoryProps) => {
|
|||||||
data-category={category.id}
|
data-category={category.id}
|
||||||
ref={refs.wrapper}
|
ref={refs.wrapper}
|
||||||
>
|
>
|
||||||
{items?.map((service) => {
|
{items?.map((app) => {
|
||||||
const { component: TileComponent, ...tile } = Tiles['service'];
|
const { component: TileComponent, ...tile } = Tiles['app'];
|
||||||
return (
|
return (
|
||||||
<GridstackTileWrapper
|
<GridstackTileWrapper
|
||||||
id={service.id}
|
id={app.id}
|
||||||
type="service"
|
type="app"
|
||||||
key={service.id}
|
key={app.id}
|
||||||
itemRef={refs.items.current[service.id]}
|
itemRef={refs.items.current[app.id]}
|
||||||
{...tile}
|
{...tile}
|
||||||
{...service.shape.location}
|
{...app.shape.location}
|
||||||
{...service.shape.size}
|
{...app.shape.size}
|
||||||
>
|
>
|
||||||
<TileComponent className="grid-stack-item-content" service={service} />
|
<TileComponent className="grid-stack-item-content" app={app} />
|
||||||
</GridstackTileWrapper>
|
</GridstackTileWrapper>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -29,19 +29,19 @@ export const DashboardSidebar = ({ location }: DashboardSidebarProps) => {
|
|||||||
gs-min-row={minRow}
|
gs-min-row={minRow}
|
||||||
ref={refs.wrapper}
|
ref={refs.wrapper}
|
||||||
>
|
>
|
||||||
{items.map((service) => {
|
{items.map((app) => {
|
||||||
const { component: TileComponent, ...tile } = Tiles['service'];
|
const { component: TileComponent, ...tile } = Tiles['app'];
|
||||||
return (
|
return (
|
||||||
<GridstackTileWrapper
|
<GridstackTileWrapper
|
||||||
id={service.id}
|
id={app.id}
|
||||||
type="service"
|
type="app"
|
||||||
key={service.id}
|
key={app.id}
|
||||||
itemRef={refs.items.current[service.id]}
|
itemRef={refs.items.current[app.id]}
|
||||||
{...tile}
|
{...tile}
|
||||||
{...service.shape.location}
|
{...app.shape.location}
|
||||||
{...service.shape.size}
|
{...app.shape.size}
|
||||||
>
|
>
|
||||||
<TileComponent className="grid-stack-item-content" service={service} />
|
<TileComponent className="grid-stack-item-content" app={app} />
|
||||||
</GridstackTileWrapper>
|
</GridstackTileWrapper>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -17,19 +17,19 @@ export const DashboardWrapper = ({ wrapper }: DashboardWrapperProps) => {
|
|||||||
data-wrapper={wrapper.id}
|
data-wrapper={wrapper.id}
|
||||||
ref={refs.wrapper}
|
ref={refs.wrapper}
|
||||||
>
|
>
|
||||||
{items?.map((service) => {
|
{items?.map((app) => {
|
||||||
const { component: TileComponent, ...tile } = Tiles['service'];
|
const { component: TileComponent, ...tile } = Tiles['app'];
|
||||||
return (
|
return (
|
||||||
<GridstackTileWrapper
|
<GridstackTileWrapper
|
||||||
id={service.id}
|
id={app.id}
|
||||||
type="service"
|
type="app"
|
||||||
key={service.id}
|
key={app.id}
|
||||||
itemRef={refs.items.current[service.id]}
|
itemRef={refs.items.current[app.id]}
|
||||||
{...tile}
|
{...tile}
|
||||||
{...service.shape.location}
|
{...app.shape.location}
|
||||||
{...service.shape.size}
|
{...app.shape.size}
|
||||||
>
|
>
|
||||||
<TileComponent className="grid-stack-item-content" service={service} />
|
<TileComponent className="grid-stack-item-content" app={app} />
|
||||||
</GridstackTileWrapper>
|
</GridstackTileWrapper>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { GridStack, GridStackNode } from 'fily-publish-gridstack';
|
import { GridStack, GridStackNode } from 'fily-publish-gridstack';
|
||||||
import { MutableRefObject, RefObject } from 'react';
|
import { MutableRefObject, RefObject } from 'react';
|
||||||
import { IntegrationsType } from '../../../../types/integration';
|
import { IntegrationsType } from '../../../../types/integration';
|
||||||
import { ServiceType } from '../../../../types/service';
|
import { AppType } from '../../../../types/app';
|
||||||
|
|
||||||
export const initializeGridstack = (
|
export const initializeGridstack = (
|
||||||
areaType: 'wrapper' | 'category' | 'sidebar',
|
areaType: 'wrapper' | 'category' | 'sidebar',
|
||||||
@@ -9,7 +9,7 @@ export const initializeGridstack = (
|
|||||||
gridRef: MutableRefObject<GridStack | undefined>,
|
gridRef: MutableRefObject<GridStack | undefined>,
|
||||||
itemRefs: MutableRefObject<Record<string, RefObject<HTMLDivElement>>>,
|
itemRefs: MutableRefObject<Record<string, RefObject<HTMLDivElement>>>,
|
||||||
areaId: string,
|
areaId: string,
|
||||||
items: ServiceType[],
|
items: AppType[],
|
||||||
integrations: IntegrationsType,
|
integrations: IntegrationsType,
|
||||||
isEditMode: boolean,
|
isEditMode: boolean,
|
||||||
events: {
|
events: {
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ import { useConfigContext } from '../../../../config/provider';
|
|||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
import { useResize } from '../../../../hooks/use-resize';
|
import { useResize } from '../../../../hooks/use-resize';
|
||||||
import { IntegrationsType } from '../../../../types/integration';
|
import { IntegrationsType } from '../../../../types/integration';
|
||||||
import { ServiceType } from '../../../../types/service';
|
import { AppType } from '../../../../types/app';
|
||||||
import { TileBaseType } from '../../../../types/tile';
|
import { TileBaseType } from '../../../../types/tile';
|
||||||
import { useEditModeStore } from '../../Views/useEditModeStore';
|
import { useEditModeStore } from '../../Views/useEditModeStore';
|
||||||
import { initializeGridstack } from './init-gridstack';
|
import { initializeGridstack } from './init-gridstack';
|
||||||
|
|
||||||
interface UseGristackReturnType {
|
interface UseGristackReturnType {
|
||||||
items: ServiceType[];
|
items: AppType[];
|
||||||
integrations: Partial<IntegrationsType>;
|
integrations: Partial<IntegrationsType>;
|
||||||
refs: {
|
refs: {
|
||||||
wrapper: RefObject<HTMLDivElement>;
|
wrapper: RefObject<HTMLDivElement>;
|
||||||
@@ -45,7 +45,7 @@ export const useGridstack = (
|
|||||||
|
|
||||||
const items = useMemo(
|
const items = useMemo(
|
||||||
() =>
|
() =>
|
||||||
config?.services.filter(
|
config?.apps.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
x.area.type === areaType &&
|
x.area.type === areaType &&
|
||||||
(x.area.type === 'sidebar'
|
(x.area.type === 'sidebar'
|
||||||
@@ -100,8 +100,8 @@ export const useGridstack = (
|
|||||||
// Updates the config and defines the new position of the item
|
// Updates the config and defines the new position of the item
|
||||||
updateConfig(configName, (previous) => {
|
updateConfig(configName, (previous) => {
|
||||||
const currentItem =
|
const currentItem =
|
||||||
itemType === 'service'
|
itemType === 'app'
|
||||||
? previous.services.find((x) => x.id === itemId)
|
? previous.apps.find((x) => x.id === itemId)
|
||||||
: previous.integrations[itemId as keyof typeof previous.integrations];
|
: previous.integrations[itemId as keyof typeof previous.integrations];
|
||||||
if (!currentItem) return previous;
|
if (!currentItem) return previous;
|
||||||
|
|
||||||
@@ -116,12 +116,12 @@ export const useGridstack = (
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (itemType === 'service') {
|
if (itemType === 'app') {
|
||||||
return {
|
return {
|
||||||
...previous,
|
...previous,
|
||||||
services: [
|
apps: [
|
||||||
...previous.services.filter((x) => x.id !== itemId),
|
...previous.apps.filter((x) => x.id !== itemId),
|
||||||
{ ...(currentItem as ServiceType) },
|
{ ...(currentItem as AppType) },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -147,8 +147,8 @@ export const useGridstack = (
|
|||||||
// Updates the config and defines the new position and wrapper of the item
|
// Updates the config and defines the new position and wrapper of the item
|
||||||
updateConfig(configName, (previous) => {
|
updateConfig(configName, (previous) => {
|
||||||
const currentItem =
|
const currentItem =
|
||||||
itemType === 'service'
|
itemType === 'app'
|
||||||
? previous.services.find((x) => x.id === itemId)
|
? previous.apps.find((x) => x.id === itemId)
|
||||||
: previous.integrations[itemId as keyof typeof previous.integrations];
|
: previous.integrations[itemId as keyof typeof previous.integrations];
|
||||||
|
|
||||||
if (!currentItem) return previous;
|
if (!currentItem) return previous;
|
||||||
@@ -180,12 +180,12 @@ export const useGridstack = (
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (itemType === 'service') {
|
if (itemType === 'app') {
|
||||||
return {
|
return {
|
||||||
...previous,
|
...previous,
|
||||||
services: [
|
apps: [
|
||||||
...previous.services.filter((x) => x.id !== itemId),
|
...previous.apps.filter((x) => x.id !== itemId),
|
||||||
{ ...(currentItem as ServiceType) },
|
{ ...(currentItem as AppType) },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
|
|||||||
<Text color="dimmed" size="xs" align="center">
|
<Text color="dimmed" size="xs" align="center">
|
||||||
Only for
|
Only for
|
||||||
<br />
|
<br />
|
||||||
services &<br />
|
apps &<br />
|
||||||
integrations
|
integrations
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -115,7 +115,7 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
|
|||||||
<Text color="dimmed" size="xs" align="center">
|
<Text color="dimmed" size="xs" align="center">
|
||||||
Only for
|
Only for
|
||||||
<br />
|
<br />
|
||||||
services &<br />
|
apps &<br />
|
||||||
integrations
|
integrations
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -126,13 +126,13 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
|
|||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Enable left sidebar"
|
label="Enable left sidebar"
|
||||||
description="Optional. Can be used for services and integrations only"
|
description="Optional. Can be used for apps and integrations only"
|
||||||
checked={leftSidebar}
|
checked={leftSidebar}
|
||||||
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
|
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Enable right sidebar"
|
label="Enable right sidebar"
|
||||||
description="Optional. Can be used for services and integrations only"
|
description="Optional. Can be used for apps and integrations only"
|
||||||
checked={rightSidebar}
|
checked={rightSidebar}
|
||||||
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
|
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { IconBrandYoutube, IconDownload, IconMovie, IconSearch } from '@tabler/i
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import React, { forwardRef, useEffect, useRef, useState } from 'react';
|
import React, { forwardRef, useEffect, useRef, useState } from 'react';
|
||||||
import SmallServiceItem from '../../AppShelf/SmallServiceItem';
|
import SmallAppItem from './SmallAppItem';
|
||||||
import Tip from '../Tip';
|
import Tip from '../Tip';
|
||||||
import { searchUrls } from '../../Settings/Common/SearchEngine/SearchEngineSelector';
|
import { searchUrls } from '../../Settings/Common/SearchEngine/SearchEngineSelector';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../config/provider';
|
||||||
@@ -56,12 +56,12 @@ export function Search() {
|
|||||||
const [debounced, cancel] = useDebouncedValue(searchQuery, 250);
|
const [debounced, cancel] = useDebouncedValue(searchQuery, 250);
|
||||||
|
|
||||||
// TODO: ask manuel-rw about overseerr
|
// TODO: ask manuel-rw about overseerr
|
||||||
// Answer: We can simply check if there is a service of the type overseer and display results if there is one.
|
// Answer: We can simply check if there is a app of the type overseer and display results if there is one.
|
||||||
// Overseerr is not use anywhere else, so it makes no sense to add a standalone toggle for displaying results
|
// Overseerr is not use anywhere else, so it makes no sense to add a standalone toggle for displaying results
|
||||||
const isOverseerrEnabled = false; //config?.settings.common.enabledModules.overseerr;
|
const isOverseerrEnabled = false; //config?.settings.common.enabledModules.overseerr;
|
||||||
const overseerrService = config?.services.find(
|
const overseerrApp = config?.apps.find(
|
||||||
(service) =>
|
(app) =>
|
||||||
service.integration?.type === 'overseerr' || service.integration?.type === 'jellyseerr'
|
app.integration?.type === 'overseerr' || app.integration?.type === 'jellyseerr'
|
||||||
);
|
);
|
||||||
const searchEngineSettings = config?.settings.common.searchEngine;
|
const searchEngineSettings = config?.settings.common.searchEngine;
|
||||||
const searchEngineUrl = !searchEngineSettings
|
const searchEngineUrl = !searchEngineSettings
|
||||||
@@ -100,32 +100,32 @@ export function Search() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <IconMovie />,
|
icon: <IconMovie />,
|
||||||
disabled: !(isOverseerrEnabled === true && overseerrService !== undefined),
|
disabled: !(isOverseerrEnabled === true && overseerrApp !== undefined),
|
||||||
label: t('searchEngines.overseerr.name'),
|
label: t('searchEngines.overseerr.name'),
|
||||||
value: 'overseerr',
|
value: 'overseerr',
|
||||||
description: t('searchEngines.overseerr.description'),
|
description: t('searchEngines.overseerr.description'),
|
||||||
url: `${overseerrService?.url}search?query=`,
|
url: `${overseerrApp?.url}search?query=`,
|
||||||
shortcut: 'm',
|
shortcut: 'm',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const [selectedSearchEngine, setSearchEngine] = useState<ItemProps>(searchEnginesList[0]);
|
const [selectedSearchEngine, setSearchEngine] = useState<ItemProps>(searchEnginesList[0]);
|
||||||
const matchingServices =
|
const matchingApps =
|
||||||
config?.services.filter((service) => {
|
config?.apps.filter((app) => {
|
||||||
if (searchQuery === '' || searchQuery === undefined) {
|
if (searchQuery === '' || searchQuery === undefined) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return service.name.toLowerCase().includes(searchQuery.toLowerCase());
|
return app.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
}) ?? [];
|
}) ?? [];
|
||||||
const autocompleteData = matchingServices.map((service) => ({
|
const autocompleteData = matchingApps.map((app) => ({
|
||||||
label: service.name,
|
label: app.name,
|
||||||
value: service.name,
|
value: app.name,
|
||||||
icon: service.appearance.iconUrl,
|
icon: app.appearance.iconUrl,
|
||||||
url: service.behaviour.onClickUrl ?? service.url,
|
url: app.behaviour.onClickUrl ?? app.url,
|
||||||
}));
|
}));
|
||||||
const AutoCompleteItem = forwardRef<HTMLDivElement, any>(
|
const AutoCompleteItem = forwardRef<HTMLDivElement, any>(
|
||||||
({ label, value, icon, url, ...others }: any, ref) => (
|
({ label, value, icon, url, ...others }: any, ref) => (
|
||||||
<div ref={ref} {...others}>
|
<div ref={ref} {...others}>
|
||||||
<SmallServiceItem service={{ label, value, icon, url }} />
|
<SmallAppItem app={{ label, value, icon, url }} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
18
src/components/layout/header/SmallAppItem.tsx
Normal file
18
src/components/layout/header/SmallAppItem.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Avatar, Group, Text } from '@mantine/core';
|
||||||
|
|
||||||
|
interface smallAppItem {
|
||||||
|
label: string;
|
||||||
|
icon?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SmallAppItem(props: any) {
|
||||||
|
const { app }: { app: smallAppItem } = props;
|
||||||
|
// TODO : Use Next/link
|
||||||
|
return (
|
||||||
|
<Group>
|
||||||
|
{app.icon && <Avatar src={app.icon} />}
|
||||||
|
<Text>{app.label}</Text>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -52,10 +52,10 @@ export default function CalendarComponent(props: any) {
|
|||||||
const [lidarrMedias, setLidarrMedias] = useState([] as any);
|
const [lidarrMedias, setLidarrMedias] = useState([] as any);
|
||||||
const [radarrMedias, setRadarrMedias] = useState([] as any);
|
const [radarrMedias, setRadarrMedias] = useState([] as any);
|
||||||
const [readarrMedias, setReadarrMedias] = useState([] as any);
|
const [readarrMedias, setReadarrMedias] = useState([] as any);
|
||||||
const sonarrServices = config.services.filter((service) => service.type === 'Sonarr');
|
const sonarrServices = config.apps.filter((service) => service.type === 'Sonarr');
|
||||||
const radarrServices = config.services.filter((service) => service.type === 'Radarr');
|
const radarrServices = config.apps.filter((service) => service.type === 'Radarr');
|
||||||
const lidarrServices = config.services.filter((service) => service.type === 'Lidarr');
|
const lidarrServices = config.apps.filter((service) => service.type === 'Lidarr');
|
||||||
const readarrServices = config.services.filter((service) => service.type === 'Readarr');
|
const readarrServices = config.apps.filter((service) => service.type === 'Readarr');
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
|
||||||
const { classes, cx } = useStyles();
|
const { classes, cx } = useStyles();
|
||||||
@@ -125,7 +125,7 @@ export default function CalendarComponent(props: any) {
|
|||||||
).then(() => {
|
).then(() => {
|
||||||
setReadarrMedias(currentReadarrMedias);
|
setReadarrMedias(currentReadarrMedias);
|
||||||
});
|
});
|
||||||
}, [config.services]);
|
}, [config.apps]);
|
||||||
|
|
||||||
const weekStartsAtSunday =
|
const weekStartsAtSunday =
|
||||||
(config?.modules?.[CalendarModule.id]?.options?.sundaystart?.value as boolean) ?? false;
|
(config?.modules?.[CalendarModule.id]?.options?.sundaystart?.value as boolean) ?? false;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export interface IMedia {
|
|||||||
export function OverseerrMediaDisplay(props: any) {
|
export function OverseerrMediaDisplay(props: any) {
|
||||||
const { media }: { media: Result } = props;
|
const { media }: { media: Result } = props;
|
||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
const service = config.services.find(
|
const service = config.apps.find(
|
||||||
(service) => service.type === 'Overseerr' || service.type === 'Jellyseerr'
|
(service) => service.type === 'Overseerr' || service.type === 'Jellyseerr'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ export function ReadarrMediaDisplay(props: any) {
|
|||||||
const { media }: { media: any } = props;
|
const { media }: { media: any } = props;
|
||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
// Find lidarr in services
|
// Find lidarr in services
|
||||||
const readarr = config.services.find((service: serviceItem) => service.type === 'Readarr');
|
const readarr = config.apps.find((service: serviceItem) => service.type === 'Readarr');
|
||||||
// Find a poster CoverType
|
// Find a poster CoverType
|
||||||
const poster = media.images.find((image: any) => image.coverType === 'cover');
|
const poster = media.images.find((image: any) => image.coverType === 'cover');
|
||||||
if (!readarr) {
|
if (!readarr) {
|
||||||
@@ -90,7 +90,7 @@ export function LidarrMediaDisplay(props: any) {
|
|||||||
const { media }: { media: any } = props;
|
const { media }: { media: any } = props;
|
||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
// Find lidarr in services
|
// Find lidarr in services
|
||||||
const lidarr = config.services.find((service: serviceItem) => service.type === 'Lidarr');
|
const lidarr = config.apps.find((service: serviceItem) => service.type === 'Lidarr');
|
||||||
// Find a poster CoverType
|
// Find a poster CoverType
|
||||||
const poster = media.images.find((image: any) => image.coverType === 'cover');
|
const poster = media.images.find((image: any) => image.coverType === 'cover');
|
||||||
if (!lidarr) {
|
if (!lidarr) {
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export function DashdotComponent() {
|
|||||||
|
|
||||||
const dashConfig = config.modules?.[DashdotModule.id].options as typeof DashdotModule['options'];
|
const dashConfig = config.modules?.[DashdotModule.id].options as typeof DashdotModule['options'];
|
||||||
const isCompact = dashConfig?.useCompactView?.value ?? false;
|
const isCompact = dashConfig?.useCompactView?.value ?? false;
|
||||||
const dashdotService: serviceItem | undefined = config.services.filter(
|
const dashdotService: serviceItem | undefined = config.apps.filter(
|
||||||
(service) => service.type === 'Dash.'
|
(service) => service.type === 'Dash.'
|
||||||
)[0];
|
)[0];
|
||||||
const dashdotUrl = dashdotService?.url ?? dashConfig?.url?.value ?? '';
|
const dashdotUrl = dashdotService?.url ?? dashConfig?.url?.value ?? '';
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|||||||
import { tryMatchService } from '../../tools/addToHomarr';
|
import { tryMatchService } from '../../tools/addToHomarr';
|
||||||
import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions';
|
import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions';
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
import { ServiceType } from '../../types/service';
|
import { AppType } from '../../types/app';
|
||||||
|
|
||||||
let t: TFunction<'modules/docker', undefined>;
|
let t: TFunction<'modules/docker', undefined>;
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
|||||||
disabled={selected.length === 0 || selected.length > 1}
|
disabled={selected.length === 0 || selected.length > 1}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const containerUrl = `http://localhost:${selected[0].Ports[0].PublicPort}`;
|
const containerUrl = `http://localhost:${selected[0].Ports[0].PublicPort}`;
|
||||||
openContextModalGeneric<{ service: ServiceType }>({
|
openContextModalGeneric<{ service: AppType }>({
|
||||||
modal: 'editService',
|
modal: 'editService',
|
||||||
innerProps: {
|
innerProps: {
|
||||||
service: {
|
service: {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const TorrentsModule: IModule = {
|
|||||||
export default function TorrentsComponent() {
|
export default function TorrentsComponent() {
|
||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
const downloadServices =
|
const downloadServices =
|
||||||
config.services.filter(
|
config.apps.filter(
|
||||||
(service) =>
|
(service) =>
|
||||||
service.type === 'qBittorrent' ||
|
service.type === 'qBittorrent' ||
|
||||||
service.type === 'Transmission' ||
|
service.type === 'Transmission' ||
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default function TotalDownloadsComponent() {
|
|||||||
const setSafeInterval = useSetSafeInterval();
|
const setSafeInterval = useSetSafeInterval();
|
||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
const downloadServices =
|
const downloadServices =
|
||||||
config.services.filter(
|
config.apps.filter(
|
||||||
(service) =>
|
(service) =>
|
||||||
service.type === 'qBittorrent' ||
|
service.type === 'qBittorrent' ||
|
||||||
service.type === 'Transmission' ||
|
service.type === 'Transmission' ||
|
||||||
@@ -68,7 +68,7 @@ export default function TotalDownloadsComponent() {
|
|||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}, [config.services]);
|
}, [config.apps]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
torrentHistoryHandlers.append({
|
torrentHistoryHandlers.append({
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const UsenetComponent: FunctionComponent = () => {
|
|||||||
const { t } = useTranslation('modules/usenet');
|
const { t } = useTranslation('modules/usenet');
|
||||||
|
|
||||||
const [selectedServiceId, setSelectedService] = useState<string | null>(downloadServices[0]?.id);
|
const [selectedServiceId, setSelectedService] = useState<string | null>(downloadServices[0]?.id);
|
||||||
const { data } = useGetUsenetInfo({ serviceId: selectedServiceId! });
|
const { data } = useGetUsenetInfo({ appId: selectedServiceId! });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedServiceId && downloadServices.length) {
|
if (!selectedServiceId && downloadServices.length) {
|
||||||
@@ -39,8 +39,8 @@ export const UsenetComponent: FunctionComponent = () => {
|
|||||||
}
|
}
|
||||||
}, [downloadServices, selectedServiceId]);
|
}, [downloadServices, selectedServiceId]);
|
||||||
|
|
||||||
const { mutate: pause } = usePauseUsenetQueue({ serviceId: selectedServiceId! });
|
const { mutate: pause } = usePauseUsenetQueue({ appId: selectedServiceId! });
|
||||||
const { mutate: resume } = useResumeUsenetQueue({ serviceId: selectedServiceId! });
|
const { mutate: resume } = useResumeUsenetQueue({ appId: selectedServiceId! });
|
||||||
|
|
||||||
if (downloadServices.length === 0) {
|
if (downloadServices.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -98,10 +98,10 @@ export const UsenetComponent: FunctionComponent = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Tabs.Panel value="queue">
|
<Tabs.Panel value="queue">
|
||||||
<UsenetQueueList serviceId={selectedServiceId} />
|
<UsenetQueueList appId={selectedServiceId} />
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
<Tabs.Panel value="history">
|
<Tabs.Panel value="history">
|
||||||
<UsenetHistoryList serviceId={selectedServiceId} />
|
<UsenetHistoryList appId={selectedServiceId} />
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export async function getServerSideProps({
|
|||||||
props: {
|
props: {
|
||||||
config: {
|
config: {
|
||||||
name: 'Default config',
|
name: 'Default config',
|
||||||
services: [],
|
apps: [],
|
||||||
settings: {
|
settings: {
|
||||||
searchUrl: 'https://www.google.com/search?q=',
|
searchUrl: 'https://www.google.com/search?q=',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import { appWithTranslation } from 'next-i18next';
|
|||||||
import { AppProps } from 'next/app';
|
import { AppProps } from 'next/app';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { ChangeAppPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeAppPositionModal';
|
||||||
import { ChangeIntegrationPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeIntegrationPositionModal';
|
import { ChangeIntegrationPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeIntegrationPositionModal';
|
||||||
import { ChangeServicePositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeServicePositionModal';
|
import { EditAppModal } from '../components/Dashboard/Modals/EditAppModal/EditAppModal';
|
||||||
import { EditServiceModal } from '../components/Dashboard/Modals/EditService/EditServiceModal';
|
|
||||||
import { SelectElementModal } from '../components/Dashboard/Modals/SelectElement/SelectElementModal';
|
import { SelectElementModal } from '../components/Dashboard/Modals/SelectElement/SelectElementModal';
|
||||||
import { WidgetsRemoveModal } from '../components/Dashboard/Tiles/Widgets/WidgetsRemoveModal';
|
|
||||||
import { WidgetsEditModal } from '../components/Dashboard/Tiles/Widgets/WidgetsEditModal';
|
import { WidgetsEditModal } from '../components/Dashboard/Tiles/Widgets/WidgetsEditModal';
|
||||||
|
import { WidgetsRemoveModal } from '../components/Dashboard/Tiles/Widgets/WidgetsRemoveModal';
|
||||||
import { CategoryEditModal } from '../components/Dashboard/Wrappers/Category/CategoryEditModal';
|
import { CategoryEditModal } from '../components/Dashboard/Wrappers/Category/CategoryEditModal';
|
||||||
import { ConfigProvider } from '../config/provider';
|
import { ConfigProvider } from '../config/provider';
|
||||||
import '../styles/global.scss';
|
import '../styles/global.scss';
|
||||||
@@ -86,12 +86,12 @@ function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
|
|||||||
<NotificationsProvider limit={4} position="bottom-left">
|
<NotificationsProvider limit={4} position="bottom-left">
|
||||||
<ModalsProvider
|
<ModalsProvider
|
||||||
modals={{
|
modals={{
|
||||||
editService: EditServiceModal,
|
editApp: EditAppModal,
|
||||||
selectElement: SelectElementModal,
|
selectElement: SelectElementModal,
|
||||||
integrationOptions: WidgetsEditModal,
|
integrationOptions: WidgetsEditModal,
|
||||||
integrationRemove: WidgetsRemoveModal,
|
integrationRemove: WidgetsRemoveModal,
|
||||||
categoryEditModal: CategoryEditModal,
|
categoryEditModal: CategoryEditModal,
|
||||||
changeServicePositionModal: ChangeServicePositionModal,
|
changeAppPositionModal: ChangeAppPositionModal,
|
||||||
changeIntegrationPositionModal: ChangeIntegrationPositionModal,
|
changeIntegrationPositionModal: ChangeIntegrationPositionModal,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { getConfig } from '../../../tools/config/getConfig';
|
import { getConfig } from '../../../tools/config/getConfig';
|
||||||
import { ServiceIntegrationType } from '../../../types/service';
|
import { AppIntegrationType } from '../../../types/app';
|
||||||
|
|
||||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
// Filter out if the reuqest is a POST or a GET
|
// Filter out if the reuqest is a POST or a GET
|
||||||
@@ -15,7 +15,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
// Parse req.body as a ServiceItem
|
// Parse req.body as a AppItem
|
||||||
const {
|
const {
|
||||||
month: monthString,
|
month: monthString,
|
||||||
year: yearString,
|
year: yearString,
|
||||||
@@ -34,20 +34,20 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
const config = getConfig(configName);
|
const config = getConfig(configName);
|
||||||
|
|
||||||
const mediaServiceIntegrationTypes: ServiceIntegrationType['type'][] = [
|
const mediaAppIntegrationTypes: AppIntegrationType['type'][] = [
|
||||||
'sonarr',
|
'sonarr',
|
||||||
'radarr',
|
'radarr',
|
||||||
'readarr',
|
'readarr',
|
||||||
'lidarr',
|
'lidarr',
|
||||||
];
|
];
|
||||||
const mediaServices = config.services.filter(
|
const mediaApps = config.apps.filter(
|
||||||
(service) =>
|
(app) =>
|
||||||
service.integration && mediaServiceIntegrationTypes.includes(service.integration.type)
|
app.integration && mediaAppIntegrationTypes.includes(app.integration.type)
|
||||||
);
|
);
|
||||||
|
|
||||||
const medias = await Promise.all(
|
const medias = await Promise.all(
|
||||||
await mediaServices.map(async (service) => {
|
await mediaApps.map(async (app) => {
|
||||||
const integration = service.integration!;
|
const integration = app.integration!;
|
||||||
const endpoint = IntegrationTypeEndpointMap.get(integration.type);
|
const endpoint = IntegrationTypeEndpointMap.get(integration.type);
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
return {
|
return {
|
||||||
@@ -57,7 +57,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the origin URL
|
// Get the origin URL
|
||||||
let { href: origin } = new URL(service.url);
|
let { href: origin } = new URL(app.url);
|
||||||
if (origin.endsWith('/')) {
|
if (origin.endsWith('/')) {
|
||||||
origin = origin.slice(0, -1);
|
origin = origin.slice(0, -1);
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const IntegrationTypeEndpointMap = new Map<ServiceIntegrationType['type'], string>([
|
const IntegrationTypeEndpointMap = new Map<AppIntegrationType['type'], string>([
|
||||||
['sonarr', '/api/calendar'],
|
['sonarr', '/api/calendar'],
|
||||||
['radarr', '/api/v3/calendar'],
|
['radarr', '/api/v3/calendar'],
|
||||||
['lidarr', '/api/v1/calendar'],
|
['lidarr', '/api/v1/calendar'],
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const { id, type } = req.query as { id: string; type: string };
|
const { id, type } = req.query as { id: string; type: string };
|
||||||
const configName = getCookie('config-name', { req });
|
const configName = getCookie('config-name', { req });
|
||||||
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
|
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
|
||||||
const service = config.services.find(
|
const app = config.apps.find(
|
||||||
(service) => service.type === 'Overseerr' || service.type === 'Jellyseerr'
|
(app) => app.type === 'Overseerr' || app.type === 'Jellyseerr'
|
||||||
);
|
);
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return res.status(400).json({ error: 'No id provided' });
|
return res.status(400).json({ error: 'No id provided' });
|
||||||
@@ -20,18 +20,18 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
if (!type) {
|
if (!type) {
|
||||||
return res.status(400).json({ error: 'No type provided' });
|
return res.status(400).json({ error: 'No type provided' });
|
||||||
}
|
}
|
||||||
if (!service?.apiKey) {
|
if (!app?.apiKey) {
|
||||||
return res.status(400).json({ error: 'No service found' });
|
return res.status(400).json({ error: 'No apps found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const serviceUrl = new URL(service.url);
|
const appUrl = new URL(app.url);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'movie':
|
case 'movie':
|
||||||
return axios
|
return axios
|
||||||
.get(`${serviceUrl.origin}/api/v1/movie/${id}`, {
|
.get(`${appUrl.origin}/api/v1/movie/${id}`, {
|
||||||
headers: {
|
headers: {
|
||||||
// Set X-Api-Key to the value of the API key
|
// Set X-Api-Key to the value of the API key
|
||||||
'X-Api-Key': service.apiKey,
|
'X-Api-Key': app.apiKey,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((axiosres) => res.status(200).json(axiosres.data))
|
.then((axiosres) => res.status(200).json(axiosres.data))
|
||||||
@@ -45,10 +45,10 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
case 'tv':
|
case 'tv':
|
||||||
// Make request to the tv api
|
// Make request to the tv api
|
||||||
return axios
|
return axios
|
||||||
.get(`${serviceUrl.origin}/api/v1/tv/${id}`, {
|
.get(`${appUrl.origin}/api/v1/tv/${id}`, {
|
||||||
headers: {
|
headers: {
|
||||||
// Set X-Api-Key to the value of the API key
|
// Set X-Api-Key to the value of the API key
|
||||||
'X-Api-Key': service.apiKey,
|
'X-Api-Key': app.apiKey,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((axiosres) => res.status(200).json(axiosres.data))
|
.then((axiosres) => res.status(200).json(axiosres.data))
|
||||||
@@ -72,8 +72,8 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const { seasons, type } = req.body as { seasons?: number[]; type: MediaType };
|
const { seasons, type } = req.body as { seasons?: number[]; type: MediaType };
|
||||||
const configName = getCookie('config-name', { req });
|
const configName = getCookie('config-name', { req });
|
||||||
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
|
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
|
||||||
const service = config.services.find(
|
const app = config.apps.find(
|
||||||
(service) => service.type === 'Overseerr' || service.type === 'Jellyseerr'
|
(app) => app.type === 'Overseerr' || app.type === 'Jellyseerr'
|
||||||
);
|
);
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return res.status(400).json({ error: 'No id provided' });
|
return res.status(400).json({ error: 'No id provided' });
|
||||||
@@ -81,13 +81,13 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
if (!type) {
|
if (!type) {
|
||||||
return res.status(400).json({ error: 'No type provided' });
|
return res.status(400).json({ error: 'No type provided' });
|
||||||
}
|
}
|
||||||
if (!service?.apiKey) {
|
if (!app?.apiKey) {
|
||||||
return res.status(400).json({ error: 'No service found' });
|
return res.status(400).json({ error: 'No app found' });
|
||||||
}
|
}
|
||||||
if (type === 'movie' && !seasons) {
|
if (type === 'movie' && !seasons) {
|
||||||
return res.status(400).json({ error: 'No seasons provided' });
|
return res.status(400).json({ error: 'No seasons provided' });
|
||||||
}
|
}
|
||||||
const serviceUrl = new URL(service.url);
|
const appUrl = new URL(app.url);
|
||||||
Consola.info('Got an Overseerr request with these arguments', {
|
Consola.info('Got an Overseerr request with these arguments', {
|
||||||
mediaType: type,
|
mediaType: type,
|
||||||
mediaId: id,
|
mediaId: id,
|
||||||
@@ -95,7 +95,7 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
});
|
});
|
||||||
return axios
|
return axios
|
||||||
.post(
|
.post(
|
||||||
`${serviceUrl.origin}/api/v1/request`,
|
`${appUrl.origin}/api/v1/request`,
|
||||||
{
|
{
|
||||||
mediaType: type,
|
mediaType: type,
|
||||||
mediaId: Number(id),
|
mediaId: Number(id),
|
||||||
@@ -104,7 +104,7 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
// Set X-Api-Key to the value of the API key
|
// Set X-Api-Key to the value of the API key
|
||||||
'X-Api-Key': service.apiKey,
|
'X-Api-Key': app.apiKey,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,24 +8,24 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const configName = getCookie('config-name', { req });
|
const configName = getCookie('config-name', { req });
|
||||||
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
|
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
|
||||||
const { query } = req.query;
|
const { query } = req.query;
|
||||||
const service = config.services.find(
|
const app = config.apps.find(
|
||||||
(service) => service.type === 'Overseerr' || service.type === 'Jellyseerr'
|
(app) => app.type === 'Overseerr' || app.type === 'Jellyseerr'
|
||||||
);
|
);
|
||||||
// If query is an empty string, return an empty array
|
// If query is an empty string, return an empty array
|
||||||
if (query === '' || query === undefined) {
|
if (query === '' || query === undefined) {
|
||||||
return res.status(200).json([]);
|
return res.status(200).json([]);
|
||||||
}
|
}
|
||||||
if (!service || !query || service === undefined || !service.apiKey) {
|
if (!app || !query || app === undefined || !app.apiKey) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: 'Wrong request',
|
error: 'Wrong request',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const serviceUrl = new URL(service.url);
|
const appUrl = new URL(app.url);
|
||||||
const data = await axios
|
const data = await axios
|
||||||
.get(`${serviceUrl.origin}/api/v1/search?query=${query}`, {
|
.get(`${appUrl.origin}/api/v1/search?query=${query}`, {
|
||||||
headers: {
|
headers: {
|
||||||
// Set X-Api-Key to the value of the API key
|
// Set X-Api-Key to the value of the API key
|
||||||
'X-Api-Key': service.apiKey,
|
'X-Api-Key': app.apiKey,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((res) => res.data);
|
.then((res) => res.data);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ping from 'ping';
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
// Parse req.body as a ServiceItem
|
// Parse req.body as a AppItem
|
||||||
const { url } = req.query;
|
const { url } = req.query;
|
||||||
// Parse url as URL object
|
// Parse url as URL object
|
||||||
const parsedUrl = new URL(url as string);
|
const parsedUrl = new URL(url as string);
|
||||||
|
|||||||
@@ -8,50 +8,50 @@ import { getConfig } from '../../../tools/getConfig';
|
|||||||
import { Config } from '../../../tools/types';
|
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 app from the request url
|
||||||
const configName = getCookie('config-name', { req });
|
const configName = getCookie('config-name', { req });
|
||||||
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
|
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
|
||||||
const qBittorrentServices = config.services.filter((service) => service.type === 'qBittorrent');
|
const qBittorrentApp = config.apps.filter((app) => app.type === 'qBittorrent');
|
||||||
const delugeServices = config.services.filter((service) => service.type === 'Deluge');
|
const delugeApp = config.apps.filter((app) => app.type === 'Deluge');
|
||||||
const transmissionServices = config.services.filter((service) => service.type === 'Transmission');
|
const transmissionApp = config.apps.filter((app) => app.type === 'Transmission');
|
||||||
|
|
||||||
const torrents: NormalizedTorrent[] = [];
|
const torrents: NormalizedTorrent[] = [];
|
||||||
|
|
||||||
if (!qBittorrentServices && !delugeServices && !transmissionServices) {
|
if (!qBittorrentApp && !delugeApp && !transmissionApp) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
message: 'Missing services',
|
message: 'Missing apps',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
qBittorrentServices.map((service) =>
|
qBittorrentApp.map((apps) =>
|
||||||
new QBittorrent({
|
new QBittorrent({
|
||||||
baseUrl: service.url,
|
baseUrl: apps.url,
|
||||||
username: service.username,
|
username: apps.username,
|
||||||
password: service.password,
|
password: apps.password,
|
||||||
})
|
})
|
||||||
.getAllData()
|
.getAllData()
|
||||||
.then((e) => torrents.push(...e.torrents))
|
.then((e) => torrents.push(...e.torrents))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
delugeServices.map((service) =>
|
delugeApp.map((apps) =>
|
||||||
new Deluge({
|
new Deluge({
|
||||||
baseUrl: service.url,
|
baseUrl: apps.url,
|
||||||
password: 'password' in service ? service.password : '',
|
password: 'password' in apps ? apps.password : '',
|
||||||
})
|
})
|
||||||
.getAllData()
|
.getAllData()
|
||||||
.then((e) => torrents.push(...e.torrents))
|
.then((e) => torrents.push(...e.torrents))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
// Map transmissionServices
|
// Map transmissionApps
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
transmissionServices.map((service) =>
|
transmissionApp.map((apps) =>
|
||||||
new Transmission({
|
new Transmission({
|
||||||
baseUrl: service.url,
|
baseUrl: apps.url,
|
||||||
username: 'username' in service ? service.username : '',
|
username: 'username' in apps ? apps.username : '',
|
||||||
password: 'password' in service ? service.password : '',
|
password: 'password' in apps ? apps.password : '',
|
||||||
})
|
})
|
||||||
.getAllData()
|
.getAllData()
|
||||||
.then((e) => torrents.push(...e.torrents))
|
.then((e) => torrents.push(...e.torrents))
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { UsenetHistoryItem } from '../../../../components/Dashboard/Tiles/UseNet
|
|||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
export interface UsenetHistoryRequestParams {
|
export interface UsenetHistoryRequestParams {
|
||||||
serviceId: string;
|
appId: string;
|
||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
@@ -25,25 +25,25 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
try {
|
try {
|
||||||
const configName = getCookie('config-name', { req });
|
const configName = getCookie('config-name', { req });
|
||||||
const config = getConfig(configName?.toString() ?? 'default');
|
const config = getConfig(configName?.toString() ?? 'default');
|
||||||
const { limit, offset, serviceId } = req.query as any as UsenetHistoryRequestParams;
|
const { limit, offset, appId } = req.query as any as UsenetHistoryRequestParams;
|
||||||
|
|
||||||
const service = config.services.find((x) => x.id === serviceId);
|
const app = config.apps.find((x) => x.id === appId);
|
||||||
|
|
||||||
if (!service) {
|
if (!app) {
|
||||||
throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`);
|
throw new Error(`App with ID "${req.query.appId}" could not be found.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let response: UsenetHistoryResponse;
|
let response: UsenetHistoryResponse;
|
||||||
switch (service.integration?.type) {
|
switch (app.integration?.type) {
|
||||||
case 'nzbGet': {
|
case 'nzbGet': {
|
||||||
const url = new URL(service.url);
|
const url = new URL(app.url);
|
||||||
const options = {
|
const options = {
|
||||||
host: url.hostname,
|
host: url.hostname,
|
||||||
port: url.port,
|
port: url.port,
|
||||||
login:
|
login:
|
||||||
service.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
||||||
hash:
|
hash:
|
||||||
service.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nzbGet = NzbgetClient(options);
|
const nzbGet = NzbgetClient(options);
|
||||||
@@ -77,11 +77,11 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'sabnzbd': {
|
case 'sabnzbd': {
|
||||||
const { origin } = new URL(service.url);
|
const { origin } = new URL(app.url);
|
||||||
|
|
||||||
const apiKey = service.integration.properties.find((x) => x.field === 'apiKey')?.value;
|
const apiKey = app.integration.properties.find((x) => x.field === 'apiKey')?.value;
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error(`API Key for service "${service.name}" is missing`);
|
throw new Error(`API Key for app "${app.name}" is missing`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const history = await new Client(origin, apiKey).history(offset, limit);
|
const history = await new Client(origin, apiKey).history(offset, limit);
|
||||||
@@ -100,7 +100,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Service type "${service.integration?.type}" unrecognized.`);
|
throw new Error(`App type "${app.integration?.type}" unrecognized.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json(response);
|
return res.status(200).json(response);
|
||||||
|
|||||||
@@ -3,16 +3,14 @@ import dayjs from 'dayjs';
|
|||||||
import duration from 'dayjs/plugin/duration';
|
import duration from 'dayjs/plugin/duration';
|
||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { Client } from 'sabnzbd-api';
|
import { Client } from 'sabnzbd-api';
|
||||||
import { getServiceById } from '../../../../tools/hooks/useGetServiceByType';
|
|
||||||
import { Config } from '../../../../tools/types';
|
|
||||||
import { NzbgetStatus } from './nzbget/types';
|
|
||||||
import { NzbgetClient } from './nzbget/nzbget-client';
|
|
||||||
import { getConfig } from '../../../../tools/config/getConfig';
|
import { getConfig } from '../../../../tools/config/getConfig';
|
||||||
|
import { NzbgetClient } from './nzbget/nzbget-client';
|
||||||
|
import { NzbgetStatus } from './nzbget/types';
|
||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
export interface UsenetInfoRequestParams {
|
export interface UsenetInfoRequestParams {
|
||||||
serviceId: string;
|
appId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UsenetInfoResponse {
|
export interface UsenetInfoResponse {
|
||||||
@@ -26,25 +24,25 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
try {
|
try {
|
||||||
const configName = getCookie('config-name', { req });
|
const configName = getCookie('config-name', { req });
|
||||||
const config = getConfig(configName?.toString() ?? 'default');
|
const config = getConfig(configName?.toString() ?? 'default');
|
||||||
const { serviceId } = req.query as any as UsenetInfoRequestParams;
|
const { appId } = req.query as any as UsenetInfoRequestParams;
|
||||||
|
|
||||||
const service = config.services.find((x) => x.id === serviceId);
|
const app = config.apps.find((x) => x.id === appId);
|
||||||
|
|
||||||
if (!service) {
|
if (!app) {
|
||||||
throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`);
|
throw new Error(`App with ID "${req.query.appId}" could not be found.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let response: UsenetInfoResponse;
|
let response: UsenetInfoResponse;
|
||||||
switch (service.integration?.type) {
|
switch (app.integration?.type) {
|
||||||
case 'nzbGet': {
|
case 'nzbGet': {
|
||||||
const url = new URL(service.url);
|
const url = new URL(app.url);
|
||||||
const options = {
|
const options = {
|
||||||
host: url.hostname,
|
host: url.hostname,
|
||||||
port: url.port,
|
port: url.port,
|
||||||
login:
|
login:
|
||||||
service.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
||||||
hash:
|
hash:
|
||||||
service.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nzbGet = NzbgetClient(options);
|
const nzbGet = NzbgetClient(options);
|
||||||
@@ -74,12 +72,12 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'sabnzbd': {
|
case 'sabnzbd': {
|
||||||
const apiKey = service.integration.properties.find((x) => x.field === 'apiKey')?.value;
|
const apiKey = app.integration.properties.find((x) => x.field === 'apiKey')?.value;
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error(`API Key for service "${service.name}" is missing`);
|
throw new Error(`API Key for app "${app.name}" is missing`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { origin } = new URL(service.url);
|
const { origin } = new URL(app.url);
|
||||||
|
|
||||||
const queue = await new Client(origin, apiKey).queue(0, -1);
|
const queue = await new Client(origin, apiKey).queue(0, -1);
|
||||||
|
|
||||||
@@ -99,7 +97,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Service type "${service.integration?.type}" unrecognized.`);
|
throw new Error(`App type "${app.integration?.type}" unrecognized.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json(response);
|
return res.status(200).json(response);
|
||||||
|
|||||||
@@ -3,19 +3,19 @@ import { NzbgetClientOptions } from './types';
|
|||||||
|
|
||||||
export function NzbgetClient(options: NzbgetClientOptions) {
|
export function NzbgetClient(options: NzbgetClientOptions) {
|
||||||
if (!options?.host) {
|
if (!options?.host) {
|
||||||
throw new Error('Cannot connect to NZBGet. Missing host in service config.');
|
throw new Error('Cannot connect to NZBGet. Missing host in app config.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options?.port) {
|
if (!options?.port) {
|
||||||
throw new Error('Cannot connect to NZBGet. Missing port in service config.');
|
throw new Error('Cannot connect to NZBGet. Missing port in app config.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options?.login) {
|
if (!options?.login) {
|
||||||
throw new Error('Cannot connect to NZBGet. Missing username in service config.');
|
throw new Error('Cannot connect to NZBGet. Missing username in app config.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options?.hash) {
|
if (!options?.hash) {
|
||||||
throw new Error('Cannot connect to NZBGet. Missing password in service config.');
|
throw new Error('Cannot connect to NZBGet. Missing password in app config.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return new NZBGet(options);
|
return new NZBGet(options);
|
||||||
|
|||||||
@@ -9,32 +9,32 @@ import { NzbgetClient } from './nzbget/nzbget-client';
|
|||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
export interface UsenetPauseRequestParams {
|
export interface UsenetPauseRequestParams {
|
||||||
serviceId: string;
|
appId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function Post(req: NextApiRequest, res: NextApiResponse) {
|
async function Post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
const configName = getCookie('config-name', { req });
|
const configName = getCookie('config-name', { req });
|
||||||
const config = getConfig(configName?.toString() ?? 'default');
|
const config = getConfig(configName?.toString() ?? 'default');
|
||||||
const { serviceId } = req.query as any as UsenetPauseRequestParams;
|
const { appId } = req.query as any as UsenetPauseRequestParams;
|
||||||
|
|
||||||
const service = config.services.find((x) => x.id === serviceId);
|
const app = config.apps.find((x) => x.id === appId);
|
||||||
|
|
||||||
if (!service) {
|
if (!app) {
|
||||||
throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`);
|
throw new Error(`App with ID "${req.query.appId}" could not be found.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let result;
|
let result;
|
||||||
switch (service.integration?.type) {
|
switch (app.integration?.type) {
|
||||||
case 'nzbGet': {
|
case 'nzbGet': {
|
||||||
const url = new URL(service.url);
|
const url = new URL(app.url);
|
||||||
const options = {
|
const options = {
|
||||||
host: url.hostname,
|
host: url.hostname,
|
||||||
port: url.port,
|
port: url.port,
|
||||||
login:
|
login:
|
||||||
service.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
||||||
hash:
|
hash:
|
||||||
service.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nzbGet = NzbgetClient(options);
|
const nzbGet = NzbgetClient(options);
|
||||||
@@ -51,18 +51,18 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'sabnzbd': {
|
case 'sabnzbd': {
|
||||||
const apiKey = service.integration.properties.find((x) => x.field === 'apiKey')?.value;
|
const apiKey = app.integration.properties.find((x) => x.field === 'apiKey')?.value;
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error(`API Key for service "${service.name}" is missing`);
|
throw new Error(`API Key for app "${app.name}" is missing`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { origin } = new URL(service.url);
|
const { origin } = new URL(app.url);
|
||||||
|
|
||||||
result = await new Client(origin, apiKey).queuePause();
|
result = await new Client(origin, apiKey).queuePause();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Service type "${service.integration?.type}" unrecognized.`);
|
throw new Error(`App type "${app.integration?.type}" unrecognized.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json(result);
|
return res.status(200).json(result);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { NzbgetQueueItem, NzbgetStatus } from './nzbget/types';
|
|||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
export interface UsenetQueueRequestParams {
|
export interface UsenetQueueRequestParams {
|
||||||
serviceId: string;
|
appId: string;
|
||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
@@ -25,25 +25,25 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
try {
|
try {
|
||||||
const configName = getCookie('config-name', { req });
|
const configName = getCookie('config-name', { req });
|
||||||
const config = getConfig(configName?.toString() ?? 'default');
|
const config = getConfig(configName?.toString() ?? 'default');
|
||||||
const { limit, offset, serviceId } = req.query as any as UsenetQueueRequestParams;
|
const { limit, offset, appId } = req.query as any as UsenetQueueRequestParams;
|
||||||
|
|
||||||
const service = config.services.find((x) => x.id === serviceId);
|
const app = config.apps.find((x) => x.id === appId);
|
||||||
|
|
||||||
if (!service) {
|
if (!app) {
|
||||||
throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`);
|
throw new Error(`App with ID "${req.query.appId}" could not be found.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let response: UsenetQueueResponse;
|
let response: UsenetQueueResponse;
|
||||||
switch (service.integration?.type) {
|
switch (app.integration?.type) {
|
||||||
case 'nzbGet': {
|
case 'nzbGet': {
|
||||||
const url = new URL(service.url);
|
const url = new URL(app.url);
|
||||||
const options = {
|
const options = {
|
||||||
host: url.hostname,
|
host: url.hostname,
|
||||||
port: url.port,
|
port: url.port,
|
||||||
login:
|
login:
|
||||||
service.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
||||||
hash:
|
hash:
|
||||||
service.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nzbGet = NzbgetClient(options);
|
const nzbGet = NzbgetClient(options);
|
||||||
@@ -93,12 +93,12 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'sabnzbd': {
|
case 'sabnzbd': {
|
||||||
const apiKey = service.integration.properties.find((x) => x.field === 'apiKey')?.value;
|
const apiKey = app.integration.properties.find((x) => x.field === 'apiKey')?.value;
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error(`API Key for service "${service.name}" is missing`);
|
throw new Error(`API Key for app "${app.name}" is missing`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { origin } = new URL(service.url);
|
const { origin } = new URL(app.url);
|
||||||
const queue = await new Client(origin, apiKey).queue(offset, limit);
|
const queue = await new Client(origin, apiKey).queue(offset, limit);
|
||||||
|
|
||||||
const items: UsenetQueueItem[] = queue.slots.map((slot) => {
|
const items: UsenetQueueItem[] = queue.slots.map((slot) => {
|
||||||
@@ -126,7 +126,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Service type "${service.integration?.type}" unrecognized.`);
|
throw new Error(`App type "${app.integration?.type}" unrecognized.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json(response);
|
return res.status(200).json(response);
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import duration from 'dayjs/plugin/duration';
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { Client } from 'sabnzbd-api';
|
import { Client } from 'sabnzbd-api';
|
||||||
import { getConfig } from '../../../../tools/config/getConfig';
|
import { getConfig } from '../../../../tools/config/getConfig';
|
||||||
import { getServiceById } from '../../../../tools/hooks/useGetServiceByType';
|
|
||||||
import { Config } from '../../../../tools/types';
|
|
||||||
import { NzbgetClient } from './nzbget/nzbget-client';
|
import { NzbgetClient } from './nzbget/nzbget-client';
|
||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
export interface UsenetResumeRequestParams {
|
export interface UsenetResumeRequestParams {
|
||||||
serviceId: string;
|
appId: string;
|
||||||
nzbId?: string;
|
nzbId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,25 +17,25 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
try {
|
try {
|
||||||
const configName = getCookie('config-name', { req });
|
const configName = getCookie('config-name', { req });
|
||||||
const config = getConfig(configName?.toString() ?? 'default');
|
const config = getConfig(configName?.toString() ?? 'default');
|
||||||
const { serviceId } = req.query as any as UsenetResumeRequestParams;
|
const { appId } = req.query as any as UsenetResumeRequestParams;
|
||||||
|
|
||||||
const service = config.services.find((x) => x.id === serviceId);
|
const app = config.apps.find((x) => x.id === appId);
|
||||||
|
|
||||||
if (!service) {
|
if (!app) {
|
||||||
throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`);
|
throw new Error(`App with ID "${req.query.appId}" could not be found.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let result;
|
let result;
|
||||||
switch (service.integration?.type) {
|
switch (app.integration?.type) {
|
||||||
case 'nzbGet': {
|
case 'nzbGet': {
|
||||||
const url = new URL(service.url);
|
const url = new URL(app.url);
|
||||||
const options = {
|
const options = {
|
||||||
host: url.hostname,
|
host: url.hostname,
|
||||||
port: url.port,
|
port: url.port,
|
||||||
login:
|
login:
|
||||||
service.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
||||||
hash:
|
hash:
|
||||||
service.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nzbGet = NzbgetClient(options);
|
const nzbGet = NzbgetClient(options);
|
||||||
@@ -54,18 +52,18 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'sabnzbd': {
|
case 'sabnzbd': {
|
||||||
const apiKey = service.integration.properties.find((x) => x.field === 'apiKey')?.value;
|
const apiKey = app.integration.properties.find((x) => x.field === 'apiKey')?.value;
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error(`API Key for service "${service.name}" is missing`);
|
throw new Error(`API Key for app "${app.name}" is missing`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { origin } = new URL(service.url);
|
const { origin } = new URL(app.url);
|
||||||
|
|
||||||
result = await new Client(origin, apiKey).queueResume();
|
result = await new Client(origin, apiKey).queueResume();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Service type "${service.integration?.type}" unrecognized.`);
|
throw new Error(`App type "${app.integration?.type}" unrecognized.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json(result);
|
return res.status(200).json(result);
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ export default async function addToHomarr(
|
|||||||
) {
|
) {
|
||||||
setConfig({
|
setConfig({
|
||||||
...config,
|
...config,
|
||||||
services: [
|
apps: [
|
||||||
...config.services,
|
...config.apps,
|
||||||
{
|
{
|
||||||
name: container.Names[0].substring(1),
|
name: container.Names[0].substring(1),
|
||||||
id: container.Id,
|
id: container.Id,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const getFallbackConfig = (name?: string): BackendConfigType => ({
|
|||||||
},
|
},
|
||||||
categories: [],
|
categories: [],
|
||||||
integrations: {},
|
integrations: {},
|
||||||
services: [],
|
apps: [],
|
||||||
settings: {
|
settings: {
|
||||||
common: {
|
common: {
|
||||||
searchEngine: {
|
searchEngine: {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const getFrontendConfig = (name: string): ConfigType => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
services: config.services.map((s) => ({
|
apps: config.apps.map((s) => ({
|
||||||
...s,
|
...s,
|
||||||
integration: s.integration
|
integration: s.integration
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const POLLING_INTERVAL = 2000;
|
|||||||
|
|
||||||
export const useGetUsenetInfo = (params: UsenetInfoRequestParams) =>
|
export const useGetUsenetInfo = (params: UsenetInfoRequestParams) =>
|
||||||
useQuery(
|
useQuery(
|
||||||
['usenetInfo', params.serviceId],
|
['usenetInfo', params.appId],
|
||||||
async () =>
|
async () =>
|
||||||
(
|
(
|
||||||
await axios.get<UsenetInfoResponse>('/api/modules/usenet', {
|
await axios.get<UsenetInfoResponse>('/api/modules/usenet', {
|
||||||
@@ -29,7 +29,7 @@ export const useGetUsenetInfo = (params: UsenetInfoRequestParams) =>
|
|||||||
refetchInterval: POLLING_INTERVAL,
|
refetchInterval: POLLING_INTERVAL,
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
retry: 2,
|
retry: 2,
|
||||||
enabled: !!params.serviceId,
|
enabled: !!params.appId,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -80,14 +80,14 @@ export const usePauseUsenetQueue = (params: UsenetPauseRequestParams) =>
|
|||||||
).data,
|
).data,
|
||||||
{
|
{
|
||||||
async onMutate() {
|
async onMutate() {
|
||||||
await queryClient.cancelQueries(['usenetInfo', params.serviceId]);
|
await queryClient.cancelQueries(['usenetInfo', params.appId]);
|
||||||
const previousInfo = queryClient.getQueryData<UsenetInfoResponse>([
|
const previousInfo = queryClient.getQueryData<UsenetInfoResponse>([
|
||||||
'usenetInfo',
|
'usenetInfo',
|
||||||
params.serviceId,
|
params.appId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (previousInfo) {
|
if (previousInfo) {
|
||||||
queryClient.setQueryData<UsenetInfoResponse>(['usenetInfo', params.serviceId], {
|
queryClient.setQueryData<UsenetInfoResponse>(['usenetInfo', params.appId], {
|
||||||
...previousInfo,
|
...previousInfo,
|
||||||
paused: true,
|
paused: true,
|
||||||
});
|
});
|
||||||
@@ -98,13 +98,13 @@ export const usePauseUsenetQueue = (params: UsenetPauseRequestParams) =>
|
|||||||
onError(err, _, context) {
|
onError(err, _, context) {
|
||||||
if (context?.previousInfo) {
|
if (context?.previousInfo) {
|
||||||
queryClient.setQueryData<UsenetInfoResponse>(
|
queryClient.setQueryData<UsenetInfoResponse>(
|
||||||
['usenetInfo', params.serviceId],
|
['usenetInfo', params.appId],
|
||||||
context.previousInfo
|
context.previousInfo
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSettled() {
|
onSettled() {
|
||||||
queryClient.invalidateQueries(['usenetInfo', params.serviceId]);
|
queryClient.invalidateQueries(['usenetInfo', params.appId]);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -124,14 +124,14 @@ export const useResumeUsenetQueue = (params: UsenetResumeRequestParams) =>
|
|||||||
).data,
|
).data,
|
||||||
{
|
{
|
||||||
async onMutate() {
|
async onMutate() {
|
||||||
await queryClient.cancelQueries(['usenetInfo', params.serviceId]);
|
await queryClient.cancelQueries(['usenetInfo', params.appId]);
|
||||||
const previousInfo = queryClient.getQueryData<UsenetInfoResponse>([
|
const previousInfo = queryClient.getQueryData<UsenetInfoResponse>([
|
||||||
'usenetInfo',
|
'usenetInfo',
|
||||||
params.serviceId,
|
params.appId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (previousInfo) {
|
if (previousInfo) {
|
||||||
queryClient.setQueryData<UsenetInfoResponse>(['usenetInfo', params.serviceId], {
|
queryClient.setQueryData<UsenetInfoResponse>(['usenetInfo', params.appId], {
|
||||||
...previousInfo,
|
...previousInfo,
|
||||||
paused: false,
|
paused: false,
|
||||||
});
|
});
|
||||||
@@ -142,13 +142,13 @@ export const useResumeUsenetQueue = (params: UsenetResumeRequestParams) =>
|
|||||||
onError(err, _, context) {
|
onError(err, _, context) {
|
||||||
if (context?.previousInfo) {
|
if (context?.previousInfo) {
|
||||||
queryClient.setQueryData<UsenetInfoResponse>(
|
queryClient.setQueryData<UsenetInfoResponse>(
|
||||||
['usenetInfo', params.serviceId],
|
['usenetInfo', params.appId],
|
||||||
context.previousInfo
|
context.previousInfo
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSettled() {
|
onSettled() {
|
||||||
queryClient.invalidateQueries(['usenetInfo', params.serviceId]);
|
queryClient.invalidateQueries(['usenetInfo', params.appId]);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const useGetServiceByType = (...serviceTypes: ServiceType[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getServiceByType = (config: Config, ...serviceTypes: ServiceType[]) =>
|
export const getServiceByType = (config: Config, ...serviceTypes: ServiceType[]) =>
|
||||||
config.services.filter((s) => serviceTypes.includes(s.type));
|
config.apps.filter((s) => serviceTypes.includes(s.type));
|
||||||
|
|
||||||
export const getServiceById = (config: Config, id: string) =>
|
export const getServiceById = (config: Config, id: string) =>
|
||||||
config.services.find((s) => s.id === id);
|
config.apps.find((s) => s.id === id);
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { Config } from './types';
|
|||||||
|
|
||||||
export function migrateToIdConfig(config: Config): Config {
|
export function migrateToIdConfig(config: Config): Config {
|
||||||
// Set the config and add an ID to all the services that don't have one
|
// Set the config and add an ID to all the services that don't have one
|
||||||
const services = config.services.map((service) => ({
|
const services = config.apps.map((service) => ({
|
||||||
...service,
|
...service,
|
||||||
id: service.id ?? uuidv4(),
|
id: service.id ?? uuidv4(),
|
||||||
}));
|
}));
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
services,
|
apps: services,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type configContextType = {
|
|||||||
const configContext = createContext<configContextType>({
|
const configContext = createContext<configContextType>({
|
||||||
config: {
|
config: {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
services: [],
|
apps: [],
|
||||||
settings: {
|
settings: {
|
||||||
searchUrl: 'https://google.com/search?q=',
|
searchUrl: 'https://google.com/search?q=',
|
||||||
},
|
},
|
||||||
@@ -41,7 +41,7 @@ type Props = {
|
|||||||
export function ConfigProvider({ children }: Props) {
|
export function ConfigProvider({ children }: Props) {
|
||||||
const [config, setConfigInternal] = useState<Config>({
|
const [config, setConfigInternal] = useState<Config>({
|
||||||
name: 'default',
|
name: 'default',
|
||||||
services: [],
|
apps: [],
|
||||||
settings: {
|
settings: {
|
||||||
searchUrl: 'https://www.google.com/search?q=',
|
searchUrl: 'https://www.google.com/search?q=',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export interface Settings {
|
|||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
name: string;
|
name: string;
|
||||||
services: serviceItem[];
|
apps: serviceItem[];
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
modules: {
|
modules: {
|
||||||
[key: string]: ConfigModule;
|
[key: string]: ConfigModule;
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
import { IconKey, IconPassword, IconUser, TablerIcon } from '@tabler/icons';
|
import { IconKey, IconPassword, IconUser, TablerIcon } from '@tabler/icons';
|
||||||
import { TileBaseType } from './tile';
|
import { TileBaseType } from './tile';
|
||||||
|
|
||||||
export interface ServiceType extends TileBaseType {
|
export interface AppType extends TileBaseType {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
behaviour: ServiceBehaviourType;
|
behaviour: AppBehaviourType;
|
||||||
network: ServiceNetworkType;
|
network: AppNetworkType;
|
||||||
appearance: ServiceAppearanceType;
|
appearance: AppAppearanceType;
|
||||||
integration: ServiceIntegrationType;
|
integration: AppIntegrationType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConfigServiceType = Omit<ServiceType, 'integration'> & {
|
export type ConfigAppType = Omit<AppType, 'integration'> & {
|
||||||
integration?: ConfigServiceIntegrationType | null;
|
integration?: ConfigAppIntegrationType | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ServiceBehaviourType {
|
interface AppBehaviourType {
|
||||||
onClickUrl: string;
|
onClickUrl: string;
|
||||||
isOpeningNewTab: boolean;
|
isOpeningNewTab: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServiceNetworkType {
|
interface AppNetworkType {
|
||||||
enabledStatusChecker: boolean;
|
enabledStatusChecker: boolean;
|
||||||
okStatus: number[];
|
okStatus: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServiceAppearanceType {
|
interface AppAppearanceType {
|
||||||
iconUrl: string;
|
iconUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,28 +42,28 @@ export type IntegrationType =
|
|||||||
| 'transmission'
|
| 'transmission'
|
||||||
| 'nzbGet';
|
| 'nzbGet';
|
||||||
|
|
||||||
export type ServiceIntegrationType = {
|
export type AppIntegrationType = {
|
||||||
type: IntegrationType | null;
|
type: IntegrationType | null;
|
||||||
properties: ServiceIntegrationPropertyType[];
|
properties: AppIntegrationPropertyType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConfigServiceIntegrationType = Omit<ServiceIntegrationType, 'properties'> & {
|
export type ConfigAppIntegrationType = Omit<AppIntegrationType, 'properties'> & {
|
||||||
properties: ConfigServiceIntegrationPropertyType[];
|
properties: ConfigAppIntegrationPropertyType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServiceIntegrationPropertyType = {
|
export type AppIntegrationPropertyType = {
|
||||||
type: 'private' | 'public';
|
type: 'private' | 'public';
|
||||||
field: IntegrationField;
|
field: IntegrationField;
|
||||||
value?: string | undefined;
|
value?: string | undefined;
|
||||||
isDefined: boolean;
|
isDefined: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ConfigServiceIntegrationPropertyType = Omit<ServiceIntegrationPropertyType, 'isDefined'>;
|
type ConfigAppIntegrationPropertyType = Omit<AppIntegrationPropertyType, 'isDefined'>;
|
||||||
|
|
||||||
export type IntegrationField = 'apiKey' | 'password' | 'username';
|
export type IntegrationField = 'apiKey' | 'password' | 'username';
|
||||||
|
|
||||||
export const integrationFieldProperties: {
|
export const integrationFieldProperties: {
|
||||||
[key in ServiceIntegrationType['type']]: IntegrationField[];
|
[key in AppIntegrationType['type']]: IntegrationField[];
|
||||||
} = {
|
} = {
|
||||||
lidarr: ['apiKey'],
|
lidarr: ['apiKey'],
|
||||||
radarr: ['apiKey'],
|
radarr: ['apiKey'],
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { CategoryType } from './category';
|
import { CategoryType } from './category';
|
||||||
import { WrapperType } from './wrapper';
|
import { WrapperType } from './wrapper';
|
||||||
import { ConfigServiceType, ServiceType } from './service';
|
import { ConfigAppType, AppType } from './app';
|
||||||
import { IntegrationsType } from './integration';
|
import { IntegrationsType } from './integration';
|
||||||
import { SettingsType } from './settings';
|
import { SettingsType } from './settings';
|
||||||
|
|
||||||
@@ -9,13 +9,13 @@ export interface ConfigType {
|
|||||||
configProperties: ConfigPropertiesType;
|
configProperties: ConfigPropertiesType;
|
||||||
categories: CategoryType[];
|
categories: CategoryType[];
|
||||||
wrappers: WrapperType[];
|
wrappers: WrapperType[];
|
||||||
services: ServiceType[];
|
apps: AppType[];
|
||||||
integrations: IntegrationsType;
|
integrations: IntegrationsType;
|
||||||
settings: SettingsType;
|
settings: SettingsType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BackendConfigType = Omit<ConfigType, 'services'> & {
|
export type BackendConfigType = Omit<ConfigType, 'apps'> & {
|
||||||
services: ConfigServiceType[];
|
apps: ConfigAppType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ConfigPropertiesType {
|
export interface ConfigPropertiesType {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export const DashDotTile = ({ module, className }: DashDotTileProps) => {
|
|||||||
{menu}
|
{menu}
|
||||||
<div>
|
<div>
|
||||||
{heading}
|
{heading}
|
||||||
<p>{t('card.errors.noService')}</p>
|
<p>{t('card.errors.noApp')}</p>
|
||||||
</div>
|
</div>
|
||||||
</HomarrCardWrapper>
|
</HomarrCardWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { useTranslation } from 'next-i18next';
|
|||||||
import { UsenetQueueList } from './UsenetQueueList';
|
import { UsenetQueueList } from './UsenetQueueList';
|
||||||
import { UsenetHistoryList } from './UsenetHistoryList';
|
import { UsenetHistoryList } from './UsenetHistoryList';
|
||||||
import { BaseTileProps } from '../../components/Dashboard/Tiles/type';
|
import { BaseTileProps } from '../../components/Dashboard/Tiles/type';
|
||||||
import { ServiceIntegrationType } from '../../types/service';
|
import { AppIntegrationType } from '../../types/app';
|
||||||
import { useConfigContext } from '../../config/provider';
|
import { useConfigContext } from '../../config/provider';
|
||||||
import { useGetUsenetInfo, usePauseUsenetQueue, useResumeUsenetQueue } from '../../tools/hooks/api';
|
import { useGetUsenetInfo, usePauseUsenetQueue, useResumeUsenetQueue } from '../../tools/hooks/api';
|
||||||
import { HomarrCardWrapper } from '../../components/Dashboard/Tiles/HomarrCardWrapper';
|
import { HomarrCardWrapper } from '../../components/Dashboard/Tiles/HomarrCardWrapper';
|
||||||
@@ -27,31 +27,31 @@ import { humanFileSize } from '../../tools/humanFileSize';
|
|||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
const downloadServiceTypes: ServiceIntegrationType['type'][] = ['sabnzbd', 'nzbGet'];
|
const downloadAppTypes: AppIntegrationType['type'][] = ['sabnzbd', 'nzbGet'];
|
||||||
|
|
||||||
interface UseNetTileProps extends BaseTileProps {}
|
interface UseNetTileProps extends BaseTileProps {}
|
||||||
|
|
||||||
export const UseNetTile = ({ className }: UseNetTileProps) => {
|
export const UseNetTile = ({ className }: UseNetTileProps) => {
|
||||||
const { t } = useTranslation('modules/usenet');
|
const { t } = useTranslation('modules/usenet');
|
||||||
const { config } = useConfigContext();
|
const { config } = useConfigContext();
|
||||||
const downloadServices =
|
const downloadApps =
|
||||||
config?.services.filter(
|
config?.apps.filter(
|
||||||
(x) => x.integration && downloadServiceTypes.includes(x.integration.type)
|
(x) => x.integration && downloadAppTypes.includes(x.integration.type)
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
const [selectedServiceId, setSelectedService] = useState<string | null>(downloadServices[0]?.id);
|
const [selectedAppId, setSelectedApp] = useState<string | null>(downloadApps[0]?.id);
|
||||||
const { data } = useGetUsenetInfo({ serviceId: selectedServiceId! });
|
const { data } = useGetUsenetInfo({ appId: selectedAppId! });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedServiceId && downloadServices.length) {
|
if (!selectedAppId && downloadApps.length) {
|
||||||
setSelectedService(downloadServices[0].id);
|
setSelectedApp(downloadApps[0].id);
|
||||||
}
|
}
|
||||||
}, [downloadServices, selectedServiceId]);
|
}, [downloadApps, selectedAppId]);
|
||||||
|
|
||||||
const { mutate: pause } = usePauseUsenetQueue({ serviceId: selectedServiceId! });
|
const { mutate: pause } = usePauseUsenetQueue({ appId: selectedAppId! });
|
||||||
const { mutate: resume } = useResumeUsenetQueue({ serviceId: selectedServiceId! });
|
const { mutate: resume } = useResumeUsenetQueue({ appId: selectedAppId! });
|
||||||
|
|
||||||
if (downloadServices.length === 0) {
|
if (downloadApps.length === 0) {
|
||||||
return (
|
return (
|
||||||
<HomarrCardWrapper className={className}>
|
<HomarrCardWrapper className={className}>
|
||||||
<Stack>
|
<Stack>
|
||||||
@@ -64,7 +64,7 @@ export const UseNetTile = ({ className }: UseNetTileProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedServiceId) {
|
if (!selectedAppId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,16 +90,16 @@ export const UseNetTile = ({ className }: UseNetTileProps) => {
|
|||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
{downloadServices.length > 1 && (
|
{downloadApps.length > 1 && (
|
||||||
<Select
|
<Select
|
||||||
value={selectedServiceId}
|
value={selectedAppId}
|
||||||
onChange={setSelectedService}
|
onChange={setSelectedApp}
|
||||||
ml="xs"
|
ml="xs"
|
||||||
data={downloadServices.map((service) => ({ value: service.id, label: service.name }))}
|
data={downloadApps.map((app) => ({ value: app.id, label: app.name }))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Tabs.Panel value="queue">
|
<Tabs.Panel value="queue">
|
||||||
<UsenetQueueList serviceId={selectedServiceId} />
|
<UsenetQueueList appId={selectedAppId} />
|
||||||
{!data ? null : data.paused ? (
|
{!data ? null : data.paused ? (
|
||||||
<Button uppercase onClick={() => resume()} radius="xl" size="xs" fullWidth mt="sm">
|
<Button uppercase onClick={() => resume()} radius="xl" size="xs" fullWidth mt="sm">
|
||||||
<IconPlayerPlay size={12} style={{ marginRight: 5 }} /> {t('info.paused')}
|
<IconPlayerPlay size={12} style={{ marginRight: 5 }} /> {t('info.paused')}
|
||||||
@@ -112,7 +112,7 @@ export const UseNetTile = ({ className }: UseNetTileProps) => {
|
|||||||
)}
|
)}
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
<Tabs.Panel value="history" style={{ display: 'flex', flexDirection: 'column' }}>
|
<Tabs.Panel value="history" style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<UsenetHistoryList serviceId={selectedServiceId} />
|
<UsenetHistoryList appId={selectedAppId} />
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</HomarrCardWrapper>
|
</HomarrCardWrapper>
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ import { parseDuration } from '../../tools/parseDuration';
|
|||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
interface UsenetHistoryListProps {
|
interface UsenetHistoryListProps {
|
||||||
serviceId: string;
|
appId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ serviceId }) => {
|
export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ appId }) => {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const { t } = useTranslation(['modules/usenet', 'common']);
|
const { t } = useTranslation(['modules/usenet', 'common']);
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ s
|
|||||||
const { data, isLoading, isError, error } = useGetUsenetHistory({
|
const { data, isLoading, isError, error } = useGetUsenetHistory({
|
||||||
limit: PAGE_SIZE,
|
limit: PAGE_SIZE,
|
||||||
offset: (page - 1) * PAGE_SIZE,
|
offset: (page - 1) * PAGE_SIZE,
|
||||||
serviceId,
|
appId: appId,
|
||||||
});
|
});
|
||||||
const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE);
|
const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE);
|
||||||
|
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ import { humanFileSize } from '../../tools/humanFileSize';
|
|||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
interface UsenetQueueListProps {
|
interface UsenetQueueListProps {
|
||||||
serviceId: string;
|
appId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ serviceId }) => {
|
export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId }) => {
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const { t } = useTranslation('modules/usenet');
|
const { t } = useTranslation('modules/usenet');
|
||||||
const progressbarBreakpoint = theme.breakpoints.xs;
|
const progressbarBreakpoint = theme.breakpoints.xs;
|
||||||
@@ -44,7 +44,7 @@ export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ servi
|
|||||||
const { data, isLoading, isError, error } = useGetUsenetDownloads({
|
const { data, isLoading, isError, error } = useGetUsenetDownloads({
|
||||||
limit: PAGE_SIZE,
|
limit: PAGE_SIZE,
|
||||||
offset: (page - 1) * PAGE_SIZE,
|
offset: (page - 1) * PAGE_SIZE,
|
||||||
serviceId,
|
appId: appId,
|
||||||
});
|
});
|
||||||
const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE);
|
const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE);
|
||||||
|
|
||||||
|
|||||||
48
yarn.lock
48
yarn.lock
@@ -1114,7 +1114,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@mantine/carousel@npm:^5.9.0":
|
"@mantine/carousel@npm:^5.9.3":
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
resolution: "@mantine/carousel@npm:5.9.3"
|
resolution: "@mantine/carousel@npm:5.9.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1128,7 +1128,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@mantine/core@npm:^5.9.0":
|
"@mantine/core@npm:^5.9.3":
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
resolution: "@mantine/core@npm:5.9.3"
|
resolution: "@mantine/core@npm:5.9.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1145,7 +1145,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@mantine/dates@npm:^5.9.0":
|
"@mantine/dates@npm:^5.9.3":
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
resolution: "@mantine/dates@npm:5.9.3"
|
resolution: "@mantine/dates@npm:5.9.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1159,7 +1159,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@mantine/dropzone@npm:^5.9.0":
|
"@mantine/dropzone@npm:^5.9.3":
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
resolution: "@mantine/dropzone@npm:5.9.3"
|
resolution: "@mantine/dropzone@npm:5.9.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1174,7 +1174,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@mantine/form@npm:^5.9.0":
|
"@mantine/form@npm:^5.9.3":
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
resolution: "@mantine/form@npm:5.9.3"
|
resolution: "@mantine/form@npm:5.9.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1186,7 +1186,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@mantine/hooks@npm:^5.9.0":
|
"@mantine/hooks@npm:^5.9.3":
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
resolution: "@mantine/hooks@npm:5.9.3"
|
resolution: "@mantine/hooks@npm:5.9.3"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1195,7 +1195,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@mantine/modals@npm:^5.9.0":
|
"@mantine/modals@npm:^5.9.3":
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
resolution: "@mantine/modals@npm:5.9.3"
|
resolution: "@mantine/modals@npm:5.9.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1209,7 +1209,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@mantine/next@npm:^5.9.0":
|
"@mantine/next@npm:^5.9.3":
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
resolution: "@mantine/next@npm:5.9.3"
|
resolution: "@mantine/next@npm:5.9.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1223,7 +1223,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@mantine/notifications@npm:^5.9.0":
|
"@mantine/notifications@npm:^5.9.3":
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
resolution: "@mantine/notifications@npm:5.9.3"
|
resolution: "@mantine/notifications@npm:5.9.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1238,7 +1238,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@mantine/prism@npm:^5.9.0":
|
"@mantine/prism@npm:^5.9.3":
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
resolution: "@mantine/prism@npm:5.9.3"
|
resolution: "@mantine/prism@npm:5.9.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4536,7 +4536,7 @@ __metadata:
|
|||||||
|
|
||||||
"fsevents@patch:fsevents@^2.3.2#~builtin<compat/fsevents>, fsevents@patch:fsevents@~2.3.2#~builtin<compat/fsevents>":
|
"fsevents@patch:fsevents@^2.3.2#~builtin<compat/fsevents>, fsevents@patch:fsevents@~2.3.2#~builtin<compat/fsevents>":
|
||||||
version: 2.3.2
|
version: 2.3.2
|
||||||
resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin<compat/fsevents>::version=2.3.2&hash=18f3a7"
|
resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin<compat/fsevents>::version=2.3.2&hash=df0bf1"
|
||||||
dependencies:
|
dependencies:
|
||||||
node-gyp: latest
|
node-gyp: latest
|
||||||
conditions: os=darwin
|
conditions: os=darwin
|
||||||
@@ -4900,16 +4900,16 @@ __metadata:
|
|||||||
"@dnd-kit/utilities": ^3.2.0
|
"@dnd-kit/utilities": ^3.2.0
|
||||||
"@emotion/react": ^11.10.5
|
"@emotion/react": ^11.10.5
|
||||||
"@emotion/server": ^11.10.0
|
"@emotion/server": ^11.10.0
|
||||||
"@mantine/carousel": ^5.9.0
|
"@mantine/carousel": ^5.9.3
|
||||||
"@mantine/core": ^5.9.0
|
"@mantine/core": ^5.9.3
|
||||||
"@mantine/dates": ^5.9.0
|
"@mantine/dates": ^5.9.3
|
||||||
"@mantine/dropzone": ^5.9.0
|
"@mantine/dropzone": ^5.9.3
|
||||||
"@mantine/form": ^5.9.0
|
"@mantine/form": ^5.9.3
|
||||||
"@mantine/hooks": ^5.9.0
|
"@mantine/hooks": ^5.9.3
|
||||||
"@mantine/modals": ^5.9.0
|
"@mantine/modals": ^5.9.3
|
||||||
"@mantine/next": ^5.9.0
|
"@mantine/next": ^5.9.3
|
||||||
"@mantine/notifications": ^5.9.0
|
"@mantine/notifications": ^5.9.3
|
||||||
"@mantine/prism": ^5.9.0
|
"@mantine/prism": ^5.9.3
|
||||||
"@next/bundle-analyzer": ^12.1.4
|
"@next/bundle-analyzer": ^12.1.4
|
||||||
"@next/eslint-plugin-next": ^12.1.4
|
"@next/eslint-plugin-next": ^12.1.4
|
||||||
"@nivo/core": ^0.79.0
|
"@nivo/core": ^0.79.0
|
||||||
@@ -7489,7 +7489,7 @@ __metadata:
|
|||||||
|
|
||||||
"resolve@patch:resolve@^1.19.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.20.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.0#~builtin<compat/resolve>":
|
"resolve@patch:resolve@^1.19.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.20.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.0#~builtin<compat/resolve>":
|
||||||
version: 1.22.1
|
version: 1.22.1
|
||||||
resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin<compat/resolve>::version=1.22.1&hash=07638b"
|
resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin<compat/resolve>::version=1.22.1&hash=c3c19d"
|
||||||
dependencies:
|
dependencies:
|
||||||
is-core-module: ^2.9.0
|
is-core-module: ^2.9.0
|
||||||
path-parse: ^1.0.7
|
path-parse: ^1.0.7
|
||||||
@@ -7502,7 +7502,7 @@ __metadata:
|
|||||||
|
|
||||||
"resolve@patch:resolve@^2.0.0-next.3#~builtin<compat/resolve>":
|
"resolve@patch:resolve@^2.0.0-next.3#~builtin<compat/resolve>":
|
||||||
version: 2.0.0-next.4
|
version: 2.0.0-next.4
|
||||||
resolution: "resolve@patch:resolve@npm%3A2.0.0-next.4#~builtin<compat/resolve>::version=2.0.0-next.4&hash=07638b"
|
resolution: "resolve@patch:resolve@npm%3A2.0.0-next.4#~builtin<compat/resolve>::version=2.0.0-next.4&hash=c3c19d"
|
||||||
dependencies:
|
dependencies:
|
||||||
is-core-module: ^2.9.0
|
is-core-module: ^2.9.0
|
||||||
path-parse: ^1.0.7
|
path-parse: ^1.0.7
|
||||||
@@ -8338,7 +8338,7 @@ __metadata:
|
|||||||
|
|
||||||
"typescript@patch:typescript@^4.7.4#~builtin<compat/typescript>":
|
"typescript@patch:typescript@^4.7.4#~builtin<compat/typescript>":
|
||||||
version: 4.9.4
|
version: 4.9.4
|
||||||
resolution: "typescript@patch:typescript@npm%3A4.9.4#~builtin<compat/typescript>::version=4.9.4&hash=7ad353"
|
resolution: "typescript@patch:typescript@npm%3A4.9.4#~builtin<compat/typescript>::version=4.9.4&hash=d73830"
|
||||||
bin:
|
bin:
|
||||||
tsc: bin/tsc
|
tsc: bin/tsc
|
||||||
tsserver: bin/tsserver
|
tsserver: bin/tsserver
|
||||||
|
|||||||
Reference in New Issue
Block a user