@cloud-ru/uikit-product-claudia 1.11.1 → 1.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/cjs/components/SshField/SshField.d.ts +1 -1
- package/dist/cjs/components/SshField/SshField.js +33 -46
- package/dist/cjs/components/SshField/components/MobileFieldAi/MobileFieldAi.d.ts +3 -0
- package/dist/cjs/components/SshField/components/MobileFieldAi/MobileFieldAi.js +2 -2
- package/dist/cjs/components/SshField/helperComponents/CheckItem/CheckItem.d.ts +6 -0
- package/dist/cjs/components/SshField/helperComponents/CheckItem/CheckItem.js +13 -0
- package/dist/cjs/components/SshField/helperComponents/CheckItem/index.d.ts +1 -0
- package/dist/cjs/components/SshField/helperComponents/CheckItem/index.js +17 -0
- package/dist/cjs/components/SshField/helperComponents/CheckItem/styles.module.css +25 -0
- package/dist/cjs/components/SshField/helperComponents/DropZoneContent/styles.module.css +1 -1
- package/dist/cjs/components/SshField/helperComponents/SshValidation/SshValidation.d.ts +6 -0
- package/dist/cjs/components/SshField/helperComponents/SshValidation/SshValidation.js +16 -0
- package/dist/cjs/components/SshField/helperComponents/SshValidation/index.d.ts +1 -0
- package/dist/cjs/components/SshField/helperComponents/SshValidation/index.js +17 -0
- package/dist/cjs/components/SshField/helperComponents/SshValidation/styles.module.css +35 -0
- package/dist/cjs/components/SshField/helperComponents/WithSshValidation/WithSshValidation.d.ts +8 -0
- package/dist/cjs/components/SshField/helperComponents/WithSshValidation/WithSshValidation.js +19 -0
- package/dist/cjs/components/SshField/helperComponents/WithSshValidation/index.d.ts +1 -0
- package/dist/cjs/components/SshField/helperComponents/WithSshValidation/index.js +17 -0
- package/dist/cjs/components/SshField/helperComponents/WithSshValidation/styles.module.css +5 -0
- package/dist/cjs/components/SshField/types.d.ts +12 -0
- package/dist/cjs/components/SshField/types.js +2 -0
- package/dist/cjs/components/SshField/utils/readFileContent.d.ts +6 -1
- package/dist/cjs/components/SshField/utils/readFileContent.js +6 -11
- package/dist/cjs/components/SshField/utils/validateSSHKey.d.ts +9 -3
- package/dist/cjs/components/SshField/utils/validateSSHKey.js +56 -59
- package/dist/esm/components/SshField/SshField.d.ts +1 -1
- package/dist/esm/components/SshField/SshField.js +34 -47
- package/dist/esm/components/SshField/components/MobileFieldAi/MobileFieldAi.d.ts +3 -0
- package/dist/esm/components/SshField/components/MobileFieldAi/MobileFieldAi.js +2 -2
- package/dist/esm/components/SshField/helperComponents/CheckItem/CheckItem.d.ts +6 -0
- package/dist/esm/components/SshField/helperComponents/CheckItem/CheckItem.js +7 -0
- package/dist/esm/components/SshField/helperComponents/CheckItem/index.d.ts +1 -0
- package/dist/esm/components/SshField/helperComponents/CheckItem/index.js +1 -0
- package/dist/esm/components/SshField/helperComponents/CheckItem/styles.module.css +25 -0
- package/dist/esm/components/SshField/helperComponents/DropZoneContent/styles.module.css +1 -1
- package/dist/esm/components/SshField/helperComponents/SshValidation/SshValidation.d.ts +6 -0
- package/dist/esm/components/SshField/helperComponents/SshValidation/SshValidation.js +10 -0
- package/dist/esm/components/SshField/helperComponents/SshValidation/index.d.ts +1 -0
- package/dist/esm/components/SshField/helperComponents/SshValidation/index.js +1 -0
- package/dist/esm/components/SshField/helperComponents/SshValidation/styles.module.css +35 -0
- package/dist/esm/components/SshField/helperComponents/WithSshValidation/WithSshValidation.d.ts +8 -0
- package/dist/esm/components/SshField/helperComponents/WithSshValidation/WithSshValidation.js +13 -0
- package/dist/esm/components/SshField/helperComponents/WithSshValidation/index.d.ts +1 -0
- package/dist/esm/components/SshField/helperComponents/WithSshValidation/index.js +1 -0
- package/dist/esm/components/SshField/helperComponents/WithSshValidation/styles.module.css +5 -0
- package/dist/esm/components/SshField/types.d.ts +12 -0
- package/dist/esm/components/SshField/types.js +1 -0
- package/dist/esm/components/SshField/utils/readFileContent.d.ts +6 -1
- package/dist/esm/components/SshField/utils/readFileContent.js +6 -11
- package/dist/esm/components/SshField/utils/validateSSHKey.d.ts +9 -3
- package/dist/esm/components/SshField/utils/validateSSHKey.js +53 -55
- package/package.json +7 -6
- package/src/components/SshField/SshField.tsx +120 -121
- package/src/components/SshField/components/MobileFieldAi/MobileFieldAi.tsx +6 -5
- package/src/components/SshField/helperComponents/CheckItem/CheckItem.tsx +23 -0
- package/src/components/SshField/helperComponents/CheckItem/index.ts +1 -0
- package/src/components/SshField/helperComponents/CheckItem/styles.module.scss +31 -0
- package/src/components/SshField/helperComponents/DropZoneContent/styles.module.scss +1 -1
- package/src/components/SshField/helperComponents/SshValidation/SshValidation.tsx +36 -0
- package/src/components/SshField/helperComponents/SshValidation/index.ts +1 -0
- package/src/components/SshField/helperComponents/SshValidation/styles.module.scss +31 -0
- package/src/components/SshField/helperComponents/WithSshValidation/WithSshValidation.tsx +43 -0
- package/src/components/SshField/helperComponents/WithSshValidation/index.ts +1 -0
- package/src/components/SshField/helperComponents/WithSshValidation/styles.module.scss +7 -0
- package/src/components/SshField/types.ts +13 -0
- package/src/components/SshField/utils/readFileContent.ts +12 -11
- package/src/components/SshField/utils/validateSSHKey.ts +60 -68
- package/dist/cjs/components/SshField/utils/handleFileError.d.ts +0 -2
- package/dist/cjs/components/SshField/utils/handleFileError.js +0 -32
- package/dist/esm/components/SshField/utils/handleFileError.d.ts +0 -2
- package/dist/esm/components/SshField/utils/handleFileError.js +0 -28
- package/src/components/SshField/utils/handleFileError.ts +0 -41
|
@@ -33,11 +33,11 @@ import { MobileFieldAi } from './components/MobileFieldAi';
|
|
|
33
33
|
import { DropZoneContent } from './helperComponents/DropZoneContent';
|
|
34
34
|
import { FieldSubmitButton } from './helperComponents/FieldSubmitButton';
|
|
35
35
|
import { TextAreaActionsFooter } from './helperComponents/TextAreaActionsFooter';
|
|
36
|
+
import { WithSshValidation } from './helperComponents/WithSshValidation';
|
|
36
37
|
import styles from './styles.module.css';
|
|
37
|
-
import { getFileErrorType } from './utils/handleFileError';
|
|
38
38
|
import { isTouchDevice as isTouchDeviceHelper } from './utils/isTouchDevice';
|
|
39
39
|
import { readFileContent } from './utils/readFileContent';
|
|
40
|
-
import {
|
|
40
|
+
import { validateFileErrors, validateSshKeyErrors } from './utils/validateSSHKey';
|
|
41
41
|
export const SshField = forwardRef((_a, ref) => {
|
|
42
42
|
var { onSubmit: handleSubmitProp, onCancel, value, disabled, className } = _a, props = __rest(_a, ["onSubmit", "onCancel", "value", "disabled", "className"]);
|
|
43
43
|
const { layoutType, validationState, onChange } = props;
|
|
@@ -46,36 +46,19 @@ export const SshField = forwardRef((_a, ref) => {
|
|
|
46
46
|
const [isLoading, setIsLoading] = useState(false);
|
|
47
47
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
48
48
|
const [isValueHidden, setIsValueHidden] = useState(true);
|
|
49
|
-
const [
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const getErrorMessage = (errorType) => {
|
|
53
|
-
switch (errorType) {
|
|
54
|
-
case 'EMPTY_FILE':
|
|
55
|
-
return t('SshField.errors.emptyFile');
|
|
56
|
-
case 'BINARY_DATA':
|
|
57
|
-
return t('SshField.errors.binaryData');
|
|
58
|
-
case 'INVALID_SSH_KEY':
|
|
59
|
-
return t('SshField.errors.invalidSSHKey');
|
|
60
|
-
case 'INVALID_EXTENSION':
|
|
61
|
-
case 'INVALID_MIME_TYPE':
|
|
62
|
-
case 'INVALID_FILE_TYPE':
|
|
63
|
-
return t('SshField.errors.invalidFileExtension');
|
|
64
|
-
case 'FILE_TOO_LARGE':
|
|
65
|
-
return t('SshField.errors.fileTooLarge');
|
|
66
|
-
case 'READ_ERROR':
|
|
67
|
-
return t('SshField.errors.readError');
|
|
68
|
-
case 'UNKNOWN_ERROR':
|
|
69
|
-
default:
|
|
70
|
-
return t('SshField.errors.unknownError');
|
|
71
|
-
}
|
|
72
|
-
};
|
|
49
|
+
const [sshValidation, setSshValidation] = useState(null);
|
|
50
|
+
const isSshValid = !sshValidation || Object.values(sshValidation).every(value => !value);
|
|
51
|
+
const isValueValid = isSshValid && typeof value === 'string' && value.trim().length > 0;
|
|
73
52
|
const handleChange = (newValue) => {
|
|
74
|
-
if (
|
|
75
|
-
|
|
53
|
+
if (sshValidation) {
|
|
54
|
+
setSshValidation(null);
|
|
76
55
|
}
|
|
77
|
-
if (onChange)
|
|
78
|
-
|
|
56
|
+
if (!onChange)
|
|
57
|
+
return;
|
|
58
|
+
onChange(newValue);
|
|
59
|
+
if (newValue) {
|
|
60
|
+
const sshValidationState = validateSshKeyErrors(newValue);
|
|
61
|
+
setSshValidation(sshValidationState);
|
|
79
62
|
}
|
|
80
63
|
};
|
|
81
64
|
const handleDragOver = (event) => {
|
|
@@ -106,18 +89,22 @@ export const SshField = forwardRef((_a, ref) => {
|
|
|
106
89
|
const onFileUpload = (file) => __awaiter(void 0, void 0, void 0, function* () {
|
|
107
90
|
try {
|
|
108
91
|
setIsLoading(true);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (onChange) {
|
|
115
|
-
onChange(fileContent);
|
|
92
|
+
setSshValidation(null);
|
|
93
|
+
const fileValidationErrorState = validateFileErrors(file);
|
|
94
|
+
if (fileValidationErrorState.fileType) {
|
|
95
|
+
setSshValidation(fileValidationErrorState);
|
|
96
|
+
return;
|
|
116
97
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
98
|
+
const { error, fileContent } = yield readFileContent(file);
|
|
99
|
+
if (error || typeof fileContent !== 'string') {
|
|
100
|
+
setSshValidation(Object.assign(Object.assign({}, fileValidationErrorState), { readError: true }));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const sshValidationState = validateSshKeyErrors(fileContent);
|
|
104
|
+
setSshValidation(Object.assign(Object.assign({}, fileValidationErrorState), sshValidationState));
|
|
105
|
+
if (!onChange)
|
|
106
|
+
return;
|
|
107
|
+
onChange(fileContent);
|
|
121
108
|
}
|
|
122
109
|
finally {
|
|
123
110
|
setIsLoading(false);
|
|
@@ -125,11 +112,11 @@ export const SshField = forwardRef((_a, ref) => {
|
|
|
125
112
|
}
|
|
126
113
|
});
|
|
127
114
|
if (isTouchDevice) {
|
|
128
|
-
return (_jsx(MobileFieldAi, Object.assign({}, props, getAdaptiveFieldProps(props), { onSubmit: handleSubmit, submitEnabled: isValueValid && !disabled, ref: ref, value: value })));
|
|
115
|
+
return (_jsx(WithSshValidation, { layoutType: layoutType, sshValidation: sshValidation, children: _jsx(MobileFieldAi, Object.assign({}, props, { onChange: handleChange, onFileUpload: onFileUpload }, getAdaptiveFieldProps(props), { onSubmit: handleSubmit, submitEnabled: isValueValid && !disabled, ref: ref, value: value })) }));
|
|
129
116
|
}
|
|
130
|
-
return (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
117
|
+
return (_jsx("div", { className: cn(styles.wrapper, className), onDragOver: handleDragOver, onDragLeave: handleDragLeave, children: _jsxs(WithSshValidation, { layoutType: layoutType, sshValidation: sshValidation, children: [_jsx(ChatStatusAnnouncement, { className: styles.chatStatus, layoutType: layoutType, icon: _jsx(PasswordLockSVG, { size: 16, color: themeVars.sys.neutral.textSupport }), content: [
|
|
118
|
+
{ content: t('SshField.chatStatusAnnouncement.content.option1') },
|
|
119
|
+
{ content: t('SshField.chatStatusAnnouncement.content.option2'), shouldFocusOnHover: true },
|
|
120
|
+
{ content: t('SshField.chatStatusAnnouncement.content.option3') },
|
|
121
|
+
], actionLabel: t('SshField.chatStatusAnnouncement.cancel'), onActionClick: onCancel }), isDragOver ? (_jsx(DropZone, { description: _jsx(DropZoneContent, {}), className: styles.dropZone, mode: 'single', onFilesUpload: (files) => onFileUpload(files[0]) })) : (_jsx(AdaptiveFieldTextArea, Object.assign({}, props, { ref: ref, value: value, onChange: handleChange, size: 'm', disabled: isLoading, minRows: 2, maxRows: 4, placeholder: t('SshField.placeholder'), className: isValueHidden ? styles.secured : undefined, onKeyDown: handleKeyDown, validationState: isSshValid ? validationState : 'error', hint: props.hint, footer: _jsx(TextAreaActionsFooter, { left: _jsx(ButtonFunction, { size: 'xs', icon: isValueHidden ? _jsx(EyeSVG, {}) : _jsx(EyeClosedSVG, {}), onClick: () => setIsValueHidden(prev => !prev), disabled: isLoading }), right: _jsxs(_Fragment, { children: [_jsx(Tooltip, { tip: t('SshField.attachFileTooltip'), hoverDelayOpen: 600, triggerClassName: styles.uploadTooltip, open: isTouchDevice ? false : undefined, children: _jsx(FileUpload, { mode: 'single', onFilesUpload: (files) => onFileUpload(files[0]), children: _jsx(ButtonFunction, { disabled: isLoading, size: isTouchDevice ? 's' : 'xs', icon: _jsx(AttachmentSVG, {}) }) }) }), _jsx(FieldSubmitButton, { disabled: isLoading, showTooltip: !isTouchDevice, className: isTouchDevice ? styles.mobileSubmitButton : undefined, active: isValueValid && !disabled, handleClick: handleSubmit, size: isTouchDevice ? 's' : 'xs' })] }) }) })))] }) }));
|
|
135
122
|
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { FieldTextAreaProps } from '@cloud-ru/uikit-product-mobile-fields';
|
|
2
2
|
export declare const MobileFieldAi: import("react").ForwardRefExoticComponent<Omit<FieldTextAreaProps, "label" | "size" | "placeholder" | "spellCheck" | "labelTooltip" | "required" | "footer"> & {
|
|
3
|
+
layoutType: import("@cloud-ru/uikit-product-utils").LayoutType;
|
|
4
|
+
} & {
|
|
3
5
|
onSubmit(): void;
|
|
4
6
|
submitEnabled: boolean;
|
|
7
|
+
onFileUpload(file: File): void;
|
|
5
8
|
} & import("react").RefAttributes<HTMLTextAreaElement>>;
|
|
@@ -23,7 +23,7 @@ import styles from './styles.module.css';
|
|
|
23
23
|
const MIN_ROWS = 1;
|
|
24
24
|
const MAX_ROWS = 6;
|
|
25
25
|
export const MobileFieldAi = forwardRef((_a, ref) => {
|
|
26
|
-
var { onSubmit, value, submitEnabled } = _a, props = __rest(_a, ["onSubmit", "value", "submitEnabled"]);
|
|
26
|
+
var { onSubmit, value, submitEnabled, onFileUpload } = _a, props = __rest(_a, ["onSubmit", "value", "submitEnabled", "onFileUpload"]);
|
|
27
27
|
const { t } = useLocale('Claudia');
|
|
28
|
-
return (_jsxs("div", { className: styles.mobileInputWrapper, style: { '--max-rows': MAX_ROWS, '--min-rows': MIN_ROWS }, "data-size": 'm', children: [_jsx(Scroll, { className: styles.scrollContainer, size: 's', barHideStrategy: 'never', children: _jsx(TextArea, Object.assign({}, props, { className: styles.textarea, ref: ref, value: value, minRows: MIN_ROWS, placeholder: t('SshField.placeholder'), spellCheck: true })) }), _jsxs("div", { className: styles.mobileSubmitButtonWrapper, children: [_jsx(Tooltip, { disableSpanWrapper: true, tip: t('SshField.attachFileTooltip'), hoverDelayOpen: 600, triggerClassName: styles.uploadTooltip, children: _jsx(FileUpload, { mode: 'multiple', onFilesUpload: () =>
|
|
28
|
+
return (_jsxs("div", { className: styles.mobileInputWrapper, style: { '--max-rows': MAX_ROWS, '--min-rows': MIN_ROWS }, "data-size": 'm', children: [_jsx(Scroll, { className: styles.scrollContainer, size: 's', barHideStrategy: 'never', children: _jsx(TextArea, Object.assign({}, props, { className: styles.textarea, ref: ref, value: value, minRows: MIN_ROWS, placeholder: t('SshField.placeholder'), spellCheck: true })) }), _jsxs("div", { className: styles.mobileSubmitButtonWrapper, children: [_jsx(Tooltip, { disableSpanWrapper: true, tip: t('SshField.attachFileTooltip'), hoverDelayOpen: 600, triggerClassName: styles.uploadTooltip, children: _jsx(FileUpload, { mode: 'multiple', onFilesUpload: (files) => onFileUpload(files[0]), children: _jsx(ButtonFunction, { size: 's', icon: _jsx(AttachmentSVG, {}) }) }) }), _jsx(FieldSubmitButton, { showTooltip: false, className: styles.mobileSubmitButton, fullWidth: true, active: submitEnabled, handleClick: onSubmit, size: 's' })] })] }));
|
|
29
29
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CrossFilledSVG } from '@cloud-ru/uikit-product-icons';
|
|
3
|
+
import { Typography } from '@snack-uikit/typography';
|
|
4
|
+
import styles from './styles.module.css';
|
|
5
|
+
export function CheckItem({ label, layoutType }) {
|
|
6
|
+
return (_jsxs("div", { className: styles.checkItem, "data-layout-type": layoutType, children: [_jsx("div", { className: styles.iconWrapper, children: _jsx(CrossFilledSVG, { size: 16, className: styles.icon }) }), _jsx(Typography.SansBodyM, { "data-layout-type": layoutType, className: styles.label, children: label })] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './CheckItem';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './CheckItem';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.checkItem{
|
|
2
|
+
display:flex;
|
|
3
|
+
flex-direction:row;
|
|
4
|
+
gap:var(--dimension-050m, 4px);
|
|
5
|
+
}
|
|
6
|
+
.checkItem[data-layout-type=mobile], .checkItem[data-layout-type=tablet]{
|
|
7
|
+
gap:var(--dimension-1m, 8px);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.iconWrapper{
|
|
11
|
+
width:var(--dimension-2m, 16px);
|
|
12
|
+
height:var(--dimension-2m, 16px);
|
|
13
|
+
transform:translateY(var(--dimension-025m, 2px));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.icon{
|
|
17
|
+
color:var(--sys-red-accent-default, #cb3f3e);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.label{
|
|
21
|
+
color:var(--sys-red-decor-default, #fdd6cd);
|
|
22
|
+
}
|
|
23
|
+
.label[data-layout-type=mobile], .label[data-layout-type=tablet]{
|
|
24
|
+
color:var(--sys-red-text-main, #7a2d2d);
|
|
25
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { WithLayoutType } from '@cloud-ru/uikit-product-utils';
|
|
2
|
+
import { ValidationState } from '../../types';
|
|
3
|
+
export type WithPasswordTooltipProps = WithLayoutType<{
|
|
4
|
+
sshValidation: ValidationState | null;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function SshValidation({ sshValidation, layoutType }: WithPasswordTooltipProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useLocale } from '@cloud-ru/uikit-product-locale';
|
|
3
|
+
import { CheckItem } from '../CheckItem';
|
|
4
|
+
import styles from './styles.module.css';
|
|
5
|
+
export function SshValidation({ sshValidation, layoutType }) {
|
|
6
|
+
const { t } = useLocale('Claudia');
|
|
7
|
+
if (!sshValidation)
|
|
8
|
+
return null;
|
|
9
|
+
return (_jsx("div", { className: styles.validationItemsContainer, "data-layout-type": layoutType, children: _jsxs("div", { className: styles.validationList, children: [sshValidation.fileSize && _jsx(CheckItem, { label: t('SshField.errors.fileSize'), layoutType: layoutType }), sshValidation.fileType && _jsx(CheckItem, { label: t('SshField.errors.fileType'), layoutType: layoutType }), sshValidation.binaryData && _jsx(CheckItem, { label: t('SshField.errors.binaryData'), layoutType: layoutType }), sshValidation.emptyFile && _jsx(CheckItem, { label: t('SshField.errors.emptyFile'), layoutType: layoutType }), sshValidation.invalidSSHKey && (_jsx(CheckItem, { label: t('SshField.errors.invalidSSHKey'), layoutType: layoutType })), sshValidation.readError && _jsx(CheckItem, { label: t('SshField.errors.readError'), layoutType: layoutType })] }) }));
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SshValidation';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SshValidation';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
.tooltipText{
|
|
2
|
+
display:flex;
|
|
3
|
+
flex-direction:column;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.validationList{
|
|
7
|
+
display:flex;
|
|
8
|
+
flex-direction:column;
|
|
9
|
+
gap:var(--dimension-1m, 8px);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.validationItemsContainer{
|
|
13
|
+
font-family:var(--sans-body-m-font-family, SB Sans Interface);
|
|
14
|
+
font-weight:var(--sans-body-m-font-weight, Regular);
|
|
15
|
+
line-height:var(--sans-body-m-line-height, 20px);
|
|
16
|
+
font-size:var(--sans-body-m-font-size, 14px);
|
|
17
|
+
letter-spacing:var(--sans-body-m-letter-spacing, 0.1px);
|
|
18
|
+
paragraph-spacing:var(--sans-body-m-paragraph-spacing, 7.7px);
|
|
19
|
+
color:var(--sys-neutral-text-main, #41424e);
|
|
20
|
+
display:flex;
|
|
21
|
+
flex-direction:column;
|
|
22
|
+
gap:var(--dimension-1m, 8px);
|
|
23
|
+
}
|
|
24
|
+
.validationItemsContainer[data-layout-type=mobile], .validationItemsContainer[data-layout-type=tablet]{
|
|
25
|
+
gap:var(--dimension-1m, 8px);
|
|
26
|
+
font-family:var(--sans-body-s-font-family, SB Sans Interface);
|
|
27
|
+
font-weight:var(--sans-body-s-font-weight, Regular);
|
|
28
|
+
line-height:var(--sans-body-s-line-height, 16px);
|
|
29
|
+
font-size:var(--sans-body-s-font-size, 12px);
|
|
30
|
+
letter-spacing:var(--sans-body-s-letter-spacing, 0.1px);
|
|
31
|
+
paragraph-spacing:var(--sans-body-s-paragraph-spacing, 6.6px);
|
|
32
|
+
}
|
|
33
|
+
.validationItemsContainer[data-layout-type=mobile] .validationList, .validationItemsContainer[data-layout-type=tablet] .validationList{
|
|
34
|
+
gap:var(--dimension-025m, 2px);
|
|
35
|
+
}
|
package/dist/esm/components/SshField/helperComponents/WithSshValidation/WithSshValidation.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { WithLayoutType } from '@cloud-ru/uikit-product-utils';
|
|
3
|
+
import { ValidationState } from '../../types';
|
|
4
|
+
export type WithSshValidationProps = WithLayoutType<{
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
sshValidation: ValidationState | null;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function WithSshValidation({ sshValidation, layoutType, children }: WithSshValidationProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { AdaptiveTooltip } from '@cloud-ru/uikit-product-mobile-tooltip';
|
|
4
|
+
import { isTouchDevice } from '../../utils/isTouchDevice';
|
|
5
|
+
import { SshValidation } from '../SshValidation';
|
|
6
|
+
import styles from './styles.module.css';
|
|
7
|
+
export function WithSshValidation({ sshValidation, layoutType, children }) {
|
|
8
|
+
const isSshValidationError = useMemo(() => (sshValidation ? Object.values(sshValidation).some(item => item) : false), [sshValidation]);
|
|
9
|
+
if (isTouchDevice(layoutType)) {
|
|
10
|
+
return (_jsxs("div", { className: styles.validationContainer, children: [_jsx(SshValidation, { sshValidation: sshValidation, layoutType: layoutType }), children] }));
|
|
11
|
+
}
|
|
12
|
+
return (_jsx(AdaptiveTooltip, { placement: 'left-end', layoutType: layoutType, tip: _jsx(SshValidation, { sshValidation: sshValidation, layoutType: layoutType }), open: isSshValidationError, offset: 8, children: children }));
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './WithSshValidation';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './WithSshValidation';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type FileError = {
|
|
2
|
+
fileType: boolean;
|
|
3
|
+
fileSize: boolean;
|
|
4
|
+
readError: boolean;
|
|
5
|
+
};
|
|
6
|
+
type FileContentError = {
|
|
7
|
+
binaryData: boolean;
|
|
8
|
+
emptyFile: boolean;
|
|
9
|
+
invalidSSHKey: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type ValidationState = Partial<FileError & FileContentError>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
export const readFileContent = (file) => new Promise(
|
|
1
|
+
export const readFileContent = (file) => new Promise(resolve => {
|
|
2
2
|
const reader = new FileReader();
|
|
3
3
|
reader.onload = (e) => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
resolve(e.target.result);
|
|
4
|
+
if (e.target && typeof e.target.result === 'string') {
|
|
5
|
+
resolve({ error: false, fileContent: e.target.result });
|
|
7
6
|
}
|
|
8
7
|
else {
|
|
9
|
-
|
|
8
|
+
resolve({ error: true });
|
|
10
9
|
}
|
|
11
10
|
};
|
|
12
|
-
reader.onerror = () => {
|
|
13
|
-
|
|
14
|
-
};
|
|
15
|
-
reader.onabort = () => {
|
|
16
|
-
reject(new Error('READ_ERROR: Чтение файла было прервано'));
|
|
17
|
-
};
|
|
11
|
+
reader.onerror = () => resolve({ error: true });
|
|
12
|
+
reader.onabort = () => resolve({ error: true });
|
|
18
13
|
// Читаем как текст
|
|
19
14
|
reader.readAsText(file);
|
|
20
15
|
});
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
export declare const
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export declare const validateFileErrors: (file: File) => {
|
|
2
|
+
fileSize: boolean;
|
|
3
|
+
fileType: boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare const validateSshKeyErrors: (value: string) => {
|
|
6
|
+
binaryChars: boolean;
|
|
7
|
+
emptyFile: boolean;
|
|
8
|
+
invalidSSHKey: boolean;
|
|
9
|
+
};
|
|
@@ -1,60 +1,58 @@
|
|
|
1
|
-
export const validateSSHKeyContent = (content) => {
|
|
2
|
-
const trimmedContent = content.trim();
|
|
3
|
-
// Проверка на пустой файл
|
|
4
|
-
if (trimmedContent.length === 0) {
|
|
5
|
-
throw new Error('INVALID_SSH_KEY: Файл пустой');
|
|
6
|
-
}
|
|
7
|
-
// Проверка на бинарный файл (простейшая проверка)
|
|
8
|
-
const binaryChars = content
|
|
9
|
-
.split('')
|
|
10
|
-
.filter(char => char.charCodeAt(0) < 32 && char !== '\n' && char !== '\r' && char !== '\t').length;
|
|
11
|
-
if (binaryChars > content.length * 0.1) {
|
|
12
|
-
// Если больше 10% бинарных символов
|
|
13
|
-
throw new Error('INVALID_SSH_KEY: Файл содержит бинарные данные, а не текстовый SSH ключ');
|
|
14
|
-
}
|
|
15
|
-
// Базовая проверка на формат SSH ключа
|
|
16
|
-
const sshKeyPatterns = [
|
|
17
|
-
/^-----BEGIN (?:RSA|DSA|EC|OPENSSH) PRIVATE KEY-----/,
|
|
18
|
-
/^ssh-(rsa|dsa|ecdsa|ed25519)/,
|
|
19
|
-
/^ecdsa-sha2-nistp/,
|
|
20
|
-
/^-----BEGIN.*PRIVATE KEY-----/,
|
|
21
|
-
/^-----BEGIN.*CERTIFICATE-----/,
|
|
22
|
-
];
|
|
23
|
-
const isValidSSHKey = sshKeyPatterns.some(pattern => pattern.test(trimmedContent));
|
|
24
|
-
if (!isValidSSHKey) {
|
|
25
|
-
throw new Error('INVALID_SSH_KEY: Файл не содержит валидный SSH ключ. Поддерживаются: RSA, DSA, ECDSA, Ed25519 ключи и сертификаты');
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
const DEFAULT_ALLOWED_MIME_TYPES = ['text/plain'];
|
|
29
1
|
const MAX_FILE_SIZE = 10 * 1024; // 10KB по умолчанию
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
2
|
+
const SSH_KEY_BEGIN_PATTERN = [
|
|
3
|
+
/^-----BEGIN (?:RSA|DSA|EC|OPENSSH) PRIVATE KEY-----/,
|
|
4
|
+
/^-----BEGIN.*PRIVATE KEY-----/,
|
|
5
|
+
/^-----BEGIN.*CERTIFICATE-----/,
|
|
6
|
+
];
|
|
7
|
+
const SSH_KEY_END_PATTERN = [
|
|
8
|
+
/-----END (?:RSA|DSA|EC|OPENSSH) PRIVATE KEY-----/,
|
|
9
|
+
/-----END.*PRIVATE KEY-----/,
|
|
10
|
+
/-----END.*CERTIFICATE-----/,
|
|
11
|
+
];
|
|
12
|
+
const checkIsFileSizeError = (file) => file.size > MAX_FILE_SIZE;
|
|
13
|
+
function getFileExtension(filename) {
|
|
14
|
+
return filename.includes('.') ? filename.split('.').pop() : '';
|
|
15
|
+
}
|
|
16
|
+
const checkIsFileTypeError = (file) => Boolean(getFileExtension(file.name));
|
|
17
|
+
export const validateFileErrors = (file) => ({
|
|
18
|
+
fileSize: checkIsFileSizeError(file),
|
|
19
|
+
fileType: checkIsFileTypeError(file),
|
|
20
|
+
});
|
|
21
|
+
const checkIsEmptyContent = (value) => !value.trim().length;
|
|
22
|
+
const checkIsBinaryCharsInContent = (value) => {
|
|
23
|
+
const binaryChars = value
|
|
24
|
+
.split('')
|
|
25
|
+
.filter(char => char.charCodeAt(0) < 32 && !['\n', '\r', '\t'].includes(char)).length;
|
|
26
|
+
return binaryChars > value.length * 0.1;
|
|
37
27
|
};
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
28
|
+
const removePEMBoundaries = (sshFileContent) => {
|
|
29
|
+
let replacedContent = sshFileContent.trim().replaceAll('\n', '');
|
|
30
|
+
let isBeginPartExist = false;
|
|
31
|
+
let isEndPartExist = false;
|
|
32
|
+
SSH_KEY_BEGIN_PATTERN.forEach(pattern => {
|
|
33
|
+
if (pattern.test(replacedContent)) {
|
|
34
|
+
isBeginPartExist = true;
|
|
35
|
+
replacedContent = replacedContent.replace(new RegExp(pattern, 'g'), '');
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
SSH_KEY_END_PATTERN.forEach(pattern => {
|
|
39
|
+
if (pattern.test(replacedContent)) {
|
|
40
|
+
isEndPartExist = true;
|
|
41
|
+
replacedContent = replacedContent.replace(new RegExp(pattern, 'g'), '');
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return { isError: !(isBeginPartExist && isEndPartExist), content: replacedContent };
|
|
49
45
|
};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
// Минимальный размер для SSH ключа (примерно 100 байт)
|
|
56
|
-
const minFileSize = 100;
|
|
57
|
-
if (file.size < minFileSize) {
|
|
58
|
-
throw new Error(`INVALID_FILE_TYPE: Файл слишком маленький для SSH ключа. Минимальный размер: ${minFileSize} байт`);
|
|
46
|
+
const checkIsSshKeyError = (value) => {
|
|
47
|
+
const { isError, content: base64content } = removePEMBoundaries(value);
|
|
48
|
+
if (isError) {
|
|
49
|
+
return true;
|
|
59
50
|
}
|
|
51
|
+
const base64Regexp = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[+/=])[A-Za-z0-9+/=\r\n]+$/;
|
|
52
|
+
return !base64Regexp.test(base64content);
|
|
60
53
|
};
|
|
54
|
+
export const validateSshKeyErrors = (value) => ({
|
|
55
|
+
binaryChars: checkIsBinaryCharsInContent(value),
|
|
56
|
+
emptyFile: checkIsEmptyContent(value),
|
|
57
|
+
invalidSSHKey: checkIsSshKeyError(value),
|
|
58
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloud-ru/uikit-product-claudia",
|
|
3
3
|
"title": "Claudia",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.12.1",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"*.css",
|
|
7
7
|
"*.woff",
|
|
@@ -36,10 +36,11 @@
|
|
|
36
36
|
},
|
|
37
37
|
"scripts": {},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@cloud-ru/uikit-product-icons": "16.1.
|
|
40
|
-
"@cloud-ru/uikit-product-mobile-dropdown": "0.9.
|
|
41
|
-
"@cloud-ru/uikit-product-mobile-fields": "0.12.
|
|
42
|
-
"@cloud-ru/uikit-product-
|
|
39
|
+
"@cloud-ru/uikit-product-icons": "16.1.1",
|
|
40
|
+
"@cloud-ru/uikit-product-mobile-dropdown": "0.9.31",
|
|
41
|
+
"@cloud-ru/uikit-product-mobile-fields": "0.12.3",
|
|
42
|
+
"@cloud-ru/uikit-product-mobile-tooltip": "0.5.4",
|
|
43
|
+
"@cloud-ru/uikit-product-utils": "8.1.0",
|
|
43
44
|
"@snack-uikit/button": "0.19.16",
|
|
44
45
|
"@snack-uikit/divider": "3.2.3",
|
|
45
46
|
"@snack-uikit/drop-zone": "0.9.6",
|
|
@@ -61,5 +62,5 @@
|
|
|
61
62
|
"@cloud-ru/uikit-product-locale": "*",
|
|
62
63
|
"@snack-uikit/figma-tokens": "*"
|
|
63
64
|
},
|
|
64
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "1a98d6d9bff32edcbb3f9b0be0a14dce3f2abe5d"
|
|
65
66
|
}
|