mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 15:35:55 +01:00
Merge branch 'gridstack' of https://github.com/manuel-rw/homarr into gridstack
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -4,11 +4,11 @@ import dashDotDefinition from '../../../widgets/dashDot/DashDotTile';
|
||||
import useNetDefinition from '../../../widgets/useNet/UseNetTile';
|
||||
import weatherDefinition 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 any | 'service']: {
|
||||
[key in keyof any | 'app']: {
|
||||
minWidth?: number;
|
||||
minHeight?: number;
|
||||
maxWidth?: number;
|
||||
@@ -18,8 +18,8 @@ type TileDefinitionProps = {
|
||||
};
|
||||
|
||||
export const Tiles: TileDefinitionProps = {
|
||||
service: {
|
||||
component: ServiceTile,
|
||||
app: {
|
||||
component: AppTile,
|
||||
minWidth: 2,
|
||||
maxWidth: 12,
|
||||
minHeight: 2,
|
||||
|
||||
@@ -29,19 +29,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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -31,19 +31,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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -19,19 +19,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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user