mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-07 23:29:16 +01:00
✨ Add the ability to add integrations
This commit is contained in:
121
src/components/Config/Integration/AddIntegrationPanel.tsx
Normal file
121
src/components/Config/Integration/AddIntegrationPanel.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { Button, Group, Stack, TextInput, useMantineTheme } from '@mantine/core';
|
||||
import { UseFormReturnType, useForm } from '@mantine/form';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { QueryKey, useQueryClient } from '@tanstack/react-query';
|
||||
import { getQueryKey } from '@trpc/react-query';
|
||||
import { getCookie } from 'cookies-next';
|
||||
import { produce } from 'immer';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { IntegrationTab } from '~/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/IntegrationTab';
|
||||
import { AppType } from '~/types/app';
|
||||
import { IntegrationTypeMap } from '~/types/config';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
const defaultAppValues: AppType = {
|
||||
id: uuidv4(),
|
||||
name: 'Your app',
|
||||
url: 'https://homarr.dev',
|
||||
appearance: {
|
||||
iconUrl: '/imgs/logo/logo.png',
|
||||
},
|
||||
network: {
|
||||
enabledStatusChecker: true,
|
||||
statusCodes: ['200', '301', '302', '304', '307', '308'],
|
||||
okStatus: [200, 301, 302, 304, 307, 308],
|
||||
},
|
||||
behaviour: {
|
||||
isOpeningNewTab: true,
|
||||
externalUrl: '',
|
||||
},
|
||||
|
||||
area: {
|
||||
type: 'wrapper',
|
||||
properties: {
|
||||
id: 'default',
|
||||
},
|
||||
},
|
||||
shape: {},
|
||||
integration: {
|
||||
id: uuidv4(),
|
||||
url: '',
|
||||
type: null,
|
||||
properties: [],
|
||||
name: 'New integration',
|
||||
},
|
||||
};
|
||||
|
||||
export function AddIntegrationPanel({
|
||||
globalForm,
|
||||
queryKey,
|
||||
integrations,
|
||||
setIntegrations,
|
||||
}: {
|
||||
globalForm: UseFormReturnType<any>;
|
||||
queryKey: QueryKey;
|
||||
integrations: IntegrationTypeMap | undefined;
|
||||
setIntegrations: React.Dispatch<React.SetStateAction<IntegrationTypeMap | undefined>>;
|
||||
}) {
|
||||
const { t } = useTranslation(['settings/integrations', 'common']);
|
||||
const queryClient = useQueryClient();
|
||||
const { primaryColor } = useMantineTheme();
|
||||
|
||||
const form = useForm<AppType>({
|
||||
initialValues: defaultAppValues,
|
||||
});
|
||||
|
||||
if (!integrations) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit(({ integration }) => {
|
||||
if (!integration.type || !integrations) return null;
|
||||
const newIntegrations = produce(integrations, (draft) => {
|
||||
integration.id = uuidv4();
|
||||
// console.log(integration.type);
|
||||
if (!integration.type) return;
|
||||
// If integration type is not in integrations, add it
|
||||
if (!draft[integration.type]) {
|
||||
draft[integration.type] = [];
|
||||
}
|
||||
draft[integration.type].push(integration);
|
||||
});
|
||||
// queryClient.setQueryData(queryKey, newIntegrations);
|
||||
form.reset();
|
||||
setIntegrations(newIntegrations);
|
||||
notifications.show({
|
||||
title: t('integration.Added'),
|
||||
message: t('integration.AddedDescription', { name: integration.name }),
|
||||
color: 'green',
|
||||
});
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<Group grow>
|
||||
<TextInput
|
||||
withAsterisk
|
||||
required
|
||||
label={'URL'}
|
||||
description={t('integration.urlDescription')}
|
||||
placeholder="http://localhost:3039"
|
||||
{...form.getInputProps('integration.url')}
|
||||
/>
|
||||
<TextInput
|
||||
withAsterisk
|
||||
required
|
||||
label={t('integrationName')}
|
||||
description={t('integrationNameDescription')}
|
||||
placeholder="My integration"
|
||||
{...form.getInputProps('integration.name')}
|
||||
/>
|
||||
</Group>
|
||||
<IntegrationTab form={form} />
|
||||
<Button type="submit" color={primaryColor} variant="light">
|
||||
{t('common:add')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import {
|
||||
Accordion,
|
||||
ActionIcon,
|
||||
Button,
|
||||
Group,
|
||||
Image,
|
||||
Loader,
|
||||
Menu,
|
||||
@@ -11,19 +14,36 @@ import {
|
||||
TextInput,
|
||||
Title,
|
||||
rem,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import { AccordionItem } from '@mantine/core/lib/Accordion/AccordionItem/AccordionItem';
|
||||
import { UseFormReturnType, useForm } from '@mantine/form';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconLock, IconPlugConnected } from '@tabler/icons-react';
|
||||
import {
|
||||
IconCheck,
|
||||
IconCircle0Filled,
|
||||
IconCircleX,
|
||||
IconCircleXFilled,
|
||||
IconDeviceFloppy,
|
||||
IconLock,
|
||||
IconPlug,
|
||||
IconPlugConnected,
|
||||
IconPlus,
|
||||
IconTrash,
|
||||
} from '@tabler/icons-react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { getQueryKey } from '@trpc/react-query';
|
||||
import { getCookie, setCookie } from 'cookies-next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { integrationsList } from '~/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector';
|
||||
import { useConfigContext } from '~/config/provider';
|
||||
import { AppIntegrationType, IntegrationType } from '~/types/app';
|
||||
import { IntegrationTypeMap } from '~/types/config';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
import { AddIntegrationPanel } from './AddIntegrationPanel';
|
||||
|
||||
const ModalTitle = ({ title, description }: { title: string; description: string }) => (
|
||||
<div>
|
||||
<Title order={3} style={{ marginBottom: 0 }}>
|
||||
@@ -97,16 +117,31 @@ function IntegrationDisplay({
|
||||
form: UseFormReturnType<any>;
|
||||
}) {
|
||||
if (!integration.type) return null;
|
||||
const { t } = useTranslation('settings/integrations');
|
||||
|
||||
return (
|
||||
<Accordion.Item value={integration.id}>
|
||||
<Accordion.Item key={integration.id} value={integration.id}>
|
||||
<Accordion.Control>{integration.name}</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label="url"
|
||||
{...form.getInputProps(`${integration.type}.${integrationIdx}.url`)}
|
||||
/>
|
||||
<Group grow>
|
||||
<TextInput
|
||||
withAsterisk
|
||||
required
|
||||
label={'URL'}
|
||||
description={t('integration.urlDescription')}
|
||||
placeholder="http://localhost:3039"
|
||||
{...form.getInputProps(`${integration.type}.${integrationIdx}.url`)}
|
||||
/>
|
||||
<TextInput
|
||||
withAsterisk
|
||||
required
|
||||
label={t('integrationName')}
|
||||
description={t('integrationNameDescription')}
|
||||
placeholder="My integration"
|
||||
{...form.getInputProps(`${integration.type}.${integrationIdx}.name`)}
|
||||
/>
|
||||
</Group>
|
||||
{integration.properties.map((property, idx) => {
|
||||
if (property.type === 'private')
|
||||
return (
|
||||
@@ -156,61 +191,138 @@ export interface IntegrationObject {
|
||||
[key: string]: AppIntegrationType;
|
||||
}
|
||||
|
||||
export function IntegrationsAccordion() {
|
||||
export function IntegrationsAccordion({ closeModal }: { closeModal: () => void }) {
|
||||
const { t } = useTranslation('settings/integrations, common');
|
||||
const cookie = getCookie('INTEGRATIONS_PASSWORD');
|
||||
const queryClient = useQueryClient();
|
||||
const { primaryColor } = useMantineTheme();
|
||||
const queryKey = getQueryKey(api.system.checkLogin, { password: cookie?.toString() }, 'query');
|
||||
let integrations: IntegrationTypeMap | undefined = queryClient.getQueryData(queryKey);
|
||||
let integrationsQuery: IntegrationTypeMap | undefined = queryClient.getQueryData(queryKey);
|
||||
const mutation = api.config.save.useMutation();
|
||||
const { config, name } = useConfigContext();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [integrations, setIntegrations] = useState<IntegrationTypeMap | undefined>(
|
||||
integrationsQuery
|
||||
);
|
||||
if (!integrations) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const form = useForm({
|
||||
initialValues: integrations,
|
||||
initialValues: integrationsQuery,
|
||||
});
|
||||
// Loop over integrations item
|
||||
useEffect(() => {
|
||||
if (!integrations) return;
|
||||
form.setValues(integrations);
|
||||
}, [integrations]);
|
||||
|
||||
return (
|
||||
<Accordion variant="separated" multiple>
|
||||
{Object.keys(integrations).map((item) => {
|
||||
if (!integrations) return null;
|
||||
const configIntegrations = integrations[item as keyof IntegrationTypeMap];
|
||||
const integrationListItem = integrationsList.find(
|
||||
(integration) => integration.value === item
|
||||
);
|
||||
if (!configIntegrations || !integrationListItem) return null;
|
||||
return (
|
||||
<Accordion.Item value={integrationListItem.value} key={integrationListItem.value}>
|
||||
<Accordion.Control
|
||||
icon={
|
||||
<Image
|
||||
src={integrationListItem.image}
|
||||
withPlaceholder
|
||||
width={24}
|
||||
height={24}
|
||||
alt={integrationListItem.value}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{integrationListItem.label}
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Accordion variant="separated" radius="md" multiple>
|
||||
{configIntegrations.map((integration, integrationIdx) => {
|
||||
return (
|
||||
<IntegrationDisplay
|
||||
integrationIdx={integrationIdx}
|
||||
form={form}
|
||||
integration={integration}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
<Stack>
|
||||
<Accordion variant="separated" multiple>
|
||||
{Object.keys(integrations).map((item) => {
|
||||
if (!integrations) return null;
|
||||
const configIntegrations = integrations[item as keyof IntegrationTypeMap];
|
||||
const integrationListItem = integrationsList.find(
|
||||
(integration) => integration.value === item
|
||||
);
|
||||
if (!configIntegrations || !integrationListItem) return null;
|
||||
return (
|
||||
<Accordion.Item value={integrationListItem.value} key={integrationListItem.value}>
|
||||
<Accordion.Control
|
||||
icon={
|
||||
<Image
|
||||
src={integrationListItem.image}
|
||||
withPlaceholder
|
||||
width={24}
|
||||
height={24}
|
||||
alt={integrationListItem.value}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{integrationListItem.label}
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Accordion variant="separated" radius="md" multiple>
|
||||
{configIntegrations.map((integration, integrationIdx) => {
|
||||
return (
|
||||
<IntegrationDisplay
|
||||
integrationIdx={integrationIdx}
|
||||
form={form}
|
||||
integration={integration}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
);
|
||||
})}
|
||||
<Accordion.Item value="add-new">
|
||||
<Accordion.Control
|
||||
chevron={
|
||||
<ActionIcon color={primaryColor} radius={'lg'} size={'md'} variant="light">
|
||||
<IconPlus />
|
||||
</ActionIcon>
|
||||
}
|
||||
icon={<IconPlug stroke={2} />}
|
||||
>
|
||||
{t('addNewIntegration')}
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<AddIntegrationPanel
|
||||
globalForm={form}
|
||||
queryKey={queryKey}
|
||||
integrations={integrations}
|
||||
setIntegrations={setIntegrations}
|
||||
/>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
<Group position="right">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="light"
|
||||
color="red"
|
||||
leftIcon={<IconCircleX />}
|
||||
onClick={() => {
|
||||
queryClient.invalidateQueries(queryKey);
|
||||
closeModal();
|
||||
}}
|
||||
>
|
||||
{t('common:close')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
loading={isLoading}
|
||||
leftIcon={<IconDeviceFloppy />}
|
||||
onClick={() => {
|
||||
setIsLoading(true);
|
||||
mutation
|
||||
.mutateAsync({
|
||||
config: {
|
||||
...config,
|
||||
integrations: form.values,
|
||||
},
|
||||
name: name!,
|
||||
})
|
||||
.then(() => {
|
||||
notifications.show({
|
||||
icon: <IconCheck />,
|
||||
title: t('common:success'),
|
||||
message: t('savedSuccessfully'),
|
||||
color: 'green',
|
||||
});
|
||||
setIsLoading(false);
|
||||
setIntegrations(form.values);
|
||||
queryClient.invalidateQueries(queryKey);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('common:save')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -226,10 +338,13 @@ export function IntegrationModal({
|
||||
<Modal
|
||||
title={<ModalTitle title={t('title')} description={t('description')} />}
|
||||
opened={opened}
|
||||
closeOnClickOutside={false}
|
||||
closeOnEscape={false}
|
||||
withCloseButton={false}
|
||||
onClose={() => closeModal()}
|
||||
size={rem(1000)}
|
||||
>
|
||||
<IntegrationsAccordion />
|
||||
<IntegrationsAccordion closeModal={closeModal} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '@tabler/icons-react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
import { AppType } from '../../../../types/app';
|
||||
@@ -204,7 +205,9 @@ export const EditAppModal = ({
|
||||
disallowAppNameProgagation={() => setAllowAppNamePropagation(false)}
|
||||
allowAppNamePropagation={allowAppNamePropagation}
|
||||
/>
|
||||
<IntegrationTab form={form} />
|
||||
<Tabs.Panel value="integration" pt="lg">
|
||||
<IntegrationTab form={form} />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
<Group position="right" mt="md">
|
||||
|
||||
@@ -3,13 +3,14 @@ import { Group, Image, Select, SelectItem, Text } from '@mantine/core';
|
||||
import { UseFormReturnType } from '@mantine/form';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import {
|
||||
IntegrationField,
|
||||
integrationFieldDefinitions,
|
||||
integrationFieldProperties,
|
||||
AppIntegrationPropertyType,
|
||||
AppIntegrationType,
|
||||
AppType,
|
||||
IntegrationField,
|
||||
integrationFieldDefinitions,
|
||||
integrationFieldProperties,
|
||||
} from '../../../../../../../../types/app';
|
||||
|
||||
interface IntegrationSelectorProps {
|
||||
@@ -97,7 +98,9 @@ export const integrationsList = [
|
||||
export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
|
||||
const { t } = useTranslation('layout/modals/add-app');
|
||||
|
||||
const data = integrationsList.filter((x) => Object.keys(integrationFieldProperties).includes(x.value));
|
||||
const data = integrationsList.filter((x) =>
|
||||
Object.keys(integrationFieldProperties).includes(x.value)
|
||||
);
|
||||
|
||||
const getNewProperties = (value: string | null): AppIntegrationPropertyType[] => {
|
||||
if (!value) return [];
|
||||
@@ -129,7 +132,6 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
|
||||
data={data}
|
||||
maxDropdownHeight={250}
|
||||
dropdownPosition="bottom"
|
||||
clearable
|
||||
variant="default"
|
||||
searchable
|
||||
zIndex={203}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Alert, Divider, Tabs, Text } from '@mantine/core';
|
||||
import { UseFormReturnType } from '@mantine/form';
|
||||
import { IconAlertTriangle } from '@tabler/icons-react';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
|
||||
import { AppType } from '../../../../../../types/app';
|
||||
import { IntegrationSelector } from './Components/InputElements/IntegrationSelector';
|
||||
import { IntegrationOptionsRenderer } from './Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer';
|
||||
@@ -15,7 +16,7 @@ export const IntegrationTab = ({ form }: IntegrationTabProps) => {
|
||||
const hasIntegrationSelected = form.values.integration?.type;
|
||||
|
||||
return (
|
||||
<Tabs.Panel value="integration" pt="lg">
|
||||
<div>
|
||||
<IntegrationSelector form={form} />
|
||||
|
||||
{hasIntegrationSelected && (
|
||||
@@ -32,6 +33,6 @@ export const IntegrationTab = ({ form }: IntegrationTabProps) => {
|
||||
</Alert>
|
||||
</>
|
||||
)}
|
||||
</Tabs.Panel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import Consola from 'consola';
|
||||
import { getCookie } from 'cookies-next';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import Consola from 'consola';
|
||||
import { z } from 'zod';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
import { BackendConfigType, ConfigType } from '~/types/config';
|
||||
import { getConfig } from '../../../tools/config/getConfig';
|
||||
import { BackendConfigType, ConfigType, IntegrationTypeMap } from '~/types/config';
|
||||
import { IRssWidget } from '~/widgets/rss/RssWidgetTile';
|
||||
|
||||
import { getConfig } from '../../../tools/config/getConfig';
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const configRouter = createTRPCRouter({
|
||||
all: publicProcedure.query(async () => {
|
||||
// Get all the configs in the /data/configs folder
|
||||
|
||||
Reference in New Issue
Block a user