diff --git a/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/GenericSecretInput.tsx b/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/GenericSecretInput.tsx index 81196bc18..b12890e14 100644 --- a/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/GenericSecretInput.tsx +++ b/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/GenericSecretInput.tsx @@ -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 ( - {secretIsPresent ? setIcon : unsetIcon} + diff --git a/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx b/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx index 5b60f778b..08b8a4ff1 100644 --- a/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx +++ b/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx @@ -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); + }} /> </> ); diff --git a/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer.tsx b/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer.tsx index de7a4d672..bbeb3a233 100644 --- a/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer.tsx +++ b/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer.tsx @@ -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`)} /> ); })} diff --git a/src/components/Dashboard/Tiles/tilesDefinitions.tsx b/src/components/Dashboard/Tiles/tilesDefinitions.tsx index 80e9fc782..3bd3aebfe 100644 --- a/src/components/Dashboard/Tiles/tilesDefinitions.tsx +++ b/src/components/Dashboard/Tiles/tilesDefinitions.tsx @@ -64,7 +64,7 @@ export const Tiles: TileDefinitionProps = { maxHeight: 12, }, useNet: { - component: UseNetTile, //CalendarTile, + component: UseNetTile, minWidth: 4, maxWidth: 12, minHeight: 5, diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 0c39022e0..c10c06585 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -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 ( diff --git a/src/tools/config/getConfig.ts b/src/tools/config/getConfig.ts index 3e0424f05..bdd584f42 100644 --- a/src/tools/config/getConfig.ts +++ b/src/tools/config/getConfig.ts @@ -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); }; diff --git a/src/tools/config/getFallbackConfig.ts b/src/tools/config/getFallbackConfig.ts index 835866fa4..857091660 100644 --- a/src/tools/config/getFallbackConfig.ts +++ b/src/tools/config/getFallbackConfig.ts @@ -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: {}, diff --git a/src/tools/config/getFrontendConfig.ts b/src/tools/config/getFrontendConfig.ts new file mode 100644 index 000000000..3945fb40d --- /dev/null +++ b/src/tools/config/getFrontendConfig.ts @@ -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, + })), + }; +}; diff --git a/src/types/config.ts b/src/types/config.ts index 7de63c21b..45addf0ec 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -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; } diff --git a/src/types/service.ts b/src/types/service.ts index a9c31637d..13156ec00 100644 --- a/src/types/service.ts +++ b/src/types/service.ts @@ -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',