mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-11 07:55:52 +01:00
✨ Add new types for integration configuration
This commit is contained in:
@@ -12,15 +12,15 @@ import {
|
|||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconDeviceFloppy } from '@tabler/icons';
|
import { IconDeviceFloppy, TablerIcon } from '@tabler/icons';
|
||||||
import { ReactNode, useState } from 'react';
|
import { ReactNode, useState } from 'react';
|
||||||
|
|
||||||
interface GenericSecretInputProps {
|
interface GenericSecretInputProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
secretIsPresent: boolean;
|
secretIsPresent: boolean;
|
||||||
unsetIcon: ReactNode;
|
unsetIcon: TablerIcon;
|
||||||
setIcon: ReactNode;
|
setIcon: TablerIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GenericSecretInput = ({
|
export const GenericSecretInput = ({
|
||||||
@@ -33,13 +33,15 @@ export const GenericSecretInput = ({
|
|||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const [dirty, setDirty] = useState(false);
|
const [dirty, setDirty] = useState(false);
|
||||||
|
|
||||||
|
const IconComponent = secretIsPresent ? setIcon : unsetIcon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card withBorder>
|
<Card withBorder>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.Col className={classes.alignSelfCenter} xs={12} md={6}>
|
<Grid.Col className={classes.alignSelfCenter} xs={12} md={6}>
|
||||||
<Group spacing="sm">
|
<Group spacing="sm">
|
||||||
<ThemeIcon color={secretIsPresent ? 'green' : 'red'} variant="light">
|
<ThemeIcon color={secretIsPresent ? 'green' : 'red'} variant="light">
|
||||||
{secretIsPresent ? setIcon : unsetIcon}
|
<IconComponent size={16} />
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<Stack spacing={0}>
|
<Stack spacing={0}>
|
||||||
<Title className={classes.subtitle} order={6}>
|
<Title className={classes.subtitle} order={6}>
|
||||||
|
|||||||
@@ -3,7 +3,15 @@ import { Group, Select, SelectItem, Text } from '@mantine/core';
|
|||||||
import { UseFormReturnType } from '@mantine/form';
|
import { UseFormReturnType } from '@mantine/form';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { forwardRef } from 'react';
|
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 {
|
interface IntegrationSelectorProps {
|
||||||
form: UseFormReturnType<ServiceType, (item: ServiceType) => ServiceType>;
|
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',
|
image: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/overseerr.png',
|
||||||
label: 'Overseerr',
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -58,8 +82,22 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
|
|||||||
clearable
|
clearable
|
||||||
variant="default"
|
variant="default"
|
||||||
mb="md"
|
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} />}
|
icon={
|
||||||
{...form.getInputProps('integration.type')}
|
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);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { UseFormReturnType } from '@mantine/form';
|
|||||||
import { IconKey, IconKeyOff, IconLock, IconLockOff, IconUser, IconUserOff } from '@tabler/icons';
|
import { IconKey, IconKeyOff, IconLock, IconLockOff, IconUser, IconUserOff } from '@tabler/icons';
|
||||||
import {
|
import {
|
||||||
IntegrationField,
|
IntegrationField,
|
||||||
IntegrationFieldDefinitionType,
|
|
||||||
integrationFieldDefinitions,
|
integrationFieldDefinitions,
|
||||||
integrationFieldProperties,
|
integrationFieldProperties,
|
||||||
|
ServiceIntegrationPropertyType,
|
||||||
ServiceType,
|
ServiceType,
|
||||||
} from '../../../../../../../../types/service';
|
} from '../../../../../../../../types/service';
|
||||||
import { GenericSecretInput } from '../InputElements/GenericSecretInput';
|
import { GenericSecretInput } from '../InputElements/GenericSecretInput';
|
||||||
@@ -14,27 +14,6 @@ interface IntegrationOptionsRendererProps {
|
|||||||
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
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) => {
|
export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererProps) => {
|
||||||
const selectedIntegration = form.values.integration?.type;
|
const selectedIntegration = form.values.integration?.type;
|
||||||
|
|
||||||
@@ -44,31 +23,49 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing="xs" mb="md">
|
<Stack spacing="xs" mb="md">
|
||||||
{displayedProperties.map((property) => {
|
{displayedProperties.map((property, index) => {
|
||||||
const mapping = Object.entries(integrationFieldDefinitions).find(
|
const [_, definition] = Object.entries(integrationFieldDefinitions).find(
|
||||||
([key, value]) => key as IntegrationField === property
|
([key]) => property === key
|
||||||
);
|
)!;
|
||||||
const isPresent = entry[1] !== undefined;
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<GenericSecretInput
|
<GenericSecretInput
|
||||||
label={`${entry[0]} (potentionally unmapped)`}
|
label={`${property} (potentionally unmapped)`}
|
||||||
value={entry[1]}
|
|
||||||
secretIsPresent={isPresent}
|
secretIsPresent={isPresent}
|
||||||
setIcon={<IconKey size={18} />}
|
setIcon={IconKey}
|
||||||
unsetIcon={<IconKeyOff size={18} />}
|
unsetIcon={IconKeyOff}
|
||||||
|
{...form.getInputProps(`integration.properties.${index}.value`)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GenericSecretInput
|
<GenericSecretInput
|
||||||
label={mapping.prettyName}
|
label={definition.label}
|
||||||
value={entry[1]}
|
value=""
|
||||||
secretIsPresent={isPresent}
|
secretIsPresent={isPresent}
|
||||||
setIcon={mapping.icon}
|
setIcon={definition.icon}
|
||||||
unsetIcon={mapping.iconUnset}
|
unsetIcon={definition.iconUnset}
|
||||||
|
{...form.getInputProps(`integration.properties.${index}.value`)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const Tiles: TileDefinitionProps = {
|
|||||||
maxHeight: 12,
|
maxHeight: 12,
|
||||||
},
|
},
|
||||||
useNet: {
|
useNet: {
|
||||||
component: UseNetTile, //CalendarTile,
|
component: UseNetTile,
|
||||||
minWidth: 4,
|
minWidth: 4,
|
||||||
maxWidth: 12,
|
maxWidth: 12,
|
||||||
minHeight: 5,
|
minHeight: 5,
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
import { getCookie, setCookie } from 'cookies-next';
|
import { getCookie, setCookie } from 'cookies-next';
|
||||||
import { GetServerSidePropsContext } from 'next';
|
import { GetServerSidePropsContext } from 'next';
|
||||||
|
import { SSRConfig } from 'next-i18next';
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||||
|
|
||||||
import LoadConfigComponent from '../components/Config/LoadConfig';
|
import LoadConfigComponent from '../components/Config/LoadConfig';
|
||||||
import { Dashboard } from '../components/Dashboard/Dashboard';
|
import { Dashboard } from '../components/Dashboard/Dashboard';
|
||||||
import Layout from '../components/layout/Layout';
|
import Layout from '../components/layout/Layout';
|
||||||
import { useInitConfig } from '../config/init';
|
import { useInitConfig } from '../config/init';
|
||||||
|
import { getFrontendConfig } from '../tools/config/getFrontendConfig';
|
||||||
import { getConfig } from '../tools/getConfig';
|
import { getConfig } from '../tools/getConfig';
|
||||||
import { dashboardNamespaces } from '../tools/translation-namespaces';
|
import { dashboardNamespaces } from '../tools/translation-namespaces';
|
||||||
import { Config } from '../tools/types';
|
import { Config } from '../tools/types';
|
||||||
import { ConfigType } from '../types/config';
|
import { ConfigType } from '../types/config';
|
||||||
|
|
||||||
|
type ServerSideProps = {
|
||||||
|
config: ConfigType;
|
||||||
|
configName: string;
|
||||||
|
_nextI18Next: SSRConfig['_nextI18Next'];
|
||||||
|
};
|
||||||
|
|
||||||
export async function getServerSideProps({
|
export async function getServerSideProps({
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
locale,
|
locale,
|
||||||
}: GetServerSidePropsContext): Promise<{ props: { config: Config } }> {
|
}: GetServerSidePropsContext): Promise<{ props: ServerSideProps }> {
|
||||||
let configName = getCookie('config-name', { req, res });
|
let configName = getCookie('config-name', { req, res });
|
||||||
const configLocale = getCookie('config-locale', { req, res });
|
const configLocale = getCookie('config-locale', { req, res });
|
||||||
if (!configName) {
|
if (!configName) {
|
||||||
@@ -32,19 +40,14 @@ export async function getServerSideProps({
|
|||||||
(configLocale ?? locale) as string,
|
(configLocale ?? locale) as string,
|
||||||
dashboardNamespaces
|
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) {
|
export default function HomePage({ config: initialConfig }: ServerSideProps) {
|
||||||
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]);*/
|
|
||||||
useInitConfig(initialConfig);
|
useInitConfig(initialConfig);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ConfigType } from '../../types/config';
|
import { BackendConfigType, ConfigType } from '../../types/config';
|
||||||
import { configExists } from './configExists';
|
import { configExists } from './configExists';
|
||||||
import { getFallbackConfig } from './getFallbackConfig';
|
import { getFallbackConfig } from './getFallbackConfig';
|
||||||
import { readConfig } from './readConfig';
|
import { readConfig } from './readConfig';
|
||||||
|
|
||||||
export const getConfig = (name: string): ConfigType => {
|
export const getConfig = (name: string): BackendConfigType => {
|
||||||
if (!configExists(name)) return getFallbackConfig();
|
if (!configExists(name)) return getFallbackConfig();
|
||||||
return readConfig(name);
|
return readConfig(name);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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',
|
schemaVersion: '1.0.0',
|
||||||
configProperties: {
|
configProperties: {
|
||||||
name: name ?? 'default',
|
name: name ?? 'default',
|
||||||
@@ -12,7 +12,12 @@ export const getFallbackConfig = (name?: string): ConfigType => ({
|
|||||||
common: {
|
common: {
|
||||||
searchEngine: {
|
searchEngine: {
|
||||||
type: 'google',
|
type: 'google',
|
||||||
|
properties: {
|
||||||
|
enabled: true,
|
||||||
|
openInNewTab: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
defaultConfig: 'default',
|
||||||
},
|
},
|
||||||
customization: {
|
customization: {
|
||||||
colors: {},
|
colors: {},
|
||||||
|
|||||||
23
src/tools/config/getFrontendConfig.ts
Normal file
23
src/tools/config/getFrontendConfig.ts
Normal 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,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { CategoryType } from './category';
|
import { CategoryType } from './category';
|
||||||
import { WrapperType } from './wrapper';
|
import { WrapperType } from './wrapper';
|
||||||
import { ServiceType } from './service';
|
import { ConfigServiceType, ServiceType } from './service';
|
||||||
import { IntegrationsType } from './integration';
|
import { IntegrationsType } from './integration';
|
||||||
import { SettingsType } from './settings';
|
import { SettingsType } from './settings';
|
||||||
|
|
||||||
@@ -14,6 +14,10 @@ export interface ConfigType {
|
|||||||
settings: SettingsType;
|
settings: SettingsType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BackendConfigType = Omit<ConfigType, 'services'> & {
|
||||||
|
services: ConfigServiceType[];
|
||||||
|
};
|
||||||
|
|
||||||
export interface ConfigPropertiesType {
|
export interface ConfigPropertiesType {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,13 @@ export interface ServiceType extends TileBaseType {
|
|||||||
behaviour: ServiceBehaviourType;
|
behaviour: ServiceBehaviourType;
|
||||||
network: ServiceNetworkType;
|
network: ServiceNetworkType;
|
||||||
appearance: ServiceAppearanceType;
|
appearance: ServiceAppearanceType;
|
||||||
integration?: ServiceIntegrationType;
|
integration?: ServiceIntegrationType | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ConfigServiceType = Omit<ServiceType, 'integration'> & {
|
||||||
|
integration?: ConfigServiceIntegrationType | null;
|
||||||
|
};
|
||||||
|
|
||||||
interface ServiceBehaviourType {
|
interface ServiceBehaviourType {
|
||||||
onClickUrl: string;
|
onClickUrl: string;
|
||||||
isOpeningNewTab: boolean;
|
isOpeningNewTab: boolean;
|
||||||
@@ -33,7 +37,7 @@ interface ServiceAppearanceType {
|
|||||||
iconUrl: string;
|
iconUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type IntegrationType =
|
export type IntegrationType =
|
||||||
| 'readarr'
|
| 'readarr'
|
||||||
| 'radarr'
|
| 'radarr'
|
||||||
| 'sonarr'
|
| 'sonarr'
|
||||||
@@ -51,12 +55,19 @@ export type ServiceIntegrationType = {
|
|||||||
properties: ServiceIntegrationPropertyType[];
|
properties: ServiceIntegrationPropertyType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type ServiceIntegrationPropertyType = {
|
export type ConfigServiceIntegrationType = Omit<ServiceIntegrationType, 'properties'> & {
|
||||||
|
properties: ConfigServiceIntegrationPropertyType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServiceIntegrationPropertyType = {
|
||||||
type: 'private' | 'public';
|
type: 'private' | 'public';
|
||||||
field: IntegrationField;
|
field: IntegrationField;
|
||||||
value?: string;
|
value?: string | null;
|
||||||
|
isDefined: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ConfigServiceIntegrationPropertyType = Omit<ServiceIntegrationPropertyType, 'isDefined'>;
|
||||||
|
|
||||||
export type IntegrationField = 'apiKey' | 'password' | 'username';
|
export type IntegrationField = 'apiKey' | 'password' | 'username';
|
||||||
|
|
||||||
export const integrationFieldProperties: {
|
export const integrationFieldProperties: {
|
||||||
@@ -76,6 +87,7 @@ export const integrationFieldProperties: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type IntegrationFieldDefinitionType = {
|
export type IntegrationFieldDefinitionType = {
|
||||||
|
type: 'private' | 'public';
|
||||||
icon: TablerIcon;
|
icon: TablerIcon;
|
||||||
iconUnset: TablerIcon;
|
iconUnset: TablerIcon;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -85,16 +97,19 @@ export const integrationFieldDefinitions: {
|
|||||||
[key in IntegrationField]: IntegrationFieldDefinitionType;
|
[key in IntegrationField]: IntegrationFieldDefinitionType;
|
||||||
} = {
|
} = {
|
||||||
apiKey: {
|
apiKey: {
|
||||||
|
type: 'private',
|
||||||
icon: IconKey,
|
icon: IconKey,
|
||||||
iconUnset: IconKeyOff,
|
iconUnset: IconKeyOff,
|
||||||
label: 'API Key',
|
label: 'API Key',
|
||||||
},
|
},
|
||||||
username: {
|
username: {
|
||||||
|
type: 'public',
|
||||||
icon: IconUser,
|
icon: IconUser,
|
||||||
iconUnset: IconUserOff,
|
iconUnset: IconUserOff,
|
||||||
label: 'Username',
|
label: 'Username',
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
|
type: 'private',
|
||||||
icon: IconPassword,
|
icon: IconPassword,
|
||||||
iconUnset: IconLockOff,
|
iconUnset: IconLockOff,
|
||||||
label: 'Password',
|
label: 'Password',
|
||||||
|
|||||||
Reference in New Issue
Block a user