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