Add new types for integration configuration

This commit is contained in:
Meierschlumpf
2022-12-11 19:16:31 +01:00
parent 68a97e5f27
commit ed64d138c5
10 changed files with 153 additions and 66 deletions

View File

@@ -12,15 +12,15 @@ import {
ThemeIcon,
Title,
} from '@mantine/core';
import { IconDeviceFloppy } from '@tabler/icons';
import { IconDeviceFloppy, TablerIcon } from '@tabler/icons';
import { ReactNode, useState } from 'react';
interface GenericSecretInputProps {
label: string;
value: string;
secretIsPresent: boolean;
unsetIcon: ReactNode;
setIcon: ReactNode;
unsetIcon: TablerIcon;
setIcon: TablerIcon;
}
export const GenericSecretInput = ({
@@ -33,13 +33,15 @@ export const GenericSecretInput = ({
const { classes } = useStyles();
const [dirty, setDirty] = useState(false);
const IconComponent = secretIsPresent ? setIcon : unsetIcon;
return (
<Card withBorder>
<Grid>
<Grid.Col className={classes.alignSelfCenter} xs={12} md={6}>
<Group spacing="sm">
<ThemeIcon color={secretIsPresent ? 'green' : 'red'} variant="light">
{secretIsPresent ? setIcon : unsetIcon}
<IconComponent size={16} />
</ThemeIcon>
<Stack spacing={0}>
<Title className={classes.subtitle} order={6}>

View File

@@ -3,7 +3,15 @@ import { Group, Select, SelectItem, Text } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form';
import { useTranslation } from 'next-i18next';
import { forwardRef } from 'react';
import { ServiceType } from '../../../../../../../../types/service';
import { IntegrationsType } from '../../../../../../../../types/integration';
import {
IntegrationField,
integrationFieldDefinitions,
integrationFieldProperties,
ServiceIntegrationPropertyType,
ServiceIntegrationType,
ServiceType,
} from '../../../../../../../../types/service';
interface IntegrationSelectorProps {
form: UseFormReturnType<ServiceType, (item: ServiceType) => ServiceType>;
@@ -44,7 +52,23 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
image: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/overseerr.png',
label: 'Overseerr',
},
];
].filter((x) => Object.keys(integrationFieldProperties).includes(x.value));
const inputProps = form.getInputProps('integration.type');
const getNewProperties = (value: string | null): ServiceIntegrationPropertyType[] => {
if (!value) return [];
const requiredProperties = Object.entries(integrationFieldDefinitions).filter(([k, v]) => {
const val = integrationFieldProperties[value as ServiceIntegrationType['type']];
return val.includes(k as IntegrationField);
})!;
return requiredProperties.map(([k, value]) => ({
type: value.type,
field: k as IntegrationField,
value: undefined,
isDefined: false,
}));
};
return (
<>
@@ -58,8 +82,22 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
clearable
variant="default"
mb="md"
icon={form.values.integration?.type && <img src={data.find(x => x.value === form.values.integration?.type)?.image} alt="test" width={20} height={20} />}
{...form.getInputProps('integration.type')}
icon={
form.values.integration?.type && (
<img
src={data.find((x) => x.value === form.values.integration?.type)?.image}
alt="test"
width={20}
height={20}
/>
)
}
{...inputProps}
onChange={(value) => {
form.setFieldValue('integration.properties', getNewProperties(value));
console.log(`changed to value ${value}`);
inputProps.onChange(value);
}}
/>
</>
);

View File

@@ -3,9 +3,9 @@ import { UseFormReturnType } from '@mantine/form';
import { IconKey, IconKeyOff, IconLock, IconLockOff, IconUser, IconUserOff } from '@tabler/icons';
import {
IntegrationField,
IntegrationFieldDefinitionType,
integrationFieldDefinitions,
integrationFieldProperties,
ServiceIntegrationPropertyType,
ServiceType,
} from '../../../../../../../../types/service';
import { GenericSecretInput } from '../InputElements/GenericSecretInput';
@@ -14,27 +14,6 @@ interface IntegrationOptionsRendererProps {
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
}
const secretMappings = [
{
label: 'username',
prettyName: 'Username',
icon: <IconUser size={18} />,
iconUnset: <IconUserOff size={18} />,
},
{
label: 'password',
prettyName: 'Password',
icon: <IconLock size={18} />,
iconUnset: <IconLockOff size={18} />,
},
{
label: 'apiKey',
prettyName: 'API Key',
icon: <IconKey size={18} />,
iconUnset: <IconKeyOff size={18} />,
},
];
export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererProps) => {
const selectedIntegration = form.values.integration?.type;
@@ -44,31 +23,49 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP
return (
<Stack spacing="xs" mb="md">
{displayedProperties.map((property) => {
const mapping = Object.entries(integrationFieldDefinitions).find(
([key, value]) => key as IntegrationField === property
);
const isPresent = entry[1] !== undefined;
{displayedProperties.map((property, index) => {
const [_, definition] = Object.entries(integrationFieldDefinitions).find(
([key]) => property === key
)!;
if (!mapping) {
let indexInFormValue =
form.values.integration?.properties.findIndex((p) => p.field === property) ?? -1;
if (indexInFormValue === -1) {
const type = Object.entries(integrationFieldDefinitions).find(
([k, v]) => k === property
)![1].type;
const newProperty: ServiceIntegrationPropertyType = {
type,
field: property as IntegrationField,
isDefined: false,
};
form.insertListItem('integration.properties', newProperty);
indexInFormValue = form.values.integration!.properties.length;
}
const formValue = form.values.integration?.properties[indexInFormValue];
const isPresent = formValue?.isDefined;
if (!definition) {
return (
<GenericSecretInput
label={`${entry[0]} (potentionally unmapped)`}
value={entry[1]}
label={`${property} (potentionally unmapped)`}
secretIsPresent={isPresent}
setIcon={<IconKey size={18} />}
unsetIcon={<IconKeyOff size={18} />}
setIcon={IconKey}
unsetIcon={IconKeyOff}
{...form.getInputProps(`integration.properties.${index}.value`)}
/>
);
}
return (
<GenericSecretInput
label={mapping.prettyName}
value={entry[1]}
label={definition.label}
value=""
secretIsPresent={isPresent}
setIcon={mapping.icon}
unsetIcon={mapping.iconUnset}
setIcon={definition.icon}
unsetIcon={definition.iconUnset}
{...form.getInputProps(`integration.properties.${index}.value`)}
/>
);
})}

View File

@@ -64,7 +64,7 @@ export const Tiles: TileDefinitionProps = {
maxHeight: 12,
},
useNet: {
component: UseNetTile, //CalendarTile,
component: UseNetTile,
minWidth: 4,
maxWidth: 12,
minHeight: 5,

View File

@@ -1,21 +1,29 @@
import { getCookie, setCookie } from 'cookies-next';
import { GetServerSidePropsContext } from 'next';
import { SSRConfig } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import LoadConfigComponent from '../components/Config/LoadConfig';
import { Dashboard } from '../components/Dashboard/Dashboard';
import Layout from '../components/layout/Layout';
import { useInitConfig } from '../config/init';
import { getFrontendConfig } from '../tools/config/getFrontendConfig';
import { getConfig } from '../tools/getConfig';
import { dashboardNamespaces } from '../tools/translation-namespaces';
import { Config } from '../tools/types';
import { ConfigType } from '../types/config';
type ServerSideProps = {
config: ConfigType;
configName: string;
_nextI18Next: SSRConfig['_nextI18Next'];
};
export async function getServerSideProps({
req,
res,
locale,
}: GetServerSidePropsContext): Promise<{ props: { config: Config } }> {
}: GetServerSidePropsContext): Promise<{ props: ServerSideProps }> {
let configName = getCookie('config-name', { req, res });
const configLocale = getCookie('config-locale', { req, res });
if (!configName) {
@@ -32,19 +40,14 @@ export async function getServerSideProps({
(configLocale ?? locale) as string,
dashboardNamespaces
);
return getConfig(configName as string, translations);
const config = getFrontendConfig(configName as string);
return {
props: { configName: configName as string, config, ...translations },
};
}
export default function HomePage(props: any) {
const { config: initialConfig }: { config: ConfigType } = props;
/*const { setConfig } = useConfig();
const { setPrimaryColor, setSecondaryColor } = useColorTheme();
useEffect(() => {
const migratedConfig = migrateToIdConfig(initialConfig);
setPrimaryColor(migratedConfig.settings.primaryColor || 'red');
setSecondaryColor(migratedConfig.settings.secondaryColor || 'orange');
setConfig(migratedConfig);
}, [initialConfig]);*/
export default function HomePage({ config: initialConfig }: ServerSideProps) {
useInitConfig(initialConfig);
return (

View File

@@ -1,9 +1,9 @@
import { ConfigType } from '../../types/config';
import { BackendConfigType, ConfigType } from '../../types/config';
import { configExists } from './configExists';
import { getFallbackConfig } from './getFallbackConfig';
import { readConfig } from './readConfig';
export const getConfig = (name: string): ConfigType => {
export const getConfig = (name: string): BackendConfigType => {
if (!configExists(name)) return getFallbackConfig();
return readConfig(name);
};

View File

@@ -1,6 +1,6 @@
import { ConfigType } from '../../types/config';
import { BackendConfigType } from '../../types/config';
export const getFallbackConfig = (name?: string): ConfigType => ({
export const getFallbackConfig = (name?: string): BackendConfigType => ({
schemaVersion: '1.0.0',
configProperties: {
name: name ?? 'default',
@@ -12,7 +12,12 @@ export const getFallbackConfig = (name?: string): ConfigType => ({
common: {
searchEngine: {
type: 'google',
properties: {
enabled: true,
openInNewTab: true,
},
},
defaultConfig: 'default',
},
customization: {
colors: {},

View File

@@ -0,0 +1,23 @@
import { ConfigType } from '../../types/config';
import { getConfig } from './getConfig';
export const getFrontendConfig = (name: string): ConfigType => {
const config = getConfig(name);
return {
...config,
services: config.services.map((s) => ({
...s,
integration: s.integration
? {
...s.integration,
properties: s.integration?.properties.map((p) => ({
...p,
value: p.type === 'private' ? null : p.value,
isDefined: p.value != null,
})),
}
: null,
})),
};
};

View File

@@ -1,6 +1,6 @@
import { CategoryType } from './category';
import { WrapperType } from './wrapper';
import { ServiceType } from './service';
import { ConfigServiceType, ServiceType } from './service';
import { IntegrationsType } from './integration';
import { SettingsType } from './settings';
@@ -14,6 +14,10 @@ export interface ConfigType {
settings: SettingsType;
}
export type BackendConfigType = Omit<ConfigType, 'services'> & {
services: ConfigServiceType[];
};
export interface ConfigPropertiesType {
name: string;
}

View File

@@ -16,9 +16,13 @@ export interface ServiceType extends TileBaseType {
behaviour: ServiceBehaviourType;
network: ServiceNetworkType;
appearance: ServiceAppearanceType;
integration?: ServiceIntegrationType;
integration?: ServiceIntegrationType | null;
}
export type ConfigServiceType = Omit<ServiceType, 'integration'> & {
integration?: ConfigServiceIntegrationType | null;
};
interface ServiceBehaviourType {
onClickUrl: string;
isOpeningNewTab: boolean;
@@ -33,7 +37,7 @@ interface ServiceAppearanceType {
iconUrl: string;
}
type IntegrationType =
export type IntegrationType =
| 'readarr'
| 'radarr'
| 'sonarr'
@@ -51,12 +55,19 @@ export type ServiceIntegrationType = {
properties: ServiceIntegrationPropertyType[];
};
type ServiceIntegrationPropertyType = {
export type ConfigServiceIntegrationType = Omit<ServiceIntegrationType, 'properties'> & {
properties: ConfigServiceIntegrationPropertyType[];
};
export type ServiceIntegrationPropertyType = {
type: 'private' | 'public';
field: IntegrationField;
value?: string;
value?: string | null;
isDefined: boolean;
};
type ConfigServiceIntegrationPropertyType = Omit<ServiceIntegrationPropertyType, 'isDefined'>;
export type IntegrationField = 'apiKey' | 'password' | 'username';
export const integrationFieldProperties: {
@@ -76,6 +87,7 @@ export const integrationFieldProperties: {
};
export type IntegrationFieldDefinitionType = {
type: 'private' | 'public';
icon: TablerIcon;
iconUnset: TablerIcon;
label: string;
@@ -85,16 +97,19 @@ export const integrationFieldDefinitions: {
[key in IntegrationField]: IntegrationFieldDefinitionType;
} = {
apiKey: {
type: 'private',
icon: IconKey,
iconUnset: IconKeyOff,
label: 'API Key',
},
username: {
type: 'public',
icon: IconUser,
iconUnset: IconUserOff,
label: 'Username',
},
password: {
type: 'private',
icon: IconPassword,
iconUnset: IconLockOff,
label: 'Password',