@cloud-ru/uikit-product-claudia 1.11.1 → 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 +11 -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 +3 -2
  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
@@ -19,11 +19,12 @@ import { MobileFieldAi } from './components/MobileFieldAi';
19
19
  import { DropZoneContent } from './helperComponents/DropZoneContent';
20
20
  import { FieldSubmitButton } from './helperComponents/FieldSubmitButton';
21
21
  import { TextAreaActionsFooter } from './helperComponents/TextAreaActionsFooter';
22
+ import { WithSshValidation } from './helperComponents/WithSshValidation';
22
23
  import styles from './styles.module.scss';
23
- import { FileErrorType, getFileErrorType } from './utils/handleFileError';
24
+ import { ValidationState } from './types';
24
25
  import { isTouchDevice as isTouchDeviceHelper } from './utils/isTouchDevice';
25
26
  import { readFileContent } from './utils/readFileContent';
26
- import { validateFileSize, validateFileType, validateSSHKeyContent } from './utils/validateSSHKey';
27
+ import { validateFileErrors, validateSshKeyErrors } from './utils/validateSSHKey';
27
28
 
28
29
  export type SshFieldProps = WithLayoutType<
29
30
  Omit<FieldTextAreaProps, 'placeholder' | 'labelTooltip' | 'label' | 'required' | 'size' | 'spellCheck' | 'footer'> & {
@@ -42,39 +43,23 @@ export const SshField = forwardRef<HTMLTextAreaElement, SshFieldProps>(
42
43
  const [isLoading, setIsLoading] = useState<boolean>(false);
43
44
  const [isDragOver, setIsDragOver] = useState(false);
44
45
  const [isValueHidden, setIsValueHidden] = useState<boolean>(true);
45
- const [fileErrorType, setFileErrorType] = useState<FileErrorType | null>(null);
46
-
47
- const isValueValid = typeof value === 'string' && value.trim().length > 0;
48
- const showFileError = Boolean(fileErrorType);
49
-
50
- const getErrorMessage = (errorType: FileErrorType): string => {
51
- switch (errorType) {
52
- case 'EMPTY_FILE':
53
- return t('SshField.errors.emptyFile');
54
- case 'BINARY_DATA':
55
- return t('SshField.errors.binaryData');
56
- case 'INVALID_SSH_KEY':
57
- return t('SshField.errors.invalidSSHKey');
58
- case 'INVALID_EXTENSION':
59
- case 'INVALID_MIME_TYPE':
60
- case 'INVALID_FILE_TYPE':
61
- return t('SshField.errors.invalidFileExtension');
62
- case 'FILE_TOO_LARGE':
63
- return t('SshField.errors.fileTooLarge');
64
- case 'READ_ERROR':
65
- return t('SshField.errors.readError');
66
- case 'UNKNOWN_ERROR':
67
- default:
68
- return t('SshField.errors.unknownError');
69
- }
70
- };
46
+ const [sshValidation, setSshValidation] = useState<ValidationState | null>(null);
47
+
48
+ const isSshValid = !sshValidation || Object.values(sshValidation).every(value => !value);
49
+ const isValueValid = isSshValid && typeof value === 'string' && value.trim().length > 0;
71
50
 
72
51
  const handleChange = (newValue: string) => {
73
- if (fileErrorType) {
74
- setFileErrorType(null);
52
+ if (sshValidation) {
53
+ setSshValidation(null);
75
54
  }
76
- if (onChange) {
77
- onChange(newValue);
55
+
56
+ if (!onChange) return;
57
+
58
+ onChange(newValue);
59
+
60
+ if (newValue) {
61
+ const sshValidationState = validateSshKeyErrors(newValue);
62
+ setSshValidation(sshValidationState);
78
63
  }
79
64
  };
80
65
 
@@ -113,20 +98,28 @@ export const SshField = forwardRef<HTMLTextAreaElement, SshFieldProps>(
113
98
  const onFileUpload = async (file: File) => {
114
99
  try {
115
100
  setIsLoading(true);
116
- setFileErrorType(null);
117
- validateFileType(file);
118
- validateFileSize(file);
101
+ setSshValidation(null);
119
102
 
120
- const fileContent = await readFileContent(file);
103
+ const fileValidationErrorState = validateFileErrors(file);
121
104
 
122
- validateSSHKeyContent(fileContent);
105
+ if (fileValidationErrorState.fileType) {
106
+ setSshValidation(fileValidationErrorState);
107
+ return;
108
+ }
109
+
110
+ const { error, fileContent } = await readFileContent(file);
123
111
 
124
- if (onChange) {
125
- onChange(fileContent);
112
+ if (error || typeof fileContent !== 'string') {
113
+ setSshValidation({ ...fileValidationErrorState, readError: true });
114
+ return;
126
115
  }
127
- } catch (err) {
128
- const errorType = getFileErrorType(err);
129
- setFileErrorType(errorType);
116
+
117
+ const sshValidationState = validateSshKeyErrors(fileContent);
118
+ setSshValidation({ ...fileValidationErrorState, ...sshValidationState });
119
+
120
+ if (!onChange) return;
121
+
122
+ onChange(fileContent);
130
123
  } finally {
131
124
  setIsLoading(false);
132
125
  setIsDragOver(false);
@@ -135,93 +128,99 @@ export const SshField = forwardRef<HTMLTextAreaElement, SshFieldProps>(
135
128
 
136
129
  if (isTouchDevice) {
137
130
  return (
138
- <MobileFieldAi
139
- {...props}
140
- {...getAdaptiveFieldProps(props)}
141
- onSubmit={handleSubmit}
142
- submitEnabled={isValueValid && !disabled}
143
- ref={ref}
144
- value={value}
145
- />
131
+ <WithSshValidation layoutType={layoutType} sshValidation={sshValidation}>
132
+ <MobileFieldAi
133
+ {...props}
134
+ onChange={handleChange}
135
+ onFileUpload={onFileUpload}
136
+ {...getAdaptiveFieldProps(props)}
137
+ onSubmit={handleSubmit}
138
+ submitEnabled={isValueValid && !disabled}
139
+ ref={ref}
140
+ value={value}
141
+ />
142
+ </WithSshValidation>
146
143
  );
147
144
  }
148
145
 
149
146
  return (
150
147
  <div className={cn(styles.wrapper, className)} onDragOver={handleDragOver} onDragLeave={handleDragLeave}>
151
- <ChatStatusAnnouncement
152
- className={styles.chatStatus}
153
- layoutType={layoutType}
154
- icon={<PasswordLockSVG size={16} color={themeVars.sys.neutral.textSupport} />}
155
- content={[
156
- { content: t('SshField.chatStatusAnnouncement.content.option1') },
157
- { content: t('SshField.chatStatusAnnouncement.content.option2'), shouldFocusOnHover: true },
158
- { content: t('SshField.chatStatusAnnouncement.content.option3') },
159
- ]}
160
- actionLabel={t('SshField.chatStatusAnnouncement.cancel')}
161
- onActionClick={onCancel}
162
- />
163
- {isDragOver ? (
164
- <DropZone
165
- description={<DropZoneContent />}
166
- className={styles.dropZone}
167
- mode='single'
168
- onFilesUpload={(files: File[]) => onFileUpload(files[0])}
148
+ <WithSshValidation layoutType={layoutType} sshValidation={sshValidation}>
149
+ <ChatStatusAnnouncement
150
+ className={styles.chatStatus}
151
+ layoutType={layoutType}
152
+ icon={<PasswordLockSVG size={16} color={themeVars.sys.neutral.textSupport} />}
153
+ content={[
154
+ { content: t('SshField.chatStatusAnnouncement.content.option1') },
155
+ { content: t('SshField.chatStatusAnnouncement.content.option2'), shouldFocusOnHover: true },
156
+ { content: t('SshField.chatStatusAnnouncement.content.option3') },
157
+ ]}
158
+ actionLabel={t('SshField.chatStatusAnnouncement.cancel')}
159
+ onActionClick={onCancel}
169
160
  />
170
- ) : (
171
- <AdaptiveFieldTextArea
172
- {...props}
173
- ref={ref}
174
- value={value}
175
- onChange={handleChange}
176
- size='m'
177
- disabled={isLoading}
178
- minRows={2}
179
- maxRows={4}
180
- placeholder={t('SshField.placeholder')}
181
- className={isValueHidden ? styles.secured : undefined}
182
- onKeyDown={handleKeyDown}
183
- validationState={showFileError ? 'error' : validationState}
184
- hint={showFileError && fileErrorType ? getErrorMessage(fileErrorType) : props.hint}
185
- footer={
186
- <TextAreaActionsFooter
187
- left={
188
- <ButtonFunction
189
- size='xs'
190
- icon={isValueHidden ? <EyeSVG /> : <EyeClosedSVG />}
191
- onClick={() => setIsValueHidden(prev => !prev)}
192
- disabled={isLoading}
193
- />
194
- }
195
- right={
196
- <>
197
- <Tooltip
198
- tip={t('SshField.attachFileTooltip')}
199
- hoverDelayOpen={600}
200
- triggerClassName={styles.uploadTooltip}
201
- open={isTouchDevice ? false : undefined}
202
- >
203
- <FileUpload mode='single' onFilesUpload={(files: File[]) => onFileUpload(files[0])}>
204
- <ButtonFunction
205
- disabled={isLoading}
206
- size={isTouchDevice ? 's' : 'xs'}
207
- icon={<AttachmentSVG />}
208
- />
209
- </FileUpload>
210
- </Tooltip>
211
- <FieldSubmitButton
161
+ {isDragOver ? (
162
+ <DropZone
163
+ description={<DropZoneContent />}
164
+ className={styles.dropZone}
165
+ mode='single'
166
+ onFilesUpload={(files: File[]) => onFileUpload(files[0])}
167
+ />
168
+ ) : (
169
+ <AdaptiveFieldTextArea
170
+ {...props}
171
+ ref={ref}
172
+ value={value}
173
+ onChange={handleChange}
174
+ size='m'
175
+ disabled={isLoading}
176
+ minRows={2}
177
+ maxRows={4}
178
+ placeholder={t('SshField.placeholder')}
179
+ className={isValueHidden ? styles.secured : undefined}
180
+ onKeyDown={handleKeyDown}
181
+ validationState={isSshValid ? validationState : 'error'}
182
+ hint={props.hint}
183
+ footer={
184
+ <TextAreaActionsFooter
185
+ left={
186
+ <ButtonFunction
187
+ size='xs'
188
+ icon={isValueHidden ? <EyeSVG /> : <EyeClosedSVG />}
189
+ onClick={() => setIsValueHidden(prev => !prev)}
212
190
  disabled={isLoading}
213
- showTooltip={!isTouchDevice}
214
- className={isTouchDevice ? styles.mobileSubmitButton : undefined}
215
- active={isValueValid && !disabled}
216
- handleClick={handleSubmit}
217
- size={isTouchDevice ? 's' : 'xs'}
218
191
  />
219
- </>
220
- }
221
- />
222
- }
223
- />
224
- )}
192
+ }
193
+ right={
194
+ <>
195
+ <Tooltip
196
+ tip={t('SshField.attachFileTooltip')}
197
+ hoverDelayOpen={600}
198
+ triggerClassName={styles.uploadTooltip}
199
+ open={isTouchDevice ? false : undefined}
200
+ >
201
+ <FileUpload mode='single' onFilesUpload={(files: File[]) => onFileUpload(files[0])}>
202
+ <ButtonFunction
203
+ disabled={isLoading}
204
+ size={isTouchDevice ? 's' : 'xs'}
205
+ icon={<AttachmentSVG />}
206
+ />
207
+ </FileUpload>
208
+ </Tooltip>
209
+ <FieldSubmitButton
210
+ disabled={isLoading}
211
+ showTooltip={!isTouchDevice}
212
+ className={isTouchDevice ? styles.mobileSubmitButton : undefined}
213
+ active={isValueValid && !disabled}
214
+ handleClick={handleSubmit}
215
+ size={isTouchDevice ? 's' : 'xs'}
216
+ />
217
+ </>
218
+ }
219
+ />
220
+ }
221
+ />
222
+ )}
223
+ </WithSshValidation>
225
224
  </div>
226
225
  );
227
226
  },
@@ -3,6 +3,7 @@ import { forwardRef } from 'react';
3
3
  import { AttachmentSVG } from '@cloud-ru/uikit-product-icons';
4
4
  import { useLocale } from '@cloud-ru/uikit-product-locale';
5
5
  import { FieldTextAreaProps } from '@cloud-ru/uikit-product-mobile-fields';
6
+ import { WithLayoutType } from '@cloud-ru/uikit-product-utils';
6
7
  import { ButtonFunction } from '@snack-uikit/button';
7
8
  import { FileUpload } from '@snack-uikit/drop-zone';
8
9
  import { Scroll } from '@snack-uikit/scroll';
@@ -12,19 +13,19 @@ import { FieldSubmitButton } from '../../helperComponents/FieldSubmitButton';
12
13
  import { TextArea } from '../TextArea';
13
14
  import styles from './styles.module.scss';
14
15
 
15
- type MobileFieldAiProps = Omit<
16
- FieldTextAreaProps,
17
- 'placeholder' | 'labelTooltip' | 'label' | 'required' | 'size' | 'spellCheck' | 'footer'
16
+ type MobileFieldAiProps = WithLayoutType<
17
+ Omit<FieldTextAreaProps, 'placeholder' | 'labelTooltip' | 'label' | 'required' | 'size' | 'spellCheck' | 'footer'>
18
18
  > & {
19
19
  onSubmit(): void;
20
20
  submitEnabled: boolean;
21
+ onFileUpload(file: File): void;
21
22
  };
22
23
 
23
24
  const MIN_ROWS = 1;
24
25
  const MAX_ROWS = 6;
25
26
 
26
27
  export const MobileFieldAi = forwardRef<HTMLTextAreaElement, MobileFieldAiProps>(
27
- ({ onSubmit, value, submitEnabled, ...props }, ref) => {
28
+ ({ onSubmit, value, submitEnabled, onFileUpload, ...props }, ref) => {
28
29
  const { t } = useLocale('Claudia');
29
30
 
30
31
  return (
@@ -52,7 +53,7 @@ export const MobileFieldAi = forwardRef<HTMLTextAreaElement, MobileFieldAiProps>
52
53
  hoverDelayOpen={600}
53
54
  triggerClassName={styles.uploadTooltip}
54
55
  >
55
- <FileUpload mode='multiple' onFilesUpload={() => {}}>
56
+ <FileUpload mode='multiple' onFilesUpload={(files: File[]) => onFileUpload(files[0])}>
56
57
  <ButtonFunction size={'s'} icon={<AttachmentSVG />} />
57
58
  </FileUpload>
58
59
  </Tooltip>
@@ -0,0 +1,23 @@
1
+ import { CrossFilledSVG } from '@cloud-ru/uikit-product-icons';
2
+ import { WithLayoutType } from '@cloud-ru/uikit-product-utils';
3
+ import { Typography } from '@snack-uikit/typography';
4
+
5
+ import styles from './styles.module.scss';
6
+
7
+ type CheckItemProps = WithLayoutType<{
8
+ label: string;
9
+ }>;
10
+
11
+ export function CheckItem({ label, layoutType }: CheckItemProps) {
12
+ return (
13
+ <div className={styles.checkItem} data-layout-type={layoutType}>
14
+ <div className={styles.iconWrapper}>
15
+ <CrossFilledSVG size={16} className={styles.icon} />
16
+ </div>
17
+
18
+ <Typography.SansBodyM data-layout-type={layoutType} className={styles.label}>
19
+ {label}
20
+ </Typography.SansBodyM>
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1 @@
1
+ export * from './CheckItem';
@@ -0,0 +1,31 @@
1
+ @use '@sbercloud/figma-tokens-cloud-platform/build/scss/styles-theme-variables' as ste;
2
+
3
+ .checkItem {
4
+ display: flex;
5
+ flex-direction: row;
6
+ gap: ste.$dimension-050m;
7
+
8
+ &[data-layout-type='mobile'],
9
+ &[data-layout-type='tablet'] {
10
+ gap: ste.$dimension-1m;
11
+ }
12
+ }
13
+
14
+ .iconWrapper {
15
+ width: ste.$dimension-2m;
16
+ height: ste.$dimension-2m;
17
+ transform: translateY(ste.$dimension-025m);
18
+ }
19
+
20
+ .icon {
21
+ color: ste.$sys-red-accent-default;
22
+ }
23
+
24
+ .label {
25
+ color: ste.$sys-red-decor-default;
26
+
27
+ &[data-layout-type='mobile'],
28
+ &[data-layout-type='tablet'] {
29
+ color: ste.$sys-red-text-main;
30
+ }
31
+ }
@@ -5,7 +5,7 @@
5
5
  flex-direction: column;
6
6
  justify-content: center;
7
7
  align-items: center;
8
- gap: 4px;
8
+ gap: stv.$dimension-050m;
9
9
  }
10
10
 
11
11
  .defaultText {
@@ -0,0 +1,36 @@
1
+ import { useLocale } from '@cloud-ru/uikit-product-locale';
2
+ import { WithLayoutType } from '@cloud-ru/uikit-product-utils';
3
+
4
+ import { ValidationState } from '../../types';
5
+ import { CheckItem } from '../CheckItem';
6
+ import styles from './styles.module.scss';
7
+
8
+ export type WithPasswordTooltipProps = WithLayoutType<{
9
+ sshValidation: ValidationState | null;
10
+ }>;
11
+
12
+ export function SshValidation({ sshValidation, layoutType }: WithPasswordTooltipProps) {
13
+ const { t } = useLocale('Claudia');
14
+
15
+ if (!sshValidation) return null;
16
+
17
+ return (
18
+ <div className={styles.validationItemsContainer} data-layout-type={layoutType}>
19
+ <div className={styles.validationList}>
20
+ {sshValidation.fileSize && <CheckItem label={t('SshField.errors.fileSize')} layoutType={layoutType} />}
21
+
22
+ {sshValidation.fileType && <CheckItem label={t('SshField.errors.fileType')} layoutType={layoutType} />}
23
+
24
+ {sshValidation.binaryData && <CheckItem label={t('SshField.errors.binaryData')} layoutType={layoutType} />}
25
+
26
+ {sshValidation.emptyFile && <CheckItem label={t('SshField.errors.emptyFile')} layoutType={layoutType} />}
27
+
28
+ {sshValidation.invalidSSHKey && (
29
+ <CheckItem label={t('SshField.errors.invalidSSHKey')} layoutType={layoutType} />
30
+ )}
31
+
32
+ {sshValidation.readError && <CheckItem label={t('SshField.errors.readError')} layoutType={layoutType} />}
33
+ </div>
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1 @@
1
+ export * from './SshValidation';
@@ -0,0 +1,31 @@
1
+ @use '@sbercloud/figma-tokens-cloud-platform/build/scss/styles-theme-variables' as ste;
2
+
3
+ .tooltipText {
4
+ display: flex;
5
+ flex-direction: column;
6
+ }
7
+
8
+ .validationList {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: ste.$dimension-1m;
12
+ }
13
+
14
+ .validationItemsContainer {
15
+ @include ste.composite-var(ste.$sans-body-m);
16
+ color: ste.$sys-neutral-text-main;
17
+
18
+ display: flex;
19
+ flex-direction: column;
20
+ gap: ste.$dimension-1m;
21
+
22
+ &[data-layout-type='mobile'],
23
+ &[data-layout-type='tablet'] {
24
+ gap: ste.$dimension-1m;
25
+ @include ste.composite-var(ste.$sans-body-s);
26
+
27
+ .validationList {
28
+ gap: ste.$dimension-025m;
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,43 @@
1
+ import { ReactNode, useMemo } from 'react';
2
+
3
+ import { AdaptiveTooltip } from '@cloud-ru/uikit-product-mobile-tooltip';
4
+ import { WithLayoutType } from '@cloud-ru/uikit-product-utils';
5
+
6
+ import { ValidationState } from '../../types';
7
+ import { isTouchDevice } from '../../utils/isTouchDevice';
8
+ import { SshValidation } from '../SshValidation';
9
+ import styles from './styles.module.scss';
10
+
11
+ export type WithSshValidationProps = WithLayoutType<{
12
+ children: ReactNode;
13
+ sshValidation: ValidationState | null;
14
+ }>;
15
+
16
+ export function WithSshValidation({ sshValidation, layoutType, children }: WithSshValidationProps) {
17
+ const isSshValidationError = useMemo(
18
+ () => (sshValidation ? Object.values(sshValidation).some(item => item) : false),
19
+ [sshValidation],
20
+ );
21
+
22
+ if (isTouchDevice(layoutType)) {
23
+ return (
24
+ <div className={styles.validationContainer}>
25
+ <SshValidation sshValidation={sshValidation} layoutType={layoutType} />
26
+
27
+ {children}
28
+ </div>
29
+ );
30
+ }
31
+
32
+ return (
33
+ <AdaptiveTooltip
34
+ placement='left-end'
35
+ layoutType={layoutType}
36
+ tip={<SshValidation sshValidation={sshValidation} layoutType={layoutType} />}
37
+ open={isSshValidationError}
38
+ offset={8}
39
+ >
40
+ {children}
41
+ </AdaptiveTooltip>
42
+ );
43
+ }
@@ -0,0 +1 @@
1
+ export * from './WithSshValidation';
@@ -0,0 +1,7 @@
1
+ @use '@sbercloud/figma-tokens-cloud-platform/build/scss/styles-theme-variables' as ste;
2
+
3
+ .validationContainer {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: ste.$dimension-1m;
7
+ }
@@ -0,0 +1,13 @@
1
+ type FileError = {
2
+ fileType: boolean;
3
+ fileSize: boolean;
4
+ readError: boolean;
5
+ };
6
+
7
+ type FileContentError = {
8
+ binaryData: boolean;
9
+ emptyFile: boolean;
10
+ invalidSSHKey: boolean;
11
+ };
12
+
13
+ export type ValidationState = Partial<FileError & FileContentError>;
@@ -1,22 +1,23 @@
1
- export const readFileContent = (file: File): Promise<string> =>
2
- new Promise((resolve, reject) => {
1
+ type ReadFileContentResult = {
2
+ error: boolean;
3
+ fileContent?: string;
4
+ };
5
+
6
+ export const readFileContent = (file: File): Promise<ReadFileContentResult> =>
7
+ new Promise(resolve => {
3
8
  const reader = new FileReader();
4
9
 
5
10
  reader.onload = (e: ProgressEvent<FileReader>) => {
6
- if (e.target?.result && typeof e.target.result === 'string') {
7
- resolve(e.target.result);
11
+ if (e.target && typeof e.target.result === 'string') {
12
+ resolve({ error: false, fileContent: e.target.result });
8
13
  } else {
9
- reject(new Error('READ_ERROR: Не удалось прочитать содержимое файла'));
14
+ resolve({ error: true });
10
15
  }
11
16
  };
12
17
 
13
- reader.onerror = () => {
14
- reject(new Error('READ_ERROR: Не удалось прочитать файл'));
15
- };
18
+ reader.onerror = () => resolve({ error: true });
16
19
 
17
- reader.onabort = () => {
18
- reject(new Error('READ_ERROR: Чтение файла было прервано'));
19
- };
20
+ reader.onabort = () => resolve({ error: true });
20
21
 
21
22
  // Читаем как текст
22
23
  reader.readAsText(file);