Add visual hint for defined and undefined credentials

This commit is contained in:
Manuel Ruwe
2023-01-03 21:53:27 +01:00
parent 8a91edbd59
commit 6fd4608b22
4 changed files with 75 additions and 9 deletions

View File

@@ -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"

View File

@@ -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',
},
})); }));

View File

@@ -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`)}
/> />
); );

View File

@@ -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';