@oneuptime/common 11.0.0 → 11.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Tests/Utils/ModelImportExport.test.ts +366 -0
- package/UI/Components/ImportExport/ExportModelCard.tsx +90 -0
- package/UI/Components/ImportExport/ImportModelsModal.tsx +239 -0
- package/UI/Components/ModelTable/ModelTable.tsx +294 -143
- package/UI/Utils/ModelImportExport.ts +207 -0
- package/Utils/ModelImportExport.ts +369 -0
- package/build/dist/UI/Components/ImportExport/ExportModelCard.js +50 -0
- package/build/dist/UI/Components/ImportExport/ExportModelCard.js.map +1 -0
- package/build/dist/UI/Components/ImportExport/ImportModelsModal.js +115 -0
- package/build/dist/UI/Components/ImportExport/ImportModelsModal.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/ModelTable.js +166 -74
- package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
- package/build/dist/UI/Utils/ModelImportExport.js +142 -0
- package/build/dist/UI/Utils/ModelImportExport.js.map +1 -0
- package/build/dist/Utils/ModelImportExport.js +257 -0
- package/build/dist/Utils/ModelImportExport.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import BaseModel, {
|
|
2
|
+
DatabaseBaseModelType,
|
|
3
|
+
} from "../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
4
|
+
import { ColumnAccessControl } from "../Types/BaseDatabase/AccessControl";
|
|
5
|
+
import Select from "../Types/BaseDatabase/Select";
|
|
6
|
+
import { TableColumnMetadata } from "../Types/Database/TableColumn";
|
|
7
|
+
import TableColumnType from "../Types/Database/TableColumnType";
|
|
8
|
+
import OneUptimeDate from "../Types/Date";
|
|
9
|
+
import Dictionary from "../Types/Dictionary";
|
|
10
|
+
import Recurring from "../Types/Events/Recurring";
|
|
11
|
+
import BadDataException from "../Types/Exception/BadDataException";
|
|
12
|
+
import { JSONArray, JSONObject } from "../Types/JSON";
|
|
13
|
+
|
|
14
|
+
export const MODEL_EXPORT_FILE_TYPE: string = "oneuptime-resource-export";
|
|
15
|
+
export const MODEL_EXPORT_SCHEMA_VERSION: number = 1;
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* Columns that are always server-managed and must never be exported or
|
|
19
|
+
* imported, regardless of model metadata.
|
|
20
|
+
*/
|
|
21
|
+
const SYSTEM_COLUMNS: Array<string> = [
|
|
22
|
+
"_id",
|
|
23
|
+
"createdAt",
|
|
24
|
+
"updatedAt",
|
|
25
|
+
"deletedAt",
|
|
26
|
+
"version",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
* Column types that never make sense in an export file: relations to other
|
|
31
|
+
* entities (their ids would not resolve in another project or instance),
|
|
32
|
+
* binary data, and secrets.
|
|
33
|
+
*/
|
|
34
|
+
const EXCLUDED_COLUMN_TYPES: Array<TableColumnType> = [
|
|
35
|
+
TableColumnType.Entity,
|
|
36
|
+
TableColumnType.EntityArray,
|
|
37
|
+
TableColumnType.File,
|
|
38
|
+
TableColumnType.Buffer,
|
|
39
|
+
TableColumnType.Slug,
|
|
40
|
+
TableColumnType.HashedString,
|
|
41
|
+
TableColumnType.Password,
|
|
42
|
+
TableColumnType.OTP,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/*
|
|
46
|
+
* Columns that pass the generic metadata filters but must still never be
|
|
47
|
+
* exported: plain-text credentials and server-managed runtime state that
|
|
48
|
+
* would either leak secrets into export files or seed imported resources
|
|
49
|
+
* with stale state from the source resource.
|
|
50
|
+
*/
|
|
51
|
+
const EXCLUDED_COLUMNS_BY_TABLE: Dictionary<Array<string>> = {
|
|
52
|
+
StatusPage: ["embeddedOverallStatusToken"],
|
|
53
|
+
Monitor: [
|
|
54
|
+
"incomingMonitorRequest",
|
|
55
|
+
"serverMonitorResponse",
|
|
56
|
+
"incomingRequestMonitorHeartbeatCheckedAt",
|
|
57
|
+
"serverMonitorRequestReceivedAt",
|
|
58
|
+
"telemetryMonitorNextMonitorAt",
|
|
59
|
+
"telemetryMonitorLastMonitorAt",
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default class ModelImportExport {
|
|
64
|
+
/*
|
|
65
|
+
* Derives the list of columns that can safely round-trip through an export
|
|
66
|
+
* file from the model's own metadata. A column qualifies when the current
|
|
67
|
+
* caller could both read it and set it on create, it is not computed or
|
|
68
|
+
* server-generated, it holds no secret, and it does not reference another
|
|
69
|
+
* entity (foreign keys of Entity relations are excluded along with the
|
|
70
|
+
* relations themselves).
|
|
71
|
+
*/
|
|
72
|
+
public static getImportExportableColumnNames(
|
|
73
|
+
modelType: DatabaseBaseModelType,
|
|
74
|
+
): Array<string> {
|
|
75
|
+
const model: BaseModel = new modelType();
|
|
76
|
+
|
|
77
|
+
const tenantColumn: string | null = model.getTenantColumn();
|
|
78
|
+
|
|
79
|
+
const foreignKeyColumns: Array<string> = [];
|
|
80
|
+
|
|
81
|
+
for (const columnName of model.getTableColumns().columns) {
|
|
82
|
+
const metadata: TableColumnMetadata | undefined =
|
|
83
|
+
model.getTableColumnMetadata(columnName);
|
|
84
|
+
|
|
85
|
+
if (metadata?.manyToOneRelationColumn) {
|
|
86
|
+
foreignKeyColumns.push(metadata.manyToOneRelationColumn);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const excludedColumnsForTable: Array<string> =
|
|
91
|
+
EXCLUDED_COLUMNS_BY_TABLE[model.tableName || ""] || [];
|
|
92
|
+
|
|
93
|
+
const exportableColumns: Array<string> = [];
|
|
94
|
+
|
|
95
|
+
for (const columnName of model.getTableColumns().columns) {
|
|
96
|
+
if (SYSTEM_COLUMNS.includes(columnName)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (excludedColumnsForTable.includes(columnName)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (tenantColumn && columnName === tenantColumn) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (foreignKeyColumns.includes(columnName)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const metadata: TableColumnMetadata | undefined =
|
|
113
|
+
model.getTableColumnMetadata(columnName);
|
|
114
|
+
|
|
115
|
+
if (!metadata) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (EXCLUDED_COLUMN_TYPES.includes(metadata.type)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (metadata.computed || metadata.hashed || metadata.encrypted) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (metadata.forceGetDefaultValueOnCreate) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const accessControl: ColumnAccessControl | null =
|
|
132
|
+
model.getColumnAccessControlFor(columnName);
|
|
133
|
+
|
|
134
|
+
if (
|
|
135
|
+
!accessControl ||
|
|
136
|
+
accessControl.read.length === 0 ||
|
|
137
|
+
accessControl.create.length === 0
|
|
138
|
+
) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
exportableColumns.push(columnName);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return exportableColumns;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public static getImportExportSelect<TBaseModel extends BaseModel>(modelType: {
|
|
149
|
+
new (): TBaseModel;
|
|
150
|
+
}): Select<TBaseModel> {
|
|
151
|
+
const select: JSONObject = {};
|
|
152
|
+
|
|
153
|
+
for (const columnName of this.getImportExportableColumnNames(modelType)) {
|
|
154
|
+
select[columnName] = true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return select as Select<TBaseModel>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public static toExportJSON<TBaseModel extends BaseModel>(
|
|
161
|
+
item: TBaseModel,
|
|
162
|
+
modelType: { new (): TBaseModel },
|
|
163
|
+
): JSONObject {
|
|
164
|
+
const json: JSONObject = BaseModel.toJSON(item, modelType);
|
|
165
|
+
|
|
166
|
+
const exportableColumns: Array<string> =
|
|
167
|
+
this.getImportExportableColumnNames(modelType);
|
|
168
|
+
|
|
169
|
+
const exportJson: JSONObject = {};
|
|
170
|
+
|
|
171
|
+
for (const key of Object.keys(json)) {
|
|
172
|
+
if (exportableColumns.includes(key)) {
|
|
173
|
+
exportJson[key] = json[key];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return exportJson;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public static buildExportEnvelope<TBaseModel extends BaseModel>(data: {
|
|
181
|
+
modelType: { new (): TBaseModel };
|
|
182
|
+
items: Array<TBaseModel>;
|
|
183
|
+
exportedAt: Date;
|
|
184
|
+
}): JSONObject {
|
|
185
|
+
const model: BaseModel = new data.modelType();
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
fileType: MODEL_EXPORT_FILE_TYPE,
|
|
189
|
+
schemaVersion: MODEL_EXPORT_SCHEMA_VERSION,
|
|
190
|
+
resourceType: model.tableName || "",
|
|
191
|
+
exportedAt: data.exportedAt.toISOString(),
|
|
192
|
+
items: data.items.map((item: TBaseModel) => {
|
|
193
|
+
return this.toExportJSON(item, data.modelType);
|
|
194
|
+
}),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/*
|
|
199
|
+
* Accepts an export envelope, a plain array of items, or a single item
|
|
200
|
+
* object, and returns the list of item JSONs to import. Throws
|
|
201
|
+
* BadDataException with a user-facing message when the payload is not
|
|
202
|
+
* usable for this model type.
|
|
203
|
+
*/
|
|
204
|
+
public static parseImportPayload(data: {
|
|
205
|
+
modelType: DatabaseBaseModelType;
|
|
206
|
+
payload: JSONObject | JSONArray;
|
|
207
|
+
}): Array<JSONObject> {
|
|
208
|
+
const model: BaseModel = new data.modelType();
|
|
209
|
+
const resourceName: string = model.singularName || "resource";
|
|
210
|
+
|
|
211
|
+
let items: JSONArray | null = null;
|
|
212
|
+
|
|
213
|
+
if (Array.isArray(data.payload)) {
|
|
214
|
+
items = data.payload;
|
|
215
|
+
} else if (data.payload && typeof data.payload === "object") {
|
|
216
|
+
if (Array.isArray(data.payload["items"])) {
|
|
217
|
+
const resourceType: unknown = data.payload["resourceType"];
|
|
218
|
+
|
|
219
|
+
if (
|
|
220
|
+
resourceType &&
|
|
221
|
+
model.tableName &&
|
|
222
|
+
resourceType !== model.tableName
|
|
223
|
+
) {
|
|
224
|
+
throw new BadDataException(
|
|
225
|
+
`This file contains ${resourceType.toString()} resources and cannot be imported here. Please select a ${resourceName} export file.`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
items = data.payload["items"] as JSONArray;
|
|
230
|
+
} else {
|
|
231
|
+
// a single item object exported by hand.
|
|
232
|
+
items = [data.payload];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!items) {
|
|
237
|
+
throw new BadDataException(
|
|
238
|
+
`This file is not a valid ${resourceName} export file.`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (items.length === 0) {
|
|
243
|
+
throw new BadDataException(
|
|
244
|
+
`This file does not contain any ${resourceName} to import.`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (const item of items) {
|
|
249
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
250
|
+
throw new BadDataException(
|
|
251
|
+
`This file is not a valid ${resourceName} export file.`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return items as Array<JSONObject>;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/*
|
|
260
|
+
* Builds a model instance from an import item, keeping only columns that
|
|
261
|
+
* are importable for this model. Anything else in the file - ids, slugs,
|
|
262
|
+
* timestamps, relations, unknown keys - is dropped so the server create
|
|
263
|
+
* API treats the item as a brand new resource.
|
|
264
|
+
*/
|
|
265
|
+
public static fromImportJSON<TBaseModel extends BaseModel>(data: {
|
|
266
|
+
json: JSONObject;
|
|
267
|
+
modelType: { new (): TBaseModel };
|
|
268
|
+
}): TBaseModel {
|
|
269
|
+
const item: TBaseModel = BaseModel.fromJSON(
|
|
270
|
+
data.json,
|
|
271
|
+
data.modelType,
|
|
272
|
+
) as TBaseModel;
|
|
273
|
+
|
|
274
|
+
const exportableColumns: Array<string> =
|
|
275
|
+
this.getImportExportableColumnNames(data.modelType);
|
|
276
|
+
|
|
277
|
+
for (const columnName of item.getTableColumns().columns) {
|
|
278
|
+
if (exportableColumns.includes(columnName)) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if ((item as any)[columnName] !== undefined) {
|
|
283
|
+
item.removeValue(columnName);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this.normalizeImportItem(item);
|
|
288
|
+
|
|
289
|
+
return item;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/*
|
|
293
|
+
* Model-specific fixups so imported items pass server-side create
|
|
294
|
+
* validation. Currently: recurring scheduled maintenance templates are
|
|
295
|
+
* rejected on create when their first-event dates are in the past (the
|
|
296
|
+
* normal case for an export taken from an established template), so the
|
|
297
|
+
* dates are advanced by the recurrence interval until they are in the
|
|
298
|
+
* future, preserving the offsets between them.
|
|
299
|
+
*/
|
|
300
|
+
private static normalizeImportItem(item: BaseModel): void {
|
|
301
|
+
if (item.tableName !== "ScheduledMaintenanceTemplate") {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const isRecurringEvent: unknown = item.getValue("isRecurringEvent");
|
|
306
|
+
const recurringInterval: unknown = item.getValue("recurringInterval");
|
|
307
|
+
|
|
308
|
+
if (!isRecurringEvent || !recurringInterval) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const dateColumns: Array<string> = [
|
|
313
|
+
"firstEventScheduledAt",
|
|
314
|
+
"firstEventStartsAt",
|
|
315
|
+
"firstEventEndsAt",
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
const dates: Array<Date> = [];
|
|
319
|
+
|
|
320
|
+
for (const dateColumn of dateColumns) {
|
|
321
|
+
const value: unknown = item.getValue(dateColumn);
|
|
322
|
+
|
|
323
|
+
if (value instanceof Date) {
|
|
324
|
+
dates.push(value);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (dates.length === 0) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/*
|
|
333
|
+
* Anchor the advance on the earliest date so every date ends up in the
|
|
334
|
+
* future once shifted by the same amount.
|
|
335
|
+
*/
|
|
336
|
+
const earliestDate: Date = dates.reduce((a: Date, b: Date) => {
|
|
337
|
+
return a.getTime() <= b.getTime() ? a : b;
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
if (OneUptimeDate.isInTheFuture(earliestDate)) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const interval: Recurring =
|
|
345
|
+
recurringInterval instanceof Recurring
|
|
346
|
+
? recurringInterval
|
|
347
|
+
: Recurring.fromJSON(recurringInterval as JSONObject);
|
|
348
|
+
|
|
349
|
+
const nextEarliestDate: Date = Recurring.getNextDate(
|
|
350
|
+
earliestDate,
|
|
351
|
+
interval,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const advanceByMs: number =
|
|
355
|
+
nextEarliestDate.getTime() - earliestDate.getTime();
|
|
356
|
+
|
|
357
|
+
if (advanceByMs <= 0) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (const dateColumn of dateColumns) {
|
|
362
|
+
const value: unknown = item.getValue(dateColumn);
|
|
363
|
+
|
|
364
|
+
if (value instanceof Date) {
|
|
365
|
+
(item as any)[dateColumn] = new Date(value.getTime() + advanceByMs);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
2
|
+
import API from "../../Utils/API/API";
|
|
3
|
+
import ModelImportExportUtil from "../../Utils/ModelImportExport";
|
|
4
|
+
import { ButtonStyleType } from "../Button/Button";
|
|
5
|
+
import Card from "../Card/Card";
|
|
6
|
+
import ConfirmModal from "../Modal/ConfirmModal";
|
|
7
|
+
import React, { useState } from "react";
|
|
8
|
+
const ExportModelCard = (props) => {
|
|
9
|
+
const model = new props.modelType();
|
|
10
|
+
const singularName = model.singularName || "Resource";
|
|
11
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
12
|
+
const [error, setError] = useState("");
|
|
13
|
+
const exportItem = async () => {
|
|
14
|
+
setIsLoading(true);
|
|
15
|
+
try {
|
|
16
|
+
const item = await ModelImportExportUtil.fetchItemForExport({
|
|
17
|
+
modelType: props.modelType,
|
|
18
|
+
modelId: props.modelId,
|
|
19
|
+
modelAPI: props.modelAPI,
|
|
20
|
+
});
|
|
21
|
+
ModelImportExportUtil.downloadExportFile({
|
|
22
|
+
modelType: props.modelType,
|
|
23
|
+
items: [item],
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
setError(API.getFriendlyMessage(err));
|
|
28
|
+
}
|
|
29
|
+
setIsLoading(false);
|
|
30
|
+
};
|
|
31
|
+
return (React.createElement(React.Fragment, null,
|
|
32
|
+
React.createElement(Card, { title: `Export ${singularName} as JSON`, description: `Download this ${singularName.toLowerCase()} as a JSON file. You can import it later to re-create this ${singularName.toLowerCase()}. Only this ${singularName.toLowerCase()}'s own settings are included - related resources (like owners, labels, or other linked resources) are not exported, and references to resources from this project may need to be re-selected after importing into another project.`, buttons: [
|
|
33
|
+
{
|
|
34
|
+
title: `Export ${singularName}`,
|
|
35
|
+
buttonStyle: ButtonStyleType.NORMAL,
|
|
36
|
+
onClick: () => {
|
|
37
|
+
exportItem().catch((err) => {
|
|
38
|
+
setError(API.getFriendlyMessage(err));
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
isLoading: isLoading,
|
|
42
|
+
icon: IconProp.Download,
|
|
43
|
+
},
|
|
44
|
+
] }),
|
|
45
|
+
error ? (React.createElement(ConfirmModal, { description: error, title: `Export Error`, onSubmit: () => {
|
|
46
|
+
setError("");
|
|
47
|
+
}, submitButtonText: `Close`, submitButtonType: ButtonStyleType.NORMAL })) : (React.createElement(React.Fragment, null))));
|
|
48
|
+
};
|
|
49
|
+
export default ExportModelCard;
|
|
50
|
+
//# sourceMappingURL=ExportModelCard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExportModelCard.js","sourceRoot":"","sources":["../../../../../UI/Components/ImportExport/ExportModelCard.tsx"],"names":[],"mappings":"AACA,OAAO,QAAQ,MAAM,8BAA8B,CAAC;AAEpD,OAAO,GAAG,MAAM,qBAAqB,CAAC;AAEtC,OAAO,qBAAqB,MAAM,+BAA+B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,IAAI,MAAM,cAAc,CAAC;AAChC,OAAO,YAAY,MAAM,uBAAuB,CAAC;AACjD,OAAO,KAAK,EAAE,EAAgB,QAAQ,EAAE,MAAM,OAAO,CAAC;AAQtD,MAAM,eAAe,GAED,CAClB,KAAiC,EACnB,EAAE;IAChB,MAAM,KAAK,GAAe,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;IAChD,MAAM,YAAY,GAAW,KAAK,CAAC,YAAY,IAAI,UAAU,CAAC;IAE9D,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAU,KAAK,CAAC,CAAC;IAC3D,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAS,EAAE,CAAC,CAAC;IAI/C,MAAM,UAAU,GAAuB,KAAK,IAAmB,EAAE;QAC/D,YAAY,CAAC,IAAI,CAAC,CAAC;QAEnB,IAAI,CAAC;YACH,MAAM,IAAI,GACR,MAAM,qBAAqB,CAAC,kBAAkB,CAAa;gBACzD,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;aACzB,CAAC,CAAC;YAEL,qBAAqB,CAAC,kBAAkB,CAAC;gBACvC,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,KAAK,EAAE,CAAC,IAAI,CAAC;aACd,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,QAAQ,CAAC,GAAG,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC;QACxC,CAAC;QAED,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC,CAAC;IAEF,OAAO,CACL;QACE,oBAAC,IAAI,IACH,KAAK,EAAE,UAAU,YAAY,UAAU,EACvC,WAAW,EAAE,iBAAiB,YAAY,CAAC,WAAW,EAAE,8DAA8D,YAAY,CAAC,WAAW,EAAE,eAAe,YAAY,CAAC,WAAW,EAAE,oOAAoO,EAC7Z,OAAO,EAAE;gBACP;oBACE,KAAK,EAAE,UAAU,YAAY,EAAE;oBAC/B,WAAW,EAAE,eAAe,CAAC,MAAM;oBACnC,OAAO,EAAE,GAAG,EAAE;wBACZ,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,GAAU,EAAE,EAAE;4BAChC,QAAQ,CAAC,GAAG,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC;wBACxC,CAAC,CAAC,CAAC;oBACL,CAAC;oBACD,SAAS,EAAE,SAAS;oBACpB,IAAI,EAAE,QAAQ,CAAC,QAAQ;iBACxB;aACF,GACD;QAED,KAAK,CAAC,CAAC,CAAC,CACP,oBAAC,YAAY,IACX,WAAW,EAAE,KAAK,EAClB,KAAK,EAAE,cAAc,EACrB,QAAQ,EAAE,GAAG,EAAE;gBACb,QAAQ,CAAC,EAAE,CAAC,CAAC;YACf,CAAC,EACD,gBAAgB,EAAE,OAAO,EACzB,gBAAgB,EAAE,eAAe,CAAC,MAAM,GACxC,CACH,CAAC,CAAC,CAAC,CACF,yCAAK,CACN,CACA,CACJ,CAAC;AACJ,CAAC,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import CodeType from "../../../Types/Code/CodeType";
|
|
2
|
+
import API from "../../Utils/API/API";
|
|
3
|
+
import ModelImportExportUtil from "../../Utils/ModelImportExport";
|
|
4
|
+
import Alert, { AlertType } from "../Alerts/Alert";
|
|
5
|
+
import { ButtonStyleType } from "../Button/Button";
|
|
6
|
+
import CodeEditor from "../CodeEditor/CodeEditor";
|
|
7
|
+
import Modal, { ModalWidth } from "../Modal/Modal";
|
|
8
|
+
import ProgressBar from "../ProgressBar/ProgressBar";
|
|
9
|
+
import React, { useState } from "react";
|
|
10
|
+
var ImportPhase;
|
|
11
|
+
(function (ImportPhase) {
|
|
12
|
+
ImportPhase["Edit"] = "Edit";
|
|
13
|
+
ImportPhase["Importing"] = "Importing";
|
|
14
|
+
ImportPhase["Done"] = "Done";
|
|
15
|
+
})(ImportPhase || (ImportPhase = {}));
|
|
16
|
+
const ImportModelsModal = (props) => {
|
|
17
|
+
const model = new props.modelType();
|
|
18
|
+
const singularName = model.singularName || "Resource";
|
|
19
|
+
const pluralName = model.pluralName || "Resources";
|
|
20
|
+
const [phase, setPhase] = useState(ImportPhase.Edit);
|
|
21
|
+
const [fileText, setFileText] = useState("");
|
|
22
|
+
const [error, setError] = useState("");
|
|
23
|
+
const [completedCount, setCompletedCount] = useState(0);
|
|
24
|
+
const [totalCount, setTotalCount] = useState(0);
|
|
25
|
+
const [result, setResult] = useState(null);
|
|
26
|
+
const onFileSelected = (event) => {
|
|
27
|
+
var _a;
|
|
28
|
+
const file = (_a = event.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
29
|
+
if (!file) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const reader = new FileReader();
|
|
33
|
+
reader.onload = () => {
|
|
34
|
+
setFileText(reader.result || "");
|
|
35
|
+
setError("");
|
|
36
|
+
};
|
|
37
|
+
reader.onerror = () => {
|
|
38
|
+
setError("Could not read the selected file. Please try again.");
|
|
39
|
+
};
|
|
40
|
+
reader.readAsText(file);
|
|
41
|
+
// allow re-selecting the same file after editing.
|
|
42
|
+
event.target.value = "";
|
|
43
|
+
};
|
|
44
|
+
const startImport = async () => {
|
|
45
|
+
setError("");
|
|
46
|
+
let itemJsons = [];
|
|
47
|
+
try {
|
|
48
|
+
itemJsons = ModelImportExportUtil.parseImportFileText({
|
|
49
|
+
modelType: props.modelType,
|
|
50
|
+
fileText: fileText,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
setError(API.getFriendlyMessage(err));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
setPhase(ImportPhase.Importing);
|
|
58
|
+
setCompletedCount(0);
|
|
59
|
+
setTotalCount(itemJsons.length);
|
|
60
|
+
try {
|
|
61
|
+
const importResult = await ModelImportExportUtil.importItems({
|
|
62
|
+
modelType: props.modelType,
|
|
63
|
+
itemJsons: itemJsons,
|
|
64
|
+
modelAPI: props.modelAPI,
|
|
65
|
+
onProgress: (completed, total) => {
|
|
66
|
+
setCompletedCount(completed);
|
|
67
|
+
setTotalCount(total);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
setResult(importResult);
|
|
71
|
+
setPhase(ImportPhase.Done);
|
|
72
|
+
props.onImportComplete(importResult);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
// never trap the user in the importing phase - return to the editor.
|
|
76
|
+
setError(API.getFriendlyMessage(err));
|
|
77
|
+
setPhase(ImportPhase.Edit);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
if (phase === ImportPhase.Importing) {
|
|
81
|
+
return (React.createElement(Modal, { title: `Importing ${pluralName}`, description: `Please wait while your ${pluralName.toLowerCase()} are being imported.`, isBodyLoading: false, onSubmit: () => { }, disableSubmitButton: true, submitButtonText: "Importing..." },
|
|
82
|
+
React.createElement("div", { className: "mt-5 mb-5" },
|
|
83
|
+
React.createElement(ProgressBar, { count: completedCount, totalCount: totalCount, suffix: pluralName }))));
|
|
84
|
+
}
|
|
85
|
+
if (phase === ImportPhase.Done && result) {
|
|
86
|
+
return (React.createElement(Modal, { title: `Import Complete`, onSubmit: () => {
|
|
87
|
+
props.onClose();
|
|
88
|
+
}, submitButtonText: "Close", submitButtonStyleType: ButtonStyleType.NORMAL },
|
|
89
|
+
React.createElement("div", { className: "mt-5 mb-5 space-y-3" },
|
|
90
|
+
result.successCount > 0 ? (React.createElement(Alert, { type: AlertType.SUCCESS, strongTitle: `${result.successCount} ${result.successCount === 1 ? singularName : pluralName} imported successfully.` })) : (React.createElement(React.Fragment, null)),
|
|
91
|
+
result.failures.length > 0 ? (React.createElement("div", null,
|
|
92
|
+
React.createElement(Alert, { type: AlertType.DANGER, strongTitle: `${result.failures.length} ${result.failures.length === 1 ? singularName : pluralName} could not be imported.` }),
|
|
93
|
+
React.createElement("ul", { className: "mt-3 list-disc pl-5 text-sm text-gray-600" }, result.failures.map((failure, index) => {
|
|
94
|
+
return (React.createElement("li", { key: index, className: "mt-1" },
|
|
95
|
+
React.createElement("span", { className: "font-medium" }, failure.itemName),
|
|
96
|
+
":",
|
|
97
|
+
" ",
|
|
98
|
+
failure.errorMessage));
|
|
99
|
+
})))) : (React.createElement(React.Fragment, null)))));
|
|
100
|
+
}
|
|
101
|
+
return (React.createElement(Modal, { title: `Import ${pluralName}`, description: `Upload a ${singularName.toLowerCase()} JSON export file, or paste its contents below. New ${pluralName.toLowerCase()} will be created in this project. Related resources (like owners, labels, or other linked resources) are not part of export files, and references to resources from another project may need to be re-selected after import.`, modalWidth: ModalWidth.Large, onClose: props.onClose, onSubmit: async () => {
|
|
102
|
+
await startImport();
|
|
103
|
+
}, disableSubmitButton: !fileText.trim(), submitButtonText: `Import`, error: error || undefined },
|
|
104
|
+
React.createElement("div", { className: "mt-5 mb-5" },
|
|
105
|
+
React.createElement("label", { htmlFor: "import-file-input", className: "block text-sm font-medium text-gray-700" }, "Select export file"),
|
|
106
|
+
React.createElement("input", { id: "import-file-input", "data-testid": "import-file-input", type: "file", accept: ".json,application/json", onChange: onFileSelected, className: "mt-2 block w-full text-sm text-gray-600 file:mr-4 file:rounded-md file:border-0 file:bg-indigo-50 file:py-2 file:px-4 file:text-sm file:font-semibold file:text-indigo-700 hover:file:bg-indigo-100" }),
|
|
107
|
+
React.createElement("div", { className: "mt-4" },
|
|
108
|
+
React.createElement("label", { className: "block text-sm font-medium text-gray-700 mb-2" }, "Or paste the export JSON"),
|
|
109
|
+
React.createElement(CodeEditor, { type: CodeType.JSON, value: fileText, onChange: (value) => {
|
|
110
|
+
setFileText(value);
|
|
111
|
+
setError("");
|
|
112
|
+
}, placeholder: `Paste your ${singularName.toLowerCase()} export JSON here.` })))));
|
|
113
|
+
};
|
|
114
|
+
export default ImportModelsModal;
|
|
115
|
+
//# sourceMappingURL=ImportModelsModal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImportModelsModal.js","sourceRoot":"","sources":["../../../../../UI/Components/ImportExport/ImportModelsModal.tsx"],"names":[],"mappings":"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"}
|