@oneuptime/common 8.0.5579 → 8.0.5581

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 (181) hide show
  1. package/Models/DatabaseModels/AlertInternalNote.ts +58 -1
  2. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +1 -0
  3. package/Models/DatabaseModels/File.ts +1 -1
  4. package/Models/DatabaseModels/IncidentInternalNote.ts +58 -1
  5. package/Models/DatabaseModels/IncidentPublicNote.ts +58 -1
  6. package/Models/DatabaseModels/ScheduledMaintenanceInternalNote.ts +58 -1
  7. package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +58 -1
  8. package/Models/DatabaseModels/StatusPageAnnouncement.ts +49 -0
  9. package/Server/API/AlertInternalNoteAPI.ts +96 -0
  10. package/Server/API/IncidentInternalNoteAPI.ts +96 -0
  11. package/Server/API/IncidentPublicNoteAPI.ts +96 -0
  12. package/Server/API/ScheduledMaintenanceInternalNoteAPI.ts +100 -0
  13. package/Server/API/ScheduledMaintenancePublicNoteAPI.ts +100 -0
  14. package/Server/API/StatusPageAPI.ts +585 -59
  15. package/Server/API/StatusPageAnnouncementAPI.ts +98 -0
  16. package/Server/API/UserAPI.ts +95 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1763471659817-MigrationName.ts +79 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1763477560906-MigrationName.ts +81 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1763480947474-MigrationName.ts +79 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  21. package/Server/Middleware/ProjectAuthorization.ts +3 -1
  22. package/Server/Services/AlertInternalNoteService.ts +75 -2
  23. package/Server/Services/IncidentInternalNoteService.ts +76 -2
  24. package/Server/Services/IncidentPublicNoteService.ts +76 -2
  25. package/Server/Services/ScheduledMaintenanceInternalNoteService.ts +76 -2
  26. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +76 -2
  27. package/Server/Services/ScheduledMaintenanceService.ts +10 -7
  28. package/Server/Services/StatusPagePrivateUserService.ts +10 -7
  29. package/Server/Services/StatusPageService.ts +12 -7
  30. package/Server/Services/StatusPageSubscriberService.ts +19 -13
  31. package/Server/Utils/FileAttachmentMarkdownUtil.ts +98 -0
  32. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +9 -1
  33. package/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.ts +34 -0
  34. package/Server/Utils/Monitor/DataToProcess.ts +3 -1
  35. package/Server/Utils/Monitor/MonitorCriteriaDataExtractor.ts +13 -0
  36. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +13 -0
  37. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +20 -0
  38. package/Server/Utils/Monitor/MonitorResource.ts +18 -0
  39. package/Server/Utils/Response.ts +13 -0
  40. package/Server/Utils/Telemetry.ts +15 -0
  41. package/Types/File/MimeType.ts +18 -0
  42. package/Types/Monitor/CriteriaFilter.ts +3 -0
  43. package/Types/Monitor/ExceptionMonitor/ExceptionMonitorResponse.ts +12 -0
  44. package/Types/Monitor/MonitorCriteriaInstance.ts +67 -0
  45. package/Types/Monitor/MonitorStep.ts +30 -0
  46. package/Types/Monitor/MonitorStepExceptionMonitor.ts +94 -0
  47. package/Types/Monitor/MonitorType.ts +10 -1
  48. package/Types/Telemetry/TelemetryQuery.ts +2 -1
  49. package/Types/Telemetry/TelemetryType.ts +1 -0
  50. package/UI/Components/AttachmentList/EventAttachmentList.tsx +121 -0
  51. package/UI/Components/EventItem/EventItem.tsx +22 -0
  52. package/UI/Components/Feed/FeedItem.tsx +9 -16
  53. package/UI/Components/FilePicker/FilePicker.tsx +441 -145
  54. package/UI/Components/Forms/Fields/FormField.tsx +32 -15
  55. package/UI/Components/Forms/FormSummary.tsx +168 -1
  56. package/UI/Components/Forms/ModelForm.tsx +46 -24
  57. package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
  58. package/UI/Components/Icon/Icon.tsx +1 -1
  59. package/UI/Utils/API/RequestOptions.ts +2 -0
  60. package/UI/Utils/ModelAPI/ModelAPI.ts +18 -0
  61. package/UI/Utils/User.ts +8 -0
  62. package/Utils/API.ts +11 -1
  63. package/build/dist/Models/DatabaseModels/AlertInternalNote.js +49 -1
  64. package/build/dist/Models/DatabaseModels/AlertInternalNote.js.map +1 -1
  65. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +1 -0
  66. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  67. package/build/dist/Models/DatabaseModels/File.js +1 -1
  68. package/build/dist/Models/DatabaseModels/File.js.map +1 -1
  69. package/build/dist/Models/DatabaseModels/IncidentInternalNote.js +49 -1
  70. package/build/dist/Models/DatabaseModels/IncidentInternalNote.js.map +1 -1
  71. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +49 -1
  72. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  73. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceInternalNote.js +49 -1
  74. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceInternalNote.js.map +1 -1
  75. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +49 -1
  76. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
  77. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +48 -0
  78. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  79. package/build/dist/Server/API/AlertInternalNoteAPI.js +68 -0
  80. package/build/dist/Server/API/AlertInternalNoteAPI.js.map +1 -0
  81. package/build/dist/Server/API/IncidentInternalNoteAPI.js +68 -0
  82. package/build/dist/Server/API/IncidentInternalNoteAPI.js.map +1 -0
  83. package/build/dist/Server/API/IncidentPublicNoteAPI.js +68 -0
  84. package/build/dist/Server/API/IncidentPublicNoteAPI.js.map +1 -0
  85. package/build/dist/Server/API/ScheduledMaintenanceInternalNoteAPI.js +68 -0
  86. package/build/dist/Server/API/ScheduledMaintenanceInternalNoteAPI.js.map +1 -0
  87. package/build/dist/Server/API/ScheduledMaintenancePublicNoteAPI.js +68 -0
  88. package/build/dist/Server/API/ScheduledMaintenancePublicNoteAPI.js.map +1 -0
  89. package/build/dist/Server/API/StatusPageAPI.js +488 -85
  90. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  91. package/build/dist/Server/API/StatusPageAnnouncementAPI.js +68 -0
  92. package/build/dist/Server/API/StatusPageAnnouncementAPI.js.map +1 -0
  93. package/build/dist/Server/API/UserAPI.js +66 -0
  94. package/build/dist/Server/API/UserAPI.js.map +1 -0
  95. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1763471659817-MigrationName.js +34 -0
  96. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1763471659817-MigrationName.js.map +1 -0
  97. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1763477560906-MigrationName.js +34 -0
  98. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1763477560906-MigrationName.js.map +1 -0
  99. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1763480947474-MigrationName.js +34 -0
  100. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1763480947474-MigrationName.js.map +1 -0
  101. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  102. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  103. package/build/dist/Server/Middleware/ProjectAuthorization.js +4 -1
  104. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  105. package/build/dist/Server/Services/AlertInternalNoteService.js +54 -2
  106. package/build/dist/Server/Services/AlertInternalNoteService.js.map +1 -1
  107. package/build/dist/Server/Services/IncidentInternalNoteService.js +54 -2
  108. package/build/dist/Server/Services/IncidentInternalNoteService.js.map +1 -1
  109. package/build/dist/Server/Services/IncidentPublicNoteService.js +54 -2
  110. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  111. package/build/dist/Server/Services/ScheduledMaintenanceInternalNoteService.js +54 -2
  112. package/build/dist/Server/Services/ScheduledMaintenanceInternalNoteService.js.map +1 -1
  113. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +54 -2
  114. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  115. package/build/dist/Server/Services/ScheduledMaintenanceService.js +6 -5
  116. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  117. package/build/dist/Server/Services/StatusPagePrivateUserService.js +6 -4
  118. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  119. package/build/dist/Server/Services/StatusPageService.js +7 -4
  120. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  121. package/build/dist/Server/Services/StatusPageSubscriberService.js +11 -7
  122. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  123. package/build/dist/Server/Utils/FileAttachmentMarkdownUtil.js +67 -0
  124. package/build/dist/Server/Utils/FileAttachmentMarkdownUtil.js.map +1 -0
  125. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +6 -1
  126. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  127. package/build/dist/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.js +34 -0
  128. package/build/dist/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.js.map +1 -0
  129. package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js +6 -0
  130. package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js.map +1 -1
  131. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +10 -0
  132. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  133. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
  134. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  135. package/build/dist/Server/Utils/Monitor/MonitorResource.js +10 -0
  136. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  137. package/build/dist/Server/Utils/Response.js +8 -0
  138. package/build/dist/Server/Utils/Response.js.map +1 -1
  139. package/build/dist/Server/Utils/Telemetry.js +8 -1
  140. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  141. package/build/dist/Types/File/MimeType.js +18 -0
  142. package/build/dist/Types/File/MimeType.js.map +1 -1
  143. package/build/dist/Types/Monitor/CriteriaFilter.js +2 -0
  144. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  145. package/build/dist/Types/Monitor/ExceptionMonitor/ExceptionMonitorResponse.js +2 -0
  146. package/build/dist/Types/Monitor/ExceptionMonitor/ExceptionMonitorResponse.js.map +1 -0
  147. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +62 -0
  148. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  149. package/build/dist/Types/Monitor/MonitorStep.js +20 -1
  150. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  151. package/build/dist/Types/Monitor/MonitorStepExceptionMonitor.js +58 -0
  152. package/build/dist/Types/Monitor/MonitorStepExceptionMonitor.js.map +1 -0
  153. package/build/dist/Types/Monitor/MonitorType.js +9 -1
  154. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  155. package/build/dist/Types/Telemetry/TelemetryType.js +1 -0
  156. package/build/dist/Types/Telemetry/TelemetryType.js.map +1 -1
  157. package/build/dist/UI/Components/AttachmentList/EventAttachmentList.js +42 -0
  158. package/build/dist/UI/Components/AttachmentList/EventAttachmentList.js.map +1 -0
  159. package/build/dist/UI/Components/EventItem/EventItem.js +5 -1
  160. package/build/dist/UI/Components/EventItem/EventItem.js.map +1 -1
  161. package/build/dist/UI/Components/Feed/FeedItem.js +6 -4
  162. package/build/dist/UI/Components/Feed/FeedItem.js.map +1 -1
  163. package/build/dist/UI/Components/FilePicker/FilePicker.js +262 -77
  164. package/build/dist/UI/Components/FilePicker/FilePicker.js.map +1 -1
  165. package/build/dist/UI/Components/Forms/Fields/FormField.js +24 -12
  166. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  167. package/build/dist/UI/Components/Forms/FormSummary.js +77 -1
  168. package/build/dist/UI/Components/Forms/FormSummary.js.map +1 -1
  169. package/build/dist/UI/Components/Forms/ModelForm.js +32 -18
  170. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  171. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
  172. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
  173. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  174. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  175. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +30 -45
  176. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
  177. package/build/dist/UI/Utils/User.js +7 -0
  178. package/build/dist/UI/Utils/User.js.map +1 -1
  179. package/build/dist/Utils/API.js +3 -0
  180. package/build/dist/Utils/API.js.map +1 -1
  181. package/package.json +6 -6
@@ -1,8 +1,7 @@
1
1
  import { FILE_URL } from "../../Config";
2
2
  import API from "../../Utils/API/API";
3
3
  import ModelAPI from "../../Utils/ModelAPI/ModelAPI";
4
- import ComponentLoader from "../ComponentLoader/ComponentLoader";
5
- import Icon, { SizeProp } from "../Icon/Icon";
4
+ import Icon from "../Icon/Icon";
6
5
  import HTTPResponse from "../../../Types/API/HTTPResponse";
7
6
  import CommonURL from "../../../Types/API/URL";
8
7
  import Dictionary from "../../../Types/Dictionary";
@@ -15,7 +14,8 @@ import React, {
15
14
  useEffect,
16
15
  useState,
17
16
  } from "react";
18
- import { useDropzone } from "react-dropzone";
17
+ import { useDropzone, type FileRejection } from "react-dropzone";
18
+ import type { AxiosProgressEvent } from "axios";
19
19
 
20
20
  export interface ComponentProps {
21
21
  initialValue?: undefined | Array<FileModel> | FileModel;
@@ -34,6 +34,31 @@ export interface ComponentProps {
34
34
  error?: string | undefined;
35
35
  }
36
36
 
37
+ type UploadStatus = {
38
+ id: string;
39
+ name: string;
40
+ progress: number;
41
+ status: "uploading" | "error";
42
+ errorMessage?: string | undefined;
43
+ };
44
+
45
+ type AddUploadStatusFunction = (status: UploadStatus) => void;
46
+ type UpdateUploadStatusFunction = (
47
+ id: string,
48
+ updates: Partial<UploadStatus>,
49
+ ) => void;
50
+ type UpdateUploadProgressFunction = (
51
+ id: string,
52
+ total?: number,
53
+ loaded?: number,
54
+ ) => void;
55
+ type RemoveUploadStatusFunction = (id: string) => void;
56
+ type BuildFileSizeErrorFunction = (fileNames: Array<string>) => string;
57
+ type ResolveMimeTypeFunction = (file: File) => MimeType | undefined;
58
+ type FormatFileSizeFunction = (file: FileModel) => string | null;
59
+
60
+ const MAX_FILE_SIZE_BYTES: number = 10 * 1024 * 1024; // 10MB limit
61
+
37
62
  const FilePicker: FunctionComponent<ComponentProps> = (
38
63
  props: ComponentProps,
39
64
  ): ReactElement => {
@@ -42,6 +67,65 @@ const FilePicker: FunctionComponent<ComponentProps> = (
42
67
  const [filesModel, setFilesModel] = useState<Array<FileModel>>([]);
43
68
 
44
69
  const [acceptTypes, setAcceptTypes] = useState<Dictionary<Array<string>>>({});
70
+ const [uploadStatuses, setUploadStatuses] = useState<Array<UploadStatus>>([]);
71
+
72
+ const addUploadStatus: AddUploadStatusFunction = (
73
+ status: UploadStatus,
74
+ ): void => {
75
+ setUploadStatuses((current: Array<UploadStatus>) => {
76
+ return [...current, status];
77
+ });
78
+ };
79
+
80
+ const updateUploadStatus: UpdateUploadStatusFunction = (
81
+ id: string,
82
+ updates: Partial<UploadStatus>,
83
+ ): void => {
84
+ setUploadStatuses((current: Array<UploadStatus>) => {
85
+ return current.map((upload: UploadStatus) => {
86
+ return upload.id === id
87
+ ? {
88
+ ...upload,
89
+ ...updates,
90
+ }
91
+ : upload;
92
+ });
93
+ });
94
+ };
95
+
96
+ const updateUploadProgress: UpdateUploadProgressFunction = (
97
+ id: string,
98
+ total?: number,
99
+ loaded?: number,
100
+ ): void => {
101
+ setUploadStatuses((current: Array<UploadStatus>) => {
102
+ return current.map((upload: UploadStatus) => {
103
+ if (upload.id !== id || upload.status === "error") {
104
+ return upload;
105
+ }
106
+
107
+ const hasTotal: boolean = Boolean(total && total > 0);
108
+ const progressFromEvent: number | null = hasTotal
109
+ ? Math.min(100, Math.round(((loaded || 0) / (total as number)) * 100))
110
+ : null;
111
+ const fallbackProgress: number = Math.min(upload.progress + 5, 95);
112
+
113
+ return {
114
+ ...upload,
115
+ progress:
116
+ progressFromEvent !== null ? progressFromEvent : fallbackProgress,
117
+ };
118
+ });
119
+ });
120
+ };
121
+
122
+ const removeUploadStatus: RemoveUploadStatusFunction = (id: string): void => {
123
+ setUploadStatuses((current: Array<UploadStatus>) => {
124
+ return current.filter((upload: UploadStatus) => {
125
+ return upload.id !== id;
126
+ });
127
+ });
128
+ };
45
129
 
46
130
  useEffect(() => {
47
131
  const _acceptTypes: Dictionary<Array<string>> = {};
@@ -77,194 +161,406 @@ const FilePicker: FunctionComponent<ComponentProps> = (
77
161
  }
78
162
  }, [props.value]);
79
163
 
80
- const { getRootProps, getInputProps } = useDropzone({
164
+ const buildFileSizeError: BuildFileSizeErrorFunction = (
165
+ fileNames: Array<string>,
166
+ ): string => {
167
+ if (fileNames.length === 0) {
168
+ return "";
169
+ }
170
+
171
+ if (fileNames.length === 1) {
172
+ return `"${fileNames[0]}" exceeds the 10MB limit.`;
173
+ }
174
+
175
+ return `These files exceed the 10MB limit: ${fileNames.join(", ")}.`;
176
+ };
177
+
178
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
81
179
  accept: acceptTypes,
82
180
  multiple: props.isMultiFilePicker,
83
181
  noClick: true,
182
+ disabled: props.readOnly || isLoading,
183
+ maxSize: MAX_FILE_SIZE_BYTES,
184
+ onDropRejected: (fileRejections: Array<FileRejection>) => {
185
+ const oversizedFiles: Array<string> = fileRejections
186
+ .filter((rejection: FileRejection) => {
187
+ return rejection.file.size > MAX_FILE_SIZE_BYTES;
188
+ })
189
+ .map((rejection: FileRejection) => {
190
+ return rejection.file.name;
191
+ });
192
+
193
+ if (oversizedFiles.length > 0) {
194
+ setError(buildFileSizeError(oversizedFiles));
195
+ }
196
+ },
84
197
  onDrop: async (acceptedFiles: Array<File>) => {
198
+ if (props.readOnly) {
199
+ return;
200
+ }
201
+
85
202
  setIsLoading(true);
86
- try {
87
- if (props.readOnly) {
88
- return;
89
- }
203
+ setError("");
90
204
 
205
+ try {
91
206
  // Upload these files.
92
207
  const filesResult: Array<FileModel> = [];
208
+ const resolveMimeType: ResolveMimeTypeFunction = (
209
+ file: File,
210
+ ): MimeType | undefined => {
211
+ const direct: string | undefined = file.type || undefined;
212
+ if (direct && Object.values(MimeType).includes(direct as MimeType)) {
213
+ return direct as MimeType;
214
+ }
215
+
216
+ // fallback based on extension
217
+ const ext: string | undefined = file.name
218
+ .split(".")
219
+ .pop()
220
+ ?.toLowerCase();
221
+ if (!ext) {
222
+ return undefined;
223
+ }
224
+ const map: { [key: string]: MimeType } = {
225
+ png: MimeType.png,
226
+ jpg: MimeType.jpg,
227
+ jpeg: MimeType.jpeg,
228
+ svg: MimeType.svg,
229
+ gif: MimeType.gif,
230
+ webp: MimeType.webp,
231
+ pdf: MimeType.pdf,
232
+ doc: MimeType.doc,
233
+ docx: MimeType.docx,
234
+ txt: MimeType.txt,
235
+ log: MimeType.txt,
236
+ md: MimeType.md,
237
+ markdown: MimeType.md,
238
+ csv: MimeType.csv,
239
+ json: MimeType.json,
240
+ zip: MimeType.zip,
241
+ rtf: MimeType.rtf,
242
+ odt: MimeType.odt,
243
+ xls: MimeType.xls,
244
+ xlsx: MimeType.xlsx,
245
+ ods: MimeType.ods,
246
+ ppt: MimeType.ppt,
247
+ pptx: MimeType.pptx,
248
+ odp: MimeType.odp,
249
+ };
250
+ return map[ext];
251
+ };
252
+
253
+ const oversizedFiles: Array<string> = [];
254
+
93
255
  for (const acceptedFile of acceptedFiles) {
94
- const fileModel: FileModel = new FileModel();
95
- fileModel.name = acceptedFile.name;
96
-
97
- const arrayBuffer: ArrayBuffer = await acceptedFile.arrayBuffer();
98
-
99
- const fileBuffer: Uint8Array = new Uint8Array(arrayBuffer);
100
- fileModel.file = Buffer.from(fileBuffer);
101
- fileModel.isPublic = false;
102
- fileModel.fileType = acceptedFile.type as MimeType;
103
-
104
- const result: HTTPResponse<FileModel> =
105
- (await ModelAPI.create<FileModel>({
106
- model: fileModel,
107
- modelType: FileModel,
108
- requestOptions: {
109
- overrideRequestUrl: CommonURL.fromURL(FILE_URL),
110
- },
111
- })) as HTTPResponse<FileModel>;
112
- filesResult.push(result.data as FileModel);
256
+ if (acceptedFile.size > MAX_FILE_SIZE_BYTES) {
257
+ oversizedFiles.push(acceptedFile.name);
258
+ continue;
259
+ }
260
+
261
+ const uploadId: string = `${acceptedFile.name}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
262
+ addUploadStatus({
263
+ id: uploadId,
264
+ name: acceptedFile.name,
265
+ progress: 0,
266
+ status: "uploading",
267
+ });
268
+
269
+ try {
270
+ const fileModel: FileModel = new FileModel();
271
+ fileModel.name = acceptedFile.name;
272
+ const arrayBuffer: ArrayBuffer = await acceptedFile.arrayBuffer();
273
+ const fileBuffer: Uint8Array = new Uint8Array(arrayBuffer);
274
+ fileModel.file = Buffer.from(fileBuffer);
275
+ fileModel.isPublic = false;
276
+ fileModel.fileType = resolveMimeType(acceptedFile) || MimeType.txt; // default to text/plain to satisfy required field
277
+
278
+ const result: HTTPResponse<FileModel> =
279
+ (await ModelAPI.create<FileModel>({
280
+ model: fileModel,
281
+ modelType: FileModel,
282
+ requestOptions: {
283
+ overrideRequestUrl: CommonURL.fromURL(FILE_URL),
284
+ apiRequestOptions: {
285
+ onUploadProgress: (progressEvent: AxiosProgressEvent) => {
286
+ updateUploadProgress(
287
+ uploadId,
288
+ progressEvent.total,
289
+ progressEvent.loaded,
290
+ );
291
+ },
292
+ },
293
+ },
294
+ })) as HTTPResponse<FileModel>;
295
+ filesResult.push(result.data as FileModel);
296
+ removeUploadStatus(uploadId);
297
+ } catch (uploadErr) {
298
+ const friendlyMessage: string = API.getFriendlyMessage(uploadErr);
299
+ updateUploadStatus(uploadId, {
300
+ status: "error",
301
+ errorMessage: friendlyMessage,
302
+ progress: 100,
303
+ });
304
+ setError(friendlyMessage);
305
+ }
306
+ }
307
+
308
+ if (oversizedFiles.length > 0) {
309
+ setError(buildFileSizeError(oversizedFiles));
113
310
  }
114
311
 
115
- setFilesModel(filesResult);
312
+ if (filesResult.length > 0) {
313
+ const updatedFiles: Array<FileModel> = props.isMultiFilePicker
314
+ ? [...filesModel, ...filesResult]
315
+ : filesResult;
116
316
 
117
- props.onBlur?.();
118
- props.onChange?.(filesResult);
317
+ setFilesModel(updatedFiles);
318
+
319
+ props.onBlur?.();
320
+ props.onChange?.(updatedFiles);
321
+ }
119
322
  } catch (err) {
120
323
  setError(API.getFriendlyMessage(err));
324
+ } finally {
325
+ setIsLoading(false);
121
326
  }
122
- setIsLoading(false);
123
327
  },
124
328
  });
125
329
 
126
330
  type GetThumbsFunction = () => Array<ReactElement>;
127
331
 
332
+ const formatFileSize: FormatFileSizeFunction = (
333
+ file: FileModel,
334
+ ): string | null => {
335
+ const buffer: Buffer | undefined = file.file;
336
+ if (!buffer) {
337
+ return null;
338
+ }
339
+
340
+ const sizeInKB: number = buffer.byteLength / 1024;
341
+ if (sizeInKB < 1024) {
342
+ return `${sizeInKB.toFixed(1)} KB`;
343
+ }
344
+
345
+ return `${(sizeInKB / 1024).toFixed(2)} MB`;
346
+ };
347
+
128
348
  const getThumbs: GetThumbsFunction = (): Array<ReactElement> => {
129
349
  return filesModel.map((file: FileModel, i: number) => {
130
- if (!file.file) {
131
- return <></>;
132
- }
350
+ const key: string = file._id?.toString() || `${file.name || "file"}-${i}`;
351
+ const removeFile: VoidFunction = (): void => {
352
+ const tempFileModel: Array<FileModel> = [...filesModel];
353
+ tempFileModel.splice(i, 1);
354
+ setFilesModel(tempFileModel);
355
+ props.onChange?.(tempFileModel);
356
+ };
133
357
 
134
- const blob: Blob = new Blob([file.file!.buffer as ArrayBuffer], {
135
- type: file.fileType as string,
136
- });
137
- const url: string = URL.createObjectURL(blob);
358
+ const metadata: Array<string> = [];
359
+ if (file.fileType) {
360
+ metadata.push(file.fileType);
361
+ }
362
+ const readableSize: string | null = formatFileSize(file);
363
+ if (readableSize) {
364
+ metadata.push(readableSize);
365
+ }
138
366
 
139
367
  return (
140
- <div key={file.name}>
141
- <div className="text-right flex justify-end">
142
- <Icon
143
- icon={IconProp.Close}
144
- className="bg-gray-400 rounded text-white h-7 w-7 align-right items-right p-1 absolute hover:bg-gray-500 cursor-pointer -ml-7"
145
- size={SizeProp.Regular}
146
- onClick={() => {
147
- const tempFileModel: Array<FileModel> = [...filesModel];
148
- tempFileModel.splice(i, 1);
149
- setFilesModel(tempFileModel);
150
- props.onChange?.(tempFileModel);
151
- }}
152
- />
153
- </div>
154
- <div>
155
- <img
156
- src={url}
157
- className="rounded"
158
- style={{
159
- height: "100px",
160
- }}
161
- />
368
+ <div
369
+ key={key}
370
+ className="flex w-full items-center justify-between gap-4 rounded-lg border border-gray-200 bg-white px-3 py-2 shadow-sm"
371
+ >
372
+ <div className="flex items-start gap-3 text-left">
373
+ <div className="flex h-10 w-10 items-center justify-center rounded border border-gray-200 bg-gray-50">
374
+ <Icon icon={IconProp.File} className="text-gray-500" />
375
+ </div>
376
+ <div className="flex flex-col">
377
+ <p className="text-sm font-medium text-gray-900">
378
+ {file.name || `File ${i + 1}`}
379
+ </p>
380
+ {metadata.length > 0 && (
381
+ <p className="text-xs text-gray-500">{metadata.join(" • ")}</p>
382
+ )}
383
+ </div>
162
384
  </div>
385
+ <button
386
+ type="button"
387
+ className="rounded-md border border-gray-200 px-3 py-1 text-xs font-medium text-gray-700 transition hover:bg-gray-50"
388
+ onClick={removeFile}
389
+ >
390
+ Remove
391
+ </button>
163
392
  </div>
164
393
  );
165
394
  });
166
395
  };
167
396
 
168
- if (isLoading) {
169
- return (
170
- <div className="flex justify-center w-full">
171
- <ComponentLoader />
172
- </div>
173
- );
174
- }
397
+ const hasActiveUploads: boolean = uploadStatuses.some(
398
+ (upload: UploadStatus) => {
399
+ return upload.status === "uploading";
400
+ },
401
+ );
175
402
 
176
403
  return (
177
- <div>
404
+ <div className="space-y-4 w-full">
178
405
  <div
179
406
  onClick={() => {
180
407
  props.onClick?.();
181
408
  props.onFocus?.();
182
409
  }}
183
410
  data-testid={props.dataTestId}
184
- className="flex max-w-lg justify-center rounded-md border-2 border-dashed border-gray-300 px-6 pt-5 pb-6"
411
+ className={`flex w-full justify-center rounded-md border-2 border-dashed px-6 py-8 transition ${props.readOnly ? "cursor-not-allowed bg-gray-50 border-gray-200" : "bg-white border-gray-300"} ${hasActiveUploads ? "ring-1 ring-indigo-200" : ""} ${isDragActive ? "border-indigo-400" : ""}`}
185
412
  >
186
- {props.isMultiFilePicker ||
187
- (filesModel.length === 0 && (
188
- <div
189
- {...getRootProps({
190
- className: "space-y-1 text-center",
191
- })}
192
- >
193
- <svg
194
- className="mx-auto h-12 w-12 text-gray-400"
195
- stroke="currentColor"
196
- fill="none"
197
- viewBox="0 0 48 48"
198
- aria-hidden="true"
199
- >
200
- <path
201
- d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
202
- strokeWidth="2"
203
- strokeLinecap="round"
204
- strokeLinejoin="round"
205
- ></path>
206
- </svg>
207
- <div className="flex text-sm text-gray-600">
208
- <label className="relative cursor-pointer rounded-md bg-white font-medium text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2 hover:text-indigo-500">
209
- {!props.placeholder && !error && (
210
- <span>{"Upload a file"}</span>
211
- )}
212
-
213
- {error && (
413
+ <div
414
+ {...getRootProps({
415
+ className:
416
+ "w-full flex flex-col items-center justify-center space-y-3 text-center",
417
+ "aria-busy": hasActiveUploads || isLoading,
418
+ })}
419
+ >
420
+ {(filesModel.length === 0 || props.isMultiFilePicker) && (
421
+ <>
422
+ <div className="flex flex-col items-center space-y-2">
423
+ <svg
424
+ className="mx-auto h-12 w-12 text-gray-400"
425
+ stroke="currentColor"
426
+ fill="none"
427
+ viewBox="0 0 48 48"
428
+ aria-hidden="true"
429
+ >
430
+ <path
431
+ d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
432
+ strokeWidth="2"
433
+ strokeLinecap="round"
434
+ strokeLinejoin="round"
435
+ ></path>
436
+ </svg>
437
+ <div className="flex flex-col items-center text-sm text-gray-600 space-y-1">
438
+ <label className="relative cursor-pointer rounded-md bg-white px-4 py-2 font-medium text-indigo-600 hover:text-indigo-500">
214
439
  <span>
215
- <span>{error}</span>
440
+ {props.placeholder
441
+ ? props.placeholder
442
+ : filesModel.length > 0
443
+ ? "Add more files"
444
+ : "Upload files"}
216
445
  </span>
446
+ <input
447
+ tabIndex={props.tabIndex}
448
+ {...(getInputProps() as any)}
449
+ id="file-upload"
450
+ name="file-upload"
451
+ type="file"
452
+ className="sr-only"
453
+ />
454
+ </label>
455
+ <p className="text-gray-500">
456
+ {isDragActive
457
+ ? "Release to start uploading"
458
+ : filesModel.length === 0
459
+ ? "Click to choose files"
460
+ : "Click to add more"}{" "}
461
+ or drag & drop.
462
+ </p>
463
+ <p className="text-xs text-gray-500">
464
+ {props.mimeTypes && props.mimeTypes?.length > 0 && (
465
+ <span>Types: </span>
466
+ )}
467
+ {props.mimeTypes &&
468
+ props.mimeTypes
469
+ .map((type: MimeType) => {
470
+ const enumKey: string | undefined =
471
+ Object.keys(MimeType)[
472
+ Object.values(MimeType).indexOf(type)
473
+ ];
474
+ return enumKey?.toUpperCase() || "";
475
+ })
476
+ .filter(
477
+ (
478
+ item: string | undefined,
479
+ pos: number,
480
+ array: Array<string | undefined>,
481
+ ) => {
482
+ return array.indexOf(item) === pos;
483
+ },
484
+ )
485
+ .join(", ")}
486
+ {props.mimeTypes && props.mimeTypes?.length > 0 && (
487
+ <span>.</span>
488
+ )}{" "}
489
+ Max 10MB each.
490
+ </p>
491
+ {error && (
492
+ <p className="text-xs text-red-500 font-medium">{error}</p>
217
493
  )}
218
-
219
- {props.placeholder && !error && (
220
- <span>{props.placeholder}</span>
221
- )}
222
-
223
- <input
224
- tabIndex={props.tabIndex}
225
- {...(getInputProps() as any)}
226
- id="file-upload"
227
- name="file-upload"
228
- type="file"
229
- className="sr-only"
230
- />
231
- </label>
232
- <p className="pl-1">or drag and drop</p>
494
+ </div>
233
495
  </div>
234
- <p className="text-xs text-gray-500">
235
- {props.mimeTypes && props.mimeTypes?.length > 0 && (
236
- <span>File types: </span>
237
- )}
238
- {props.mimeTypes &&
239
- props.mimeTypes
240
- .map((type: MimeType) => {
241
- const enumKey: string | undefined =
242
- Object.keys(MimeType)[
243
- Object.values(MimeType).indexOf(type)
244
- ];
245
- return enumKey?.toUpperCase() || "";
246
- })
247
- .filter(
248
- (
249
- item: string | undefined,
250
- pos: number,
251
- array: Array<string | undefined>,
252
- ) => {
253
- return array.indexOf(item) === pos;
254
- },
255
- )
256
- .join(", ")}
257
- {props.mimeTypes && props.mimeTypes?.length > 0 && (
258
- <span>.</span>
259
- )}
260
- &nbsp;10 MB or less.
261
- </p>
262
- </div>
263
- ))}
264
- <aside>{getThumbs()}</aside>
496
+ </>
497
+ )}
498
+ </div>
265
499
  </div>
500
+ {uploadStatuses.length > 0 && (
501
+ <div className="space-y-2 w-full">
502
+ <p className="text-sm font-medium text-gray-700 text-left">
503
+ {hasActiveUploads ? "Uploading files" : "Upload status"}
504
+ </p>
505
+ <div className="space-y-2">
506
+ {uploadStatuses.map((upload: UploadStatus) => {
507
+ return (
508
+ <div
509
+ key={upload.id}
510
+ className={`rounded border px-3 py-2 ${upload.status === "error" ? "border-red-200 bg-red-50" : "border-gray-200 bg-white"}`}
511
+ >
512
+ <div className="flex items-center justify-between text-sm">
513
+ <p className="font-medium text-gray-800 truncate">
514
+ {upload.name}
515
+ </p>
516
+ <span
517
+ className={`text-xs ${upload.status === "error" ? "text-red-600" : "text-gray-500"}`}
518
+ >
519
+ {upload.status === "error"
520
+ ? "Failed"
521
+ : `${upload.progress}%`}
522
+ </span>
523
+ </div>
524
+ <div className="mt-2 h-2 rounded bg-gray-200 overflow-hidden">
525
+ <div
526
+ className={`h-full transition-all duration-300 ${upload.status === "error" ? "bg-red-400" : "bg-indigo-500"}`}
527
+ style={{ width: `${Math.min(upload.progress, 100)}%` }}
528
+ ></div>
529
+ </div>
530
+ {upload.status === "error" && upload.errorMessage && (
531
+ <p className="mt-2 text-xs text-red-600 text-left">
532
+ {upload.errorMessage}
533
+ </p>
534
+ )}
535
+ {upload.status === "error" && (
536
+ <div className="mt-2 text-right">
537
+ <button
538
+ type="button"
539
+ className="text-xs font-medium text-gray-600 hover:text-gray-800"
540
+ onClick={() => {
541
+ removeUploadStatus(upload.id);
542
+ }}
543
+ >
544
+ Dismiss
545
+ </button>
546
+ </div>
547
+ )}
548
+ </div>
549
+ );
550
+ })}
551
+ </div>
552
+ </div>
553
+ )}
554
+ {filesModel.length > 0 && (
555
+ <div className="space-y-2 w-full">
556
+ <p className="text-sm font-medium text-gray-700 text-left">
557
+ Uploaded files
558
+ </p>
559
+ <div className="flex flex-wrap gap-4">{getThumbs()}</div>
560
+ </div>
561
+ )}
266
562
  {props.error && (
267
- <p data-testid="error-message" className="mt-1 text-sm text-red-400">
563
+ <p data-testid="error-message" className="text-sm text-red-400">
268
564
  {props.error}
269
565
  </p>
270
566
  )}