@cloud-ru/uikit-product-claudia 1.11.0 → 1.12.0

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/cjs/components/SshField/SshField.js +33 -46
  3. package/dist/cjs/components/SshField/components/MobileFieldAi/MobileFieldAi.d.ts +3 -0
  4. package/dist/cjs/components/SshField/components/MobileFieldAi/MobileFieldAi.js +2 -2
  5. package/dist/cjs/components/SshField/helperComponents/CheckItem/CheckItem.d.ts +6 -0
  6. package/dist/cjs/components/SshField/helperComponents/CheckItem/CheckItem.js +13 -0
  7. package/dist/cjs/components/SshField/helperComponents/CheckItem/index.d.ts +1 -0
  8. package/dist/cjs/components/SshField/helperComponents/CheckItem/index.js +17 -0
  9. package/dist/cjs/components/SshField/helperComponents/CheckItem/styles.module.css +25 -0
  10. package/dist/cjs/components/SshField/helperComponents/DropZoneContent/styles.module.css +1 -1
  11. package/dist/cjs/components/SshField/helperComponents/SshValidation/SshValidation.d.ts +6 -0
  12. package/dist/cjs/components/SshField/helperComponents/SshValidation/SshValidation.js +16 -0
  13. package/dist/cjs/components/SshField/helperComponents/SshValidation/index.d.ts +1 -0
  14. package/dist/cjs/components/SshField/helperComponents/SshValidation/index.js +17 -0
  15. package/dist/cjs/components/SshField/helperComponents/SshValidation/styles.module.css +35 -0
  16. package/dist/cjs/components/SshField/helperComponents/WithSshValidation/WithSshValidation.d.ts +8 -0
  17. package/dist/cjs/components/SshField/helperComponents/WithSshValidation/WithSshValidation.js +19 -0
  18. package/dist/cjs/components/SshField/helperComponents/WithSshValidation/index.d.ts +1 -0
  19. package/dist/cjs/components/SshField/helperComponents/WithSshValidation/index.js +17 -0
  20. package/dist/cjs/components/SshField/helperComponents/WithSshValidation/styles.module.css +5 -0
  21. package/dist/cjs/components/SshField/types.d.ts +12 -0
  22. package/dist/cjs/components/SshField/types.js +2 -0
  23. package/dist/cjs/components/SshField/utils/readFileContent.d.ts +6 -1
  24. package/dist/cjs/components/SshField/utils/readFileContent.js +6 -11
  25. package/dist/cjs/components/SshField/utils/validateSSHKey.d.ts +9 -3
  26. package/dist/cjs/components/SshField/utils/validateSSHKey.js +56 -59
  27. package/dist/esm/components/SshField/SshField.js +34 -47
  28. package/dist/esm/components/SshField/components/MobileFieldAi/MobileFieldAi.d.ts +3 -0
  29. package/dist/esm/components/SshField/components/MobileFieldAi/MobileFieldAi.js +2 -2
  30. package/dist/esm/components/SshField/helperComponents/CheckItem/CheckItem.d.ts +6 -0
  31. package/dist/esm/components/SshField/helperComponents/CheckItem/CheckItem.js +7 -0
  32. package/dist/esm/components/SshField/helperComponents/CheckItem/index.d.ts +1 -0
  33. package/dist/esm/components/SshField/helperComponents/CheckItem/index.js +1 -0
  34. package/dist/esm/components/SshField/helperComponents/CheckItem/styles.module.css +25 -0
  35. package/dist/esm/components/SshField/helperComponents/DropZoneContent/styles.module.css +1 -1
  36. package/dist/esm/components/SshField/helperComponents/SshValidation/SshValidation.d.ts +6 -0
  37. package/dist/esm/components/SshField/helperComponents/SshValidation/SshValidation.js +10 -0
  38. package/dist/esm/components/SshField/helperComponents/SshValidation/index.d.ts +1 -0
  39. package/dist/esm/components/SshField/helperComponents/SshValidation/index.js +1 -0
  40. package/dist/esm/components/SshField/helperComponents/SshValidation/styles.module.css +35 -0
  41. package/dist/esm/components/SshField/helperComponents/WithSshValidation/WithSshValidation.d.ts +8 -0
  42. package/dist/esm/components/SshField/helperComponents/WithSshValidation/WithSshValidation.js +13 -0
  43. package/dist/esm/components/SshField/helperComponents/WithSshValidation/index.d.ts +1 -0
  44. package/dist/esm/components/SshField/helperComponents/WithSshValidation/index.js +1 -0
  45. package/dist/esm/components/SshField/helperComponents/WithSshValidation/styles.module.css +5 -0
  46. package/dist/esm/components/SshField/types.d.ts +12 -0
  47. package/dist/esm/components/SshField/types.js +1 -0
  48. package/dist/esm/components/SshField/utils/readFileContent.d.ts +6 -1
  49. package/dist/esm/components/SshField/utils/readFileContent.js +6 -11
  50. package/dist/esm/components/SshField/utils/validateSSHKey.d.ts +9 -3
  51. package/dist/esm/components/SshField/utils/validateSSHKey.js +53 -55
  52. package/package.json +6 -5
  53. package/src/components/SshField/SshField.tsx +120 -121
  54. package/src/components/SshField/components/MobileFieldAi/MobileFieldAi.tsx +6 -5
  55. package/src/components/SshField/helperComponents/CheckItem/CheckItem.tsx +23 -0
  56. package/src/components/SshField/helperComponents/CheckItem/index.ts +1 -0
  57. package/src/components/SshField/helperComponents/CheckItem/styles.module.scss +31 -0
  58. package/src/components/SshField/helperComponents/DropZoneContent/styles.module.scss +1 -1
  59. package/src/components/SshField/helperComponents/SshValidation/SshValidation.tsx +36 -0
  60. package/src/components/SshField/helperComponents/SshValidation/index.ts +1 -0
  61. package/src/components/SshField/helperComponents/SshValidation/styles.module.scss +31 -0
  62. package/src/components/SshField/helperComponents/WithSshValidation/WithSshValidation.tsx +43 -0
  63. package/src/components/SshField/helperComponents/WithSshValidation/index.ts +1 -0
  64. package/src/components/SshField/helperComponents/WithSshValidation/styles.module.scss +7 -0
  65. package/src/components/SshField/types.ts +13 -0
  66. package/src/components/SshField/utils/readFileContent.ts +12 -11
  67. package/src/components/SshField/utils/validateSSHKey.ts +60 -68
  68. package/dist/cjs/components/SshField/utils/handleFileError.d.ts +0 -2
  69. package/dist/cjs/components/SshField/utils/handleFileError.js +0 -32
  70. package/dist/esm/components/SshField/utils/handleFileError.d.ts +0 -2
  71. package/dist/esm/components/SshField/utils/handleFileError.js +0 -28
  72. 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 { validateFileSize, validateFileType, validateSSHKeyContent } from './utils/validateSSHKey';
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 [fileErrorType, setFileErrorType] = useState(null);
50
- const isValueValid = typeof value === 'string' && value.trim().length > 0;
51
- const showFileError = Boolean(fileErrorType);
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 (fileErrorType) {
75
- setFileErrorType(null);
53
+ if (sshValidation) {
54
+ setSshValidation(null);
76
55
  }
77
- if (onChange) {
78
- onChange(newValue);
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
- setFileErrorType(null);
110
- validateFileType(file);
111
- validateFileSize(file);
112
- const fileContent = yield readFileContent(file);
113
- validateSSHKeyContent(fileContent);
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
- catch (err) {
119
- const errorType = getFileErrorType(err);
120
- setFileErrorType(errorType);
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 (_jsxs("div", { className: cn(styles.wrapper, className), onDragOver: handleDragOver, onDragLeave: handleDragLeave, children: [_jsx(ChatStatusAnnouncement, { className: styles.chatStatus, layoutType: layoutType, icon: _jsx(PasswordLockSVG, { size: 16, color: themeVars.sys.neutral.textSupport }), content: [
131
- { content: t('SshField.chatStatusAnnouncement.content.option1') },
132
- { content: t('SshField.chatStatusAnnouncement.content.option2'), shouldFocusOnHover: true },
133
- { content: t('SshField.chatStatusAnnouncement.content.option3') },
134
- ], 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: showFileError ? 'error' : validationState, hint: showFileError && fileErrorType ? getErrorMessage(fileErrorType) : 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' })] }) }) })))] }));
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: () => { }, children: _jsx(ButtonFunction, { size: 's', icon: _jsx(AttachmentSVG, {}) }) }) }), _jsx(FieldSubmitButton, { showTooltip: false, className: styles.mobileSubmitButton, fullWidth: true, active: submitEnabled, handleClick: onSubmit, size: 's' })] })] }));
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,6 @@
1
+ import { WithLayoutType } from '@cloud-ru/uikit-product-utils';
2
+ type CheckItemProps = WithLayoutType<{
3
+ label: string;
4
+ }>;
5
+ export declare function CheckItem({ label, layoutType }: CheckItemProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -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
+ }
@@ -3,7 +3,7 @@
3
3
  flex-direction:column;
4
4
  justify-content:center;
5
5
  align-items:center;
6
- gap:4px;
6
+ gap:var(--dimension-050m, 4px);
7
7
  }
8
8
 
9
9
  .defaultText{
@@ -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
+ }
@@ -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,5 @@
1
+ .validationContainer{
2
+ display:flex;
3
+ flex-direction:column;
4
+ gap:var(--dimension-1m, 8px);
5
+ }
@@ -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 +1,6 @@
1
- export declare const readFileContent: (file: File) => Promise<string>;
1
+ type ReadFileContentResult = {
2
+ error: boolean;
3
+ fileContent?: string;
4
+ };
5
+ export declare const readFileContent: (file: File) => Promise<ReadFileContentResult>;
6
+ export {};
@@ -1,20 +1,15 @@
1
- export const readFileContent = (file) => new Promise((resolve, reject) => {
1
+ export const readFileContent = (file) => new Promise(resolve => {
2
2
  const reader = new FileReader();
3
3
  reader.onload = (e) => {
4
- var _a;
5
- if (((_a = e.target) === null || _a === void 0 ? void 0 : _a.result) && typeof e.target.result === 'string') {
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
- reject(new Error('READ_ERROR: Не удалось прочитать содержимое файла'));
8
+ resolve({ error: true });
10
9
  }
11
10
  };
12
- reader.onerror = () => {
13
- reject(new Error('READ_ERROR: Не удалось прочитать файл'));
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 validateSSHKeyContent: (content: string) => void;
2
- export declare const validateFileType: (file: File) => void;
3
- export declare const validateFileSize: (file: File) => void;
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 getFileExtension = (filename) => {
31
- const lastDotIndex = filename.lastIndexOf('.');
32
- // Если точка не найдена или это первая точка в имени файла (скрытые файлы в Unix)
33
- if (lastDotIndex <= 0) {
34
- return '';
35
- }
36
- return filename.toLowerCase().slice(lastDotIndex);
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 getFileMimeType = (file) => file.type;
39
- export const validateFileType = (file) => {
40
- const fileExtension = getFileExtension(file.name);
41
- const mimeType = getFileMimeType(file);
42
- // Проверка по MIME type (более надежный способ)
43
- const isValidMimeType = DEFAULT_ALLOWED_MIME_TYPES.some(allowedMime => mimeType.includes(allowedMime) || allowedMime.includes(mimeType));
44
- // Файл должен пройти хотя бы одну проверку
45
- if (!isValidMimeType) {
46
- throw new Error(`INVALID_MIME_TYPE: Неподходящий тип файла. MIME: ${mimeType}, расширение: ${fileExtension || 'нет'}. ` +
47
- `Разрешенные MIME types: ${DEFAULT_ALLOWED_MIME_TYPES.join(', ')}, `);
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
- export const validateFileSize = (file) => {
51
- if (file.size > MAX_FILE_SIZE) {
52
- throw new Error(`FILE_TOO_LARGE: Файл слишком большой. Размер: ${(file.size / 1024).toFixed(2)}KB, ` +
53
- `максимальный: ${MAX_FILE_SIZE / 1024}KB`);
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.11.0",
4
+ "version": "1.12.0",
5
5
  "sideEffects": [
6
6
  "*.css",
7
7
  "*.woff",
@@ -36,9 +36,10 @@
36
36
  },
37
37
  "scripts": {},
38
38
  "dependencies": {
39
- "@cloud-ru/uikit-product-icons": "16.0.1",
40
- "@cloud-ru/uikit-product-mobile-dropdown": "0.9.29",
41
- "@cloud-ru/uikit-product-mobile-fields": "0.12.1",
39
+ "@cloud-ru/uikit-product-icons": "16.1.0",
40
+ "@cloud-ru/uikit-product-mobile-dropdown": "0.9.30",
41
+ "@cloud-ru/uikit-product-mobile-fields": "0.12.2",
42
+ "@cloud-ru/uikit-product-mobile-tooltip": "0.5.3",
42
43
  "@cloud-ru/uikit-product-utils": "8.0.2",
43
44
  "@snack-uikit/button": "0.19.16",
44
45
  "@snack-uikit/divider": "3.2.3",
@@ -61,5 +62,5 @@
61
62
  "@cloud-ru/uikit-product-locale": "*",
62
63
  "@snack-uikit/figma-tokens": "*"
63
64
  },
64
- "gitHead": "c0456040cbe87eb2ae4cc85990654dafee744543"
65
+ "gitHead": "77987340e4904dec7ce4ba5ae8da7e3e8a75dd1d"
65
66
  }