mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 15:35:55 +01:00
✨ Add visual hint for defined and undefined credentials
This commit is contained in:
@@ -46,11 +46,17 @@
|
|||||||
"type": {
|
"type": {
|
||||||
"label": "Integration configuration",
|
"label": "Integration configuration",
|
||||||
"description": "Treats this app as the selected integration and provides you with per-app configuration",
|
"description": "Treats this app as the selected integration and provides you with per-app configuration",
|
||||||
"placeholder": "Select an integration"
|
"placeholder": "Select an integration",
|
||||||
|
"defined": "Defined",
|
||||||
|
"undefined": "Undefined",
|
||||||
|
"public": "Public",
|
||||||
|
"private": "Private",
|
||||||
|
"explanationPublic": "A private secret will be sent to the server. Once your browser has refreshed the page, it will never be sent to the client.",
|
||||||
|
"explanationPrivate": "A public secret will always be sent to the client and is accessible over the API. It should not contain any confidential values such as usernames, passwords, tokens, certificates and similar"
|
||||||
},
|
},
|
||||||
"secrets": {
|
"secrets": {
|
||||||
"description": "To update a secret, enter a value and click the save button. To remove a secret, use the clear button.",
|
"description": "To update a secret, enter a value and click the save button. To remove a secret, use the clear button.",
|
||||||
"warning": "Please note that Homarr removes secrets from the configuration for security reasons. Thus, you can only either define or unset any credentials. Your credentials act as the main access for your integrations and you should <strong>never</strong> share them with anybody else. Make sure to <strong>store and manage your secrets safely</strong>.",
|
"warning": "Your credentials act as the access for your integrations and you should <strong>never</strong> share them with anybody else. The official Homarr team will never ask for credentials. Make sure to <strong>store and manage your secrets safely</strong>.",
|
||||||
"clear": "Clear secret",
|
"clear": "Clear secret",
|
||||||
"save": "Save secret",
|
"save": "Save secret",
|
||||||
"update": "Update secret"
|
"update": "Update secret"
|
||||||
|
|||||||
@@ -9,15 +9,21 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
|
Text,
|
||||||
|
Badge,
|
||||||
|
Tooltip,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { TablerIcon } from '@tabler/icons';
|
import { IconLock, TablerIcon } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { AppIntegrationPropertyAccessabilityType } from '../../../../../../../../types/app';
|
||||||
|
|
||||||
interface GenericSecretInputProps {
|
interface GenericSecretInputProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
setIcon: TablerIcon;
|
setIcon: TablerIcon;
|
||||||
|
secretIsPresent: boolean;
|
||||||
|
type: AppIntegrationPropertyAccessabilityType;
|
||||||
onClickUpdateButton: (value: string | undefined) => void;
|
onClickUpdateButton: (value: string | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +31,8 @@ export const GenericSecretInput = ({
|
|||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
setIcon,
|
setIcon,
|
||||||
|
secretIsPresent,
|
||||||
|
type,
|
||||||
onClickUpdateButton,
|
onClickUpdateButton,
|
||||||
...props
|
...props
|
||||||
}: GenericSecretInputProps) => {
|
}: GenericSecretInputProps) => {
|
||||||
@@ -36,17 +44,61 @@ export const GenericSecretInput = ({
|
|||||||
const { t } = useTranslation(['layout/modals/add-app', 'common']);
|
const { t } = useTranslation(['layout/modals/add-app', 'common']);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card withBorder>
|
<Card p="xs" 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="green" variant="light">
|
<ThemeIcon color={secretIsPresent ? 'green' : 'red'} variant="light" size="lg">
|
||||||
<Icon size={18} />
|
<Icon size={18} />
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<Stack spacing={0}>
|
<Stack spacing={0}>
|
||||||
|
<Group spacing="xs">
|
||||||
<Title className={classes.subtitle} order={6}>
|
<Title className={classes.subtitle} order={6}>
|
||||||
{t(label)}
|
{t(label)}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
|
<Group spacing="xs">
|
||||||
|
{secretIsPresent ? (
|
||||||
|
<Badge className={classes.textTransformUnset} color="green" variant="dot">
|
||||||
|
{t('integration.type.defined')}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge className={classes.textTransformUnset} color="red" variant="dot">
|
||||||
|
{t('integration.type.undefined')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{type === 'private' ? (
|
||||||
|
<Tooltip
|
||||||
|
label={t('integration.type.explanationPrivate')}
|
||||||
|
width={200}
|
||||||
|
multiline
|
||||||
|
withinPortal
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<Badge className={classes.textTransformUnset} color="orange" variant="dot">
|
||||||
|
{t('integration.type.private')}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Tooltip
|
||||||
|
label={t('integration.type.explanationPublic')}
|
||||||
|
width={200}
|
||||||
|
multiline
|
||||||
|
withinPortal
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<Badge className={classes.textTransformUnset} color="red" variant="dot">
|
||||||
|
{t('integration.type.public')}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
<Text size="xs" color="dimmed">
|
||||||
|
{type === 'private'
|
||||||
|
? 'Private: Once saved, you cannot read out this value again'
|
||||||
|
: 'Public: Can be read out repeatedly'}
|
||||||
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
@@ -80,4 +132,7 @@ const useStyles = createStyles(() => ({
|
|||||||
alignSelfCenter: {
|
alignSelfCenter: {
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
|
textTransformUnset: {
|
||||||
|
textTransform: 'inherit',
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP
|
|||||||
const formValue = form.values.integration?.properties[indexInFormValue];
|
const formValue = form.values.integration?.properties[indexInFormValue];
|
||||||
|
|
||||||
const isPresent = formValue?.isDefined;
|
const isPresent = formValue?.isDefined;
|
||||||
|
const accessabilityType = formValue?.type;
|
||||||
|
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
return (
|
return (
|
||||||
@@ -57,6 +58,7 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP
|
|||||||
secretIsPresent={isPresent}
|
secretIsPresent={isPresent}
|
||||||
setIcon={IconKey}
|
setIcon={IconKey}
|
||||||
value={formValue.value}
|
value={formValue.value}
|
||||||
|
type={accessabilityType}
|
||||||
{...form.getInputProps(`integration.properties.${index}.value`)}
|
{...form.getInputProps(`integration.properties.${index}.value`)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -72,6 +74,7 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP
|
|||||||
value=""
|
value=""
|
||||||
secretIsPresent={isPresent}
|
secretIsPresent={isPresent}
|
||||||
setIcon={definition.icon}
|
setIcon={definition.icon}
|
||||||
|
type={accessabilityType}
|
||||||
{...form.getInputProps(`integration.properties.${index}.value`)}
|
{...form.getInputProps(`integration.properties.${index}.value`)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -52,12 +52,14 @@ export type ConfigAppIntegrationType = Omit<AppIntegrationType, 'properties'> &
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type AppIntegrationPropertyType = {
|
export type AppIntegrationPropertyType = {
|
||||||
type: 'private' | 'public';
|
type: AppIntegrationPropertyAccessabilityType;
|
||||||
field: IntegrationField;
|
field: IntegrationField;
|
||||||
value?: string | null;
|
value?: string | null;
|
||||||
isDefined: boolean;
|
isDefined: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AppIntegrationPropertyAccessabilityType = 'private' | 'public';
|
||||||
|
|
||||||
type ConfigAppIntegrationPropertyType = Omit<AppIntegrationPropertyType, 'isDefined'>;
|
type ConfigAppIntegrationPropertyType = Omit<AppIntegrationPropertyType, 'isDefined'>;
|
||||||
|
|
||||||
export type IntegrationField = 'apiKey' | 'password' | 'username';
|
export type IntegrationField = 'apiKey' | 'password' | 'username';
|
||||||
|
|||||||
Reference in New Issue
Block a user