@solidxai/core-ui 0.1.12-beta.0 → 0.1.12-beta.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/dist/components/auth/SolidLogin.d.ts.map +1 -1
- package/dist/components/auth/SolidLogin.js +6 -3
- package/dist/components/auth/SolidLogin.js.map +1 -1
- package/dist/components/auth/SolidLogin.tsx +16 -2
- package/dist/components/auth/SolidRegister.d.ts.map +1 -1
- package/dist/components/auth/SolidRegister.js +7 -4
- package/dist/components/auth/SolidRegister.js.map +1 -1
- package/dist/components/auth/SolidRegister.tsx +16 -2
- package/dist/components/common/SolidLoadingState.d.ts +10 -0
- package/dist/components/common/SolidLoadingState.d.ts.map +1 -0
- package/dist/components/common/SolidLoadingState.js +8 -0
- package/dist/components/common/SolidLoadingState.js.map +1 -0
- package/dist/components/common/SolidLoadingState.module.css +56 -0
- package/dist/components/common/SolidLoadingState.tsx +28 -0
- package/dist/components/core/json/SolidJsonEditor.css +195 -0
- package/dist/components/core/json/SolidJsonEditor.d.ts +19 -0
- package/dist/components/core/json/SolidJsonEditor.d.ts.map +1 -0
- package/dist/components/core/json/SolidJsonEditor.js +109 -0
- package/dist/components/core/json/SolidJsonEditor.js.map +1 -0
- package/dist/components/core/json/SolidJsonEditor.tsx +150 -0
- package/dist/components/core/module/CreateModule.css +70 -2
- package/dist/components/core/module/CreateModule.d.ts.map +1 -1
- package/dist/components/core/module/CreateModule.js +2 -2
- package/dist/components/core/module/CreateModule.js.map +1 -1
- package/dist/components/core/module/CreateModule.tsx +13 -1
- package/dist/components/shad-cn-ui/SolidTabs.d.ts.map +1 -1
- package/dist/components/shad-cn-ui/SolidTabs.js +1 -1
- package/dist/components/shad-cn-ui/SolidTabs.js.map +1 -1
- package/dist/components/shad-cn-ui/SolidTabs.tsx +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/index.ts +14 -0
- package/dist/redux/api/datasourceIntrospectionApi.d.ts +203 -0
- package/dist/redux/api/datasourceIntrospectionApi.d.ts.map +1 -0
- package/dist/redux/api/datasourceIntrospectionApi.js +71 -0
- package/dist/redux/api/datasourceIntrospectionApi.js.map +1 -0
- package/dist/redux/api/datasourceIntrospectionApi.ts +261 -0
- package/dist/redux/api/datasourceManagementApi.d.ts +46 -0
- package/dist/redux/api/datasourceManagementApi.d.ts.map +1 -0
- package/dist/redux/api/datasourceManagementApi.js +35 -0
- package/dist/redux/api/datasourceManagementApi.js.map +1 -0
- package/dist/redux/api/datasourceManagementApi.ts +86 -0
- package/dist/redux/store/StoreProvider.d.ts.map +1 -1
- package/dist/redux/store/StoreProvider.js +4 -2
- package/dist/redux/store/StoreProvider.js.map +1 -1
- package/dist/redux/store/StoreProvider.tsx +9 -1
- package/dist/redux/store/defaultStoreConfig.d.ts +23 -0
- package/dist/redux/store/defaultStoreConfig.d.ts.map +1 -1
- package/dist/redux/store/defaultStoreConfig.js +4 -0
- package/dist/redux/store/defaultStoreConfig.js.map +1 -1
- package/dist/redux/store/defaultStoreConfig.ts +4 -0
- package/dist/resources/solid-custom.css +70 -16
- package/dist/routes/guards/AdminGuard.d.ts.map +1 -1
- package/dist/routes/guards/AdminGuard.js +2 -1
- package/dist/routes/guards/AdminGuard.js.map +1 -1
- package/dist/routes/guards/AdminGuard.tsx +7 -1
- package/dist/routes/guards/AuthGuard.d.ts.map +1 -1
- package/dist/routes/guards/AuthGuard.js +2 -1
- package/dist/routes/guards/AuthGuard.js.map +1 -1
- package/dist/routes/guards/AuthGuard.tsx +8 -2
- package/dist/routes/guards/GuestGuard.d.ts.map +1 -1
- package/dist/routes/guards/GuestGuard.js +2 -1
- package/dist/routes/guards/GuestGuard.js.map +1 -1
- package/dist/routes/guards/GuestGuard.tsx +8 -2
- package/dist/routes/pages/admin/core/DashboardPage.d.ts.map +1 -1
- package/dist/routes/pages/admin/core/DashboardPage.js +7 -5
- package/dist/routes/pages/admin/core/DashboardPage.js.map +1 -1
- package/dist/routes/pages/admin/core/DashboardPage.module.css +18 -0
- package/dist/routes/pages/admin/core/DashboardPage.tsx +50 -32
- package/dist/routes/pages/admin/core/DatasourceIntrospectionPage.css +828 -0
- package/dist/routes/pages/admin/core/DatasourceIntrospectionPage.d.ts +3 -0
- package/dist/routes/pages/admin/core/DatasourceIntrospectionPage.d.ts.map +1 -0
- package/dist/routes/pages/admin/core/DatasourceIntrospectionPage.js +947 -0
- package/dist/routes/pages/admin/core/DatasourceIntrospectionPage.js.map +1 -0
- package/dist/routes/pages/admin/core/DatasourceIntrospectionPage.tsx +1719 -0
- package/dist/routes/pages/admin/core/DatasourcesPage.css +337 -0
- package/dist/routes/pages/admin/core/DatasourcesPage.d.ts +3 -0
- package/dist/routes/pages/admin/core/DatasourcesPage.d.ts.map +1 -0
- package/dist/routes/pages/admin/core/DatasourcesPage.js +221 -0
- package/dist/routes/pages/admin/core/DatasourcesPage.js.map +1 -0
- package/dist/routes/pages/admin/core/DatasourcesPage.tsx +540 -0
- package/dist/routes/solidRoutes.d.ts.map +1 -1
- package/dist/routes/solidRoutes.js +5 -1
- package/dist/routes/solidRoutes.js.map +1 -1
- package/dist/routes/solidRoutes.tsx +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1719 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Cog,
|
|
3
|
+
Database,
|
|
4
|
+
ExternalLink,
|
|
5
|
+
RefreshCw,
|
|
6
|
+
Save,
|
|
7
|
+
Search,
|
|
8
|
+
ShieldAlert,
|
|
9
|
+
Sparkles,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import { camelCase, startCase } from "lodash";
|
|
12
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
13
|
+
import { useDispatch } from "react-redux";
|
|
14
|
+
import { useParams } from "react-router-dom";
|
|
15
|
+
import { showToast } from "../../../../redux/features/toastSlice";
|
|
16
|
+
import { SolidJsonEditor } from "../../../../components/core/json/SolidJsonEditor";
|
|
17
|
+
import { Column, SolidDataTable } from "../../../../components/core/list/SolidDataTable";
|
|
18
|
+
import {
|
|
19
|
+
SolidButton,
|
|
20
|
+
SolidCheckbox,
|
|
21
|
+
SolidDialog,
|
|
22
|
+
SolidDialogBody,
|
|
23
|
+
SolidDialogClose,
|
|
24
|
+
SolidDialogDescription,
|
|
25
|
+
SolidDialogFooter,
|
|
26
|
+
SolidDialogHeader,
|
|
27
|
+
SolidDialogSeparator,
|
|
28
|
+
SolidDialogTitle,
|
|
29
|
+
SolidInput,
|
|
30
|
+
SolidSelect,
|
|
31
|
+
SolidSpinner,
|
|
32
|
+
SolidSwitch,
|
|
33
|
+
SolidTabGroup,
|
|
34
|
+
SolidTextarea,
|
|
35
|
+
} from "../../../../components/shad-cn-ui";
|
|
36
|
+
import {
|
|
37
|
+
type DatasourceIntrospectionTableDetail,
|
|
38
|
+
type DatasourceIntrospectionMappingPreview,
|
|
39
|
+
type DatasourceIntrospectionMappingRequest,
|
|
40
|
+
type DatasourceIntrospectionTableRecord,
|
|
41
|
+
useApplyDatasourceIntrospectionMappingMutation,
|
|
42
|
+
useGetDatasourceIntrospectionBootstrapQuery,
|
|
43
|
+
useGetDatasourceIntrospectionTableDetailQuery,
|
|
44
|
+
useGetDatasourceIntrospectionTablesQuery,
|
|
45
|
+
usePreviewDatasourceIntrospectionMappingMutation,
|
|
46
|
+
} from "../../../../redux/api/datasourceIntrospectionApi";
|
|
47
|
+
import { useGetmodelByIdQuery } from "../../../../redux/api/modelApi";
|
|
48
|
+
import "./DatasourceIntrospectionPage.css";
|
|
49
|
+
|
|
50
|
+
type SessionSettings = {
|
|
51
|
+
relationInferenceEnabled: boolean;
|
|
52
|
+
prioritizeUnmappedTables: boolean;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type DrawerColumnDraft = {
|
|
56
|
+
columnName: string;
|
|
57
|
+
fieldName: string;
|
|
58
|
+
dataType: string;
|
|
59
|
+
solidFieldType: string;
|
|
60
|
+
ormType: string;
|
|
61
|
+
nullable: boolean;
|
|
62
|
+
defaultValue: string | null;
|
|
63
|
+
characterMaximumLength: number | null;
|
|
64
|
+
numericPrecision: number | null;
|
|
65
|
+
numericScale: number | null;
|
|
66
|
+
ordinalPosition: number | null;
|
|
67
|
+
isAutoGenerated: boolean;
|
|
68
|
+
handledBySuperclass: boolean;
|
|
69
|
+
superclassFieldName: string | null;
|
|
70
|
+
fieldId: number | null;
|
|
71
|
+
include: boolean;
|
|
72
|
+
matched: boolean;
|
|
73
|
+
isPrimaryKey: boolean;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
type DrawerDraft = {
|
|
77
|
+
mapped: boolean;
|
|
78
|
+
modelId: number | null;
|
|
79
|
+
mappedModuleLabel: string | null;
|
|
80
|
+
singularName: string;
|
|
81
|
+
pluralName: string;
|
|
82
|
+
displayName: string;
|
|
83
|
+
description: string;
|
|
84
|
+
tableName: string;
|
|
85
|
+
schema: string | null;
|
|
86
|
+
dataSource: string;
|
|
87
|
+
dataSourceType: string;
|
|
88
|
+
legacyTableType: "none" | "existing_id" | "generated_id";
|
|
89
|
+
baseClassName: string;
|
|
90
|
+
userKeyField: string;
|
|
91
|
+
enableAuditTracking: boolean;
|
|
92
|
+
enableSoftDelete: boolean;
|
|
93
|
+
draftPublishWorkflow: boolean;
|
|
94
|
+
internationalisation: boolean;
|
|
95
|
+
columns: DrawerColumnDraft[];
|
|
96
|
+
preservedFields: any[];
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function providerLabel(type: string) {
|
|
100
|
+
if (type === "postgres") return "PostgreSQL";
|
|
101
|
+
if (type === "mysql") return "MySQL";
|
|
102
|
+
if (type === "mssql") return "MS SQL Server";
|
|
103
|
+
return type;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const scalarFieldTypeOptions = [
|
|
107
|
+
{ value: "shortText", label: "Short Text" },
|
|
108
|
+
{ value: "longText", label: "Long Text" },
|
|
109
|
+
{ value: "richText", label: "Rich Text" },
|
|
110
|
+
{ value: "boolean", label: "Boolean" },
|
|
111
|
+
{ value: "int", label: "Integer" },
|
|
112
|
+
{ value: "bigint", label: "Big Integer" },
|
|
113
|
+
{ value: "decimal", label: "Decimal" },
|
|
114
|
+
{ value: "date", label: "Date" },
|
|
115
|
+
{ value: "datetime", label: "Date Time" },
|
|
116
|
+
{ value: "time", label: "Time" },
|
|
117
|
+
{ value: "json", label: "JSON" },
|
|
118
|
+
{ value: "uuid", label: "UUID" },
|
|
119
|
+
{ value: "email", label: "Email" },
|
|
120
|
+
{ value: "password", label: "Password" },
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
const scalarFieldTypeValueSet = new Set(scalarFieldTypeOptions.map((option) => option.value));
|
|
124
|
+
|
|
125
|
+
function getDefaultOrmTypeForSolidFieldType(dataSourceType: string, solidFieldType: string) {
|
|
126
|
+
const normalizedDatasourceType = dataSourceType.toLowerCase();
|
|
127
|
+
|
|
128
|
+
const ormTypesByDatasource: Record<string, Record<string, string>> = {
|
|
129
|
+
mssql: {
|
|
130
|
+
shortText: "varchar",
|
|
131
|
+
longText: "nvarchar",
|
|
132
|
+
richText: "nvarchar",
|
|
133
|
+
boolean: "bit",
|
|
134
|
+
int: "int",
|
|
135
|
+
bigint: "bigint",
|
|
136
|
+
decimal: "numeric",
|
|
137
|
+
date: "date",
|
|
138
|
+
datetime: "datetime",
|
|
139
|
+
time: "time",
|
|
140
|
+
json: "simple-json",
|
|
141
|
+
uuid: "uniqueidentifier",
|
|
142
|
+
email: "nvarchar",
|
|
143
|
+
password: "nvarchar",
|
|
144
|
+
},
|
|
145
|
+
mysql: {
|
|
146
|
+
shortText: "varchar",
|
|
147
|
+
longText: "longtext",
|
|
148
|
+
richText: "longtext",
|
|
149
|
+
boolean: "boolean",
|
|
150
|
+
int: "int",
|
|
151
|
+
bigint: "int",
|
|
152
|
+
decimal: "decimal",
|
|
153
|
+
date: "date",
|
|
154
|
+
datetime: "datetime",
|
|
155
|
+
time: "time",
|
|
156
|
+
json: "json",
|
|
157
|
+
uuid: "varchar",
|
|
158
|
+
email: "varchar",
|
|
159
|
+
password: "varchar",
|
|
160
|
+
},
|
|
161
|
+
postgres: {
|
|
162
|
+
shortText: "varchar",
|
|
163
|
+
longText: "text",
|
|
164
|
+
richText: "text",
|
|
165
|
+
boolean: "boolean",
|
|
166
|
+
int: "integer",
|
|
167
|
+
bigint: "bigint",
|
|
168
|
+
decimal: "decimal",
|
|
169
|
+
date: "date",
|
|
170
|
+
datetime: "timestamp",
|
|
171
|
+
time: "time",
|
|
172
|
+
json: "json",
|
|
173
|
+
uuid: "varchar",
|
|
174
|
+
email: "varchar",
|
|
175
|
+
password: "varchar",
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return ormTypesByDatasource[normalizedDatasourceType]?.[solidFieldType] ?? "varchar";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function legacyTypeLabel(value: string) {
|
|
183
|
+
if (value === "generated_id") return "Legacy generated id";
|
|
184
|
+
if (value === "existing_id") return "Legacy existing id";
|
|
185
|
+
return "Legacy generated id";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function statusLabel(record: DatasourceIntrospectionTableRecord) {
|
|
189
|
+
return record.mapped ? "Mapped" : "Ready to map";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function readQueryError(error: any) {
|
|
193
|
+
return (
|
|
194
|
+
error?.data?.message
|
|
195
|
+
|| error?.error
|
|
196
|
+
|| error?.message
|
|
197
|
+
|| "Something went wrong."
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeName(value: string) {
|
|
202
|
+
return value.trim().toLowerCase();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function findMatchingModelField(column: DatasourceIntrospectionTableDetail["columns"][number], modelFields: any[]) {
|
|
206
|
+
return modelFields.find((field) => {
|
|
207
|
+
if (field?.isSystem) return false;
|
|
208
|
+
|
|
209
|
+
const sameColumnName = normalizeName(field?.columnName ?? "") === normalizeName(column.columnName);
|
|
210
|
+
if (sameColumnName) return true;
|
|
211
|
+
|
|
212
|
+
return Boolean(column.isAutoGenerated && column.fieldName === "id" && field?.name === "id");
|
|
213
|
+
}) ?? null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function sanitizeExistingFieldPayload(field: any) {
|
|
217
|
+
const {
|
|
218
|
+
createdAt,
|
|
219
|
+
updatedAt,
|
|
220
|
+
deletedAt,
|
|
221
|
+
model,
|
|
222
|
+
mediaStorageProvider,
|
|
223
|
+
identifier,
|
|
224
|
+
...rest
|
|
225
|
+
} = field ?? {};
|
|
226
|
+
|
|
227
|
+
if (mediaStorageProvider?.id && !rest.mediaStorageProviderId) {
|
|
228
|
+
rest.mediaStorageProviderId = mediaStorageProvider.id;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return rest;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildDrawerDraft(
|
|
235
|
+
tableDetail: DatasourceIntrospectionTableDetail,
|
|
236
|
+
selectedTable: DatasourceIntrospectionTableRecord,
|
|
237
|
+
mappedModelData?: any,
|
|
238
|
+
): DrawerDraft {
|
|
239
|
+
const modelData = selectedTable.mapped ? (mappedModelData?.data ?? null) : null;
|
|
240
|
+
const modelFields = modelData?.fields ?? [];
|
|
241
|
+
const preservedFields = modelFields.map((field: any) => sanitizeExistingFieldPayload(field));
|
|
242
|
+
const modelUserKeyField =
|
|
243
|
+
modelData?.userKeyField?.name
|
|
244
|
+
|| modelFields.find((field: any) => field?.isUserKey)?.name
|
|
245
|
+
|| "";
|
|
246
|
+
|
|
247
|
+
const columns: DrawerColumnDraft[] = tableDetail.columns.map((column) => {
|
|
248
|
+
const matchingField = findMatchingModelField(column, modelFields);
|
|
249
|
+
return {
|
|
250
|
+
columnName: column.columnName,
|
|
251
|
+
fieldName: matchingField?.name ?? column.fieldName,
|
|
252
|
+
dataType: column.dataType,
|
|
253
|
+
solidFieldType: matchingField?.type ?? column.solidFieldType,
|
|
254
|
+
ormType: matchingField?.ormType ?? column.ormType,
|
|
255
|
+
nullable: column.nullable,
|
|
256
|
+
defaultValue: matchingField?.defaultValue ?? column.defaultValue,
|
|
257
|
+
characterMaximumLength: column.characterMaximumLength,
|
|
258
|
+
numericPrecision: column.numericPrecision,
|
|
259
|
+
numericScale: column.numericScale,
|
|
260
|
+
ordinalPosition: column.ordinalPosition,
|
|
261
|
+
isAutoGenerated: column.isAutoGenerated,
|
|
262
|
+
handledBySuperclass: column.handledBySuperclass,
|
|
263
|
+
superclassFieldName: column.superclassFieldName,
|
|
264
|
+
fieldId: matchingField?.id ?? null,
|
|
265
|
+
include: column.handledBySuperclass ? false : Boolean(matchingField) || !selectedTable.mapped,
|
|
266
|
+
matched: Boolean(matchingField),
|
|
267
|
+
isPrimaryKey: Boolean(matchingField?.isPrimaryKey) || (column.isAutoGenerated && column.fieldName === "id"),
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const draftUserKeyField = modelUserKeyField || tableDetail.suggestedMetadata.userKeyField;
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
mapped: selectedTable.mapped,
|
|
275
|
+
modelId: selectedTable.mappedModelId,
|
|
276
|
+
mappedModuleLabel: selectedTable.mappedModuleDisplayName || selectedTable.mappedModuleName || null,
|
|
277
|
+
singularName: modelData?.singularName ?? tableDetail.suggestedMetadata.singularName,
|
|
278
|
+
pluralName: modelData?.pluralName ?? tableDetail.suggestedMetadata.pluralName,
|
|
279
|
+
displayName: modelData?.displayName ?? tableDetail.suggestedMetadata.displayName,
|
|
280
|
+
description: modelData?.description ?? tableDetail.suggestedMetadata.displayName,
|
|
281
|
+
tableName: modelData?.tableName ?? tableDetail.suggestedMetadata.tableName,
|
|
282
|
+
schema: tableDetail.suggestedMetadata.schema,
|
|
283
|
+
dataSource: modelData?.dataSource ?? tableDetail.suggestedMetadata.dataSource,
|
|
284
|
+
dataSourceType: modelData?.dataSourceType ?? tableDetail.suggestedMetadata.dataSourceType,
|
|
285
|
+
legacyTableType: modelData?.legacyTableType ?? tableDetail.suggestedMetadata.legacyTableType,
|
|
286
|
+
baseClassName: tableDetail.suggestedMetadata.baseClassName,
|
|
287
|
+
userKeyField: draftUserKeyField,
|
|
288
|
+
enableAuditTracking: modelData?.enableAuditTracking ?? true,
|
|
289
|
+
enableSoftDelete: modelData?.enableSoftDelete ?? false,
|
|
290
|
+
draftPublishWorkflow: modelData?.draftPublishWorkflow ?? false,
|
|
291
|
+
internationalisation: modelData?.internationalisation ?? false,
|
|
292
|
+
columns,
|
|
293
|
+
preservedFields,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getDrawerValidationErrors(draft: DrawerDraft) {
|
|
298
|
+
const errors: string[] = [];
|
|
299
|
+
const includedColumns = draft.columns.filter((column) => !column.handledBySuperclass && column.include);
|
|
300
|
+
const seenFieldNames = new Set<string>();
|
|
301
|
+
|
|
302
|
+
if (!draft.singularName.trim()) {
|
|
303
|
+
errors.push("Singular name is required.");
|
|
304
|
+
}
|
|
305
|
+
if (!draft.pluralName.trim()) {
|
|
306
|
+
errors.push("Plural name is required.");
|
|
307
|
+
}
|
|
308
|
+
if (!draft.displayName.trim()) {
|
|
309
|
+
errors.push("Display name is required.");
|
|
310
|
+
}
|
|
311
|
+
if (!includedColumns.length) {
|
|
312
|
+
errors.push("Select at least one physical column to include in the mapping.");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (const column of includedColumns) {
|
|
316
|
+
const normalizedFieldName = column.fieldName.trim();
|
|
317
|
+
if (!normalizedFieldName) {
|
|
318
|
+
errors.push(`Field name is required for column "${column.columnName}".`);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!scalarFieldTypeValueSet.has(column.solidFieldType)) {
|
|
323
|
+
errors.push(`SolidX type "${column.solidFieldType}" is not allowed in step 1 for column "${column.columnName}".`);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (seenFieldNames.has(normalizedFieldName.toLowerCase())) {
|
|
328
|
+
errors.push(`Field name "${normalizedFieldName}" is duplicated in the mapping draft.`);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
seenFieldNames.add(normalizedFieldName.toLowerCase());
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (!draft.userKeyField.trim()) {
|
|
336
|
+
errors.push("Choose a user key field before saving the mapping.");
|
|
337
|
+
} else {
|
|
338
|
+
const userKeyExists = includedColumns.some((column) => column.fieldName === draft.userKeyField);
|
|
339
|
+
if (!userKeyExists) {
|
|
340
|
+
errors.push("The chosen user key field must remain part of the mapped columns.");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (draft.legacyTableType === "existing_id") {
|
|
345
|
+
const hasPrimaryKey = includedColumns.some((column) => column.isPrimaryKey);
|
|
346
|
+
if (!hasPrimaryKey) {
|
|
347
|
+
errors.push("Legacy existing-id mappings must keep the generated id column mapped to field name \"id\".");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return errors;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function buildMappingRequest(
|
|
355
|
+
draft: DrawerDraft,
|
|
356
|
+
plannedMigrationTimestamp?: number | null,
|
|
357
|
+
): DatasourceIntrospectionMappingRequest {
|
|
358
|
+
return {
|
|
359
|
+
mapped: draft.mapped,
|
|
360
|
+
modelId: draft.modelId,
|
|
361
|
+
singularName: draft.singularName.trim(),
|
|
362
|
+
pluralName: draft.pluralName.trim(),
|
|
363
|
+
displayName: draft.displayName.trim(),
|
|
364
|
+
description: draft.description.trim() || draft.displayName.trim(),
|
|
365
|
+
tableName: draft.tableName,
|
|
366
|
+
schema: draft.schema ?? null,
|
|
367
|
+
dataSource: draft.dataSource,
|
|
368
|
+
dataSourceType: draft.dataSourceType,
|
|
369
|
+
legacyTableType: draft.legacyTableType,
|
|
370
|
+
baseClassName: draft.baseClassName,
|
|
371
|
+
userKeyField: draft.userKeyField,
|
|
372
|
+
enableAuditTracking: draft.enableAuditTracking,
|
|
373
|
+
enableSoftDelete: draft.enableSoftDelete,
|
|
374
|
+
draftPublishWorkflow: draft.draftPublishWorkflow,
|
|
375
|
+
internationalisation: draft.internationalisation,
|
|
376
|
+
plannedMigrationTimestamp: plannedMigrationTimestamp ?? null,
|
|
377
|
+
columns: draft.columns.map((column) => ({
|
|
378
|
+
columnName: column.columnName,
|
|
379
|
+
fieldName: column.fieldName,
|
|
380
|
+
dataType: column.dataType,
|
|
381
|
+
solidFieldType: column.solidFieldType,
|
|
382
|
+
ormType: column.ormType,
|
|
383
|
+
nullable: column.nullable,
|
|
384
|
+
defaultValue: column.defaultValue,
|
|
385
|
+
characterMaximumLength: column.characterMaximumLength,
|
|
386
|
+
numericPrecision: column.numericPrecision,
|
|
387
|
+
numericScale: column.numericScale,
|
|
388
|
+
ordinalPosition: column.ordinalPosition,
|
|
389
|
+
isAutoGenerated: column.isAutoGenerated,
|
|
390
|
+
handledBySuperclass: column.handledBySuperclass,
|
|
391
|
+
superclassFieldName: column.superclassFieldName,
|
|
392
|
+
fieldId: column.fieldId,
|
|
393
|
+
include: column.include,
|
|
394
|
+
matched: column.matched,
|
|
395
|
+
isPrimaryKey: column.isPrimaryKey,
|
|
396
|
+
})),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function parseAndValidateReviewedModelJson(
|
|
401
|
+
metadataJsonText: string,
|
|
402
|
+
draft: DrawerDraft,
|
|
403
|
+
) {
|
|
404
|
+
let parsed: any;
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
parsed = JSON.parse(metadataJsonText);
|
|
408
|
+
} catch (error: any) {
|
|
409
|
+
return {
|
|
410
|
+
error: `Metadata JSON is not valid JSON: ${error?.message ?? "Unknown parse error."}`,
|
|
411
|
+
parsed: null,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
416
|
+
return {
|
|
417
|
+
error: "Metadata JSON must be an object.",
|
|
418
|
+
parsed: null,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const requiredStringKeys = [
|
|
423
|
+
"singularName",
|
|
424
|
+
"pluralName",
|
|
425
|
+
"displayName",
|
|
426
|
+
"description",
|
|
427
|
+
"dataSource",
|
|
428
|
+
"dataSourceType",
|
|
429
|
+
"tableName",
|
|
430
|
+
"userKeyFieldUserKey",
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
for (const key of requiredStringKeys) {
|
|
434
|
+
if (typeof parsed[key] !== "string" || !parsed[key].trim()) {
|
|
435
|
+
return {
|
|
436
|
+
error: `Metadata JSON must include a non-empty "${key}" string.`,
|
|
437
|
+
parsed: null,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const immutableChecks: Array<{ key: string; expected: string }> = [
|
|
443
|
+
{ key: "dataSource", expected: draft.dataSource },
|
|
444
|
+
{ key: "dataSourceType", expected: draft.dataSourceType },
|
|
445
|
+
{ key: "tableName", expected: draft.tableName },
|
|
446
|
+
{ key: "legacyTableType", expected: draft.legacyTableType },
|
|
447
|
+
];
|
|
448
|
+
|
|
449
|
+
for (const check of immutableChecks) {
|
|
450
|
+
if (`${parsed[check.key] ?? ""}` !== `${check.expected}`) {
|
|
451
|
+
return {
|
|
452
|
+
error: `Metadata JSON cannot change immutable binding key "${check.key}".`,
|
|
453
|
+
parsed: null,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const requiredBooleanKeys = [
|
|
459
|
+
"enableAuditTracking",
|
|
460
|
+
"enableSoftDelete",
|
|
461
|
+
"draftPublishWorkflow",
|
|
462
|
+
"internationalisation",
|
|
463
|
+
];
|
|
464
|
+
|
|
465
|
+
for (const key of requiredBooleanKeys) {
|
|
466
|
+
if (typeof parsed[key] !== "boolean") {
|
|
467
|
+
return {
|
|
468
|
+
error: `Metadata JSON must include a boolean "${key}" value.`,
|
|
469
|
+
parsed: null,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (!Array.isArray(parsed.fields)) {
|
|
475
|
+
return {
|
|
476
|
+
error: "Metadata JSON must include a fields array.",
|
|
477
|
+
parsed: null,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const seenFieldNames = new Set<string>();
|
|
482
|
+
for (const [index, field] of parsed.fields.entries()) {
|
|
483
|
+
if (!field || typeof field !== "object" || Array.isArray(field)) {
|
|
484
|
+
return {
|
|
485
|
+
error: `Field at index ${index} is not a valid object.`,
|
|
486
|
+
parsed: null,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (typeof field.name !== "string" || !field.name.trim()) {
|
|
491
|
+
return {
|
|
492
|
+
error: `Field at index ${index} must include a non-empty "name".`,
|
|
493
|
+
parsed: null,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (typeof field.type !== "string" || !field.type.trim()) {
|
|
498
|
+
return {
|
|
499
|
+
error: `Field "${field.name}" must include a non-empty "type".`,
|
|
500
|
+
parsed: null,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (typeof field.ormType !== "string" || !field.ormType.trim()) {
|
|
505
|
+
return {
|
|
506
|
+
error: `Field "${field.name}" must include a non-empty "ormType".`,
|
|
507
|
+
parsed: null,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (typeof field.columnName !== "string" || !field.columnName.trim()) {
|
|
512
|
+
return {
|
|
513
|
+
error: `Field "${field.name}" must include a non-empty "columnName".`,
|
|
514
|
+
parsed: null,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const normalizedFieldName = field.name.trim().toLowerCase();
|
|
519
|
+
if (seenFieldNames.has(normalizedFieldName)) {
|
|
520
|
+
return {
|
|
521
|
+
error: `Field name "${field.name}" is duplicated in metadata JSON.`,
|
|
522
|
+
parsed: null,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
seenFieldNames.add(normalizedFieldName);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (!parsed.fields.some((field: any) => field?.name === parsed.userKeyFieldUserKey && !field?.isMarkedForRemoval)) {
|
|
530
|
+
return {
|
|
531
|
+
error: "Metadata JSON must keep userKeyFieldUserKey present in the fields array.",
|
|
532
|
+
parsed: null,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
error: null,
|
|
538
|
+
parsed,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function DatasourceIntrospectionPage() {
|
|
543
|
+
const params = useParams();
|
|
544
|
+
const dispatch = useDispatch();
|
|
545
|
+
const moduleId = Number(params.moduleId ?? 0);
|
|
546
|
+
const isValidModuleId = Number.isFinite(moduleId) && moduleId > 0;
|
|
547
|
+
|
|
548
|
+
const {
|
|
549
|
+
data: bootstrap,
|
|
550
|
+
isLoading: isBootstrapLoading,
|
|
551
|
+
isFetching: isBootstrapFetching,
|
|
552
|
+
error: bootstrapError,
|
|
553
|
+
refetch: refetchBootstrap,
|
|
554
|
+
} = useGetDatasourceIntrospectionBootstrapQuery(moduleId, {
|
|
555
|
+
skip: !isValidModuleId,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const [selectedDatasource, setSelectedDatasource] = useState("");
|
|
559
|
+
const [selectedTableKey, setSelectedTableKey] = useState("");
|
|
560
|
+
const [tableSearch, setTableSearch] = useState("");
|
|
561
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
562
|
+
const [settingsTab, setSettingsTab] = useState("datasource-info");
|
|
563
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
564
|
+
const [drawerDraft, setDrawerDraft] = useState<DrawerDraft | null>(null);
|
|
565
|
+
const [drawerTab, setDrawerTab] = useState("general-info");
|
|
566
|
+
const [drawerStep, setDrawerStep] = useState<"edit" | "review">("edit");
|
|
567
|
+
const [reviewTab, setReviewTab] = useState("metadata-json");
|
|
568
|
+
const [mappingPreview, setMappingPreview] = useState<DatasourceIntrospectionMappingPreview | null>(null);
|
|
569
|
+
const [reviewedMetadataJsonValue, setReviewedMetadataJsonValue] = useState<any>(null);
|
|
570
|
+
const [metadataJsonText, setMetadataJsonText] = useState("");
|
|
571
|
+
const [metadataJsonValidationError, setMetadataJsonValidationError] = useState<string | null>(null);
|
|
572
|
+
const [metadataJsonResetToken, setMetadataJsonResetToken] = useState("initial");
|
|
573
|
+
const [drawerBootstrapping, setDrawerBootstrapping] = useState(false);
|
|
574
|
+
const [sessionSettings, setSessionSettings] = useState<SessionSettings>({
|
|
575
|
+
relationInferenceEnabled: false,
|
|
576
|
+
prioritizeUnmappedTables: true,
|
|
577
|
+
});
|
|
578
|
+
const [draftSessionSettings, setDraftSessionSettings] = useState<SessionSettings>(sessionSettings);
|
|
579
|
+
const drawerBootstrapTimerRef = useRef<number | null>(null);
|
|
580
|
+
|
|
581
|
+
const scrollDrawerBodyToTop = () => {
|
|
582
|
+
if (typeof document === "undefined") return;
|
|
583
|
+
|
|
584
|
+
window.requestAnimationFrame(() => {
|
|
585
|
+
const drawerBody = document.querySelector<HTMLDivElement>(".sdix-drawer-body");
|
|
586
|
+
drawerBody?.scrollTo({ top: 0, left: 0 });
|
|
587
|
+
});
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const [
|
|
591
|
+
previewMapping,
|
|
592
|
+
{ isLoading: isPreviewMappingLoading },
|
|
593
|
+
] = usePreviewDatasourceIntrospectionMappingMutation();
|
|
594
|
+
|
|
595
|
+
const [
|
|
596
|
+
applyMapping,
|
|
597
|
+
{ isLoading: isApplyMappingLoading },
|
|
598
|
+
] = useApplyDatasourceIntrospectionMappingMutation();
|
|
599
|
+
|
|
600
|
+
const isSavingMapping = isApplyMappingLoading;
|
|
601
|
+
|
|
602
|
+
useEffect(() => {
|
|
603
|
+
if (!bootstrap) return;
|
|
604
|
+
if (selectedDatasource) return;
|
|
605
|
+
if (bootstrap.recommendedDatasourceName) {
|
|
606
|
+
setSelectedDatasource(bootstrap.recommendedDatasourceName);
|
|
607
|
+
}
|
|
608
|
+
}, [bootstrap, selectedDatasource]);
|
|
609
|
+
|
|
610
|
+
useEffect(() => {
|
|
611
|
+
setSelectedTableKey("");
|
|
612
|
+
setDrawerDraft(null);
|
|
613
|
+
setDrawerOpen(false);
|
|
614
|
+
setTableSearch("");
|
|
615
|
+
setDrawerStep("edit");
|
|
616
|
+
setReviewTab("metadata-json");
|
|
617
|
+
setMappingPreview(null);
|
|
618
|
+
setReviewedMetadataJsonValue(null);
|
|
619
|
+
setMetadataJsonText("");
|
|
620
|
+
setMetadataJsonValidationError(null);
|
|
621
|
+
setMetadataJsonResetToken(`datasource-${Date.now()}`);
|
|
622
|
+
}, [selectedDatasource]);
|
|
623
|
+
|
|
624
|
+
useEffect(() => () => {
|
|
625
|
+
if (drawerBootstrapTimerRef.current) {
|
|
626
|
+
window.clearTimeout(drawerBootstrapTimerRef.current);
|
|
627
|
+
}
|
|
628
|
+
}, []);
|
|
629
|
+
|
|
630
|
+
useEffect(() => {
|
|
631
|
+
if (!drawerOpen) return;
|
|
632
|
+
scrollDrawerBodyToTop();
|
|
633
|
+
}, [drawerOpen, drawerStep, drawerTab, reviewTab, selectedTableKey]);
|
|
634
|
+
|
|
635
|
+
const datasourceOptions = useMemo(
|
|
636
|
+
() => (bootstrap?.datasources ?? []).map((datasource) => ({
|
|
637
|
+
label: `${datasource.displayName} (${providerLabel(datasource.type)})`,
|
|
638
|
+
value: datasource.name,
|
|
639
|
+
})),
|
|
640
|
+
[bootstrap],
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
const {
|
|
644
|
+
data: tablesResponse,
|
|
645
|
+
isLoading: isTablesLoading,
|
|
646
|
+
isFetching: isTablesFetching,
|
|
647
|
+
error: tablesError,
|
|
648
|
+
refetch: refetchTables,
|
|
649
|
+
} = useGetDatasourceIntrospectionTablesQuery(
|
|
650
|
+
{ moduleId, datasource: selectedDatasource },
|
|
651
|
+
{
|
|
652
|
+
skip: !isValidModuleId || !selectedDatasource,
|
|
653
|
+
},
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
const selectedTable = useMemo(
|
|
657
|
+
() => tablesResponse?.records?.find((record) => `${record.schema ?? ""}::${record.tableName}` === selectedTableKey) ?? null,
|
|
658
|
+
[tablesResponse, selectedTableKey],
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
const {
|
|
662
|
+
data: tableDetail,
|
|
663
|
+
isLoading: isTableDetailLoading,
|
|
664
|
+
isFetching: isTableDetailFetching,
|
|
665
|
+
error: tableDetailError,
|
|
666
|
+
refetch: refetchTableDetail,
|
|
667
|
+
} = useGetDatasourceIntrospectionTableDetailQuery(
|
|
668
|
+
{
|
|
669
|
+
moduleId,
|
|
670
|
+
datasource: selectedDatasource,
|
|
671
|
+
table: selectedTable?.tableName ?? "",
|
|
672
|
+
schema: selectedTable?.schema ?? undefined,
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
skip: !drawerOpen || !isValidModuleId || !selectedDatasource || !selectedTable || !!tablesResponse?.synchronizeBlocked,
|
|
676
|
+
},
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
const {
|
|
680
|
+
data: mappedModelResponse,
|
|
681
|
+
isLoading: isMappedModelLoading,
|
|
682
|
+
isFetching: isMappedModelFetching,
|
|
683
|
+
error: mappedModelError,
|
|
684
|
+
} = useGetmodelByIdQuery(selectedTable?.mappedModelId ?? 0, {
|
|
685
|
+
skip: !drawerOpen || !selectedTable?.mapped || !selectedTable?.mappedModelId,
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const mappedModelData = mappedModelResponse?.data;
|
|
689
|
+
const selectedDatasourceRecord = bootstrap?.datasources?.find((datasource) => datasource.name === selectedDatasource) ?? null;
|
|
690
|
+
const drawerLoadingError = selectedTable?.mapped ? (tableDetailError || mappedModelError) : tableDetailError;
|
|
691
|
+
const isDrawerBlockingLoading = Boolean(
|
|
692
|
+
selectedTable
|
|
693
|
+
&& (
|
|
694
|
+
drawerBootstrapping
|
|
695
|
+
|| !drawerDraft
|
|
696
|
+
|| isTableDetailLoading
|
|
697
|
+
|| isTableDetailFetching
|
|
698
|
+
|| (selectedTable.mapped && (isMappedModelLoading || isMappedModelFetching))
|
|
699
|
+
),
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
useEffect(() => {
|
|
703
|
+
if (!drawerOpen || !selectedTable || !tableDetail) return;
|
|
704
|
+
if (selectedTable.mapped && (!mappedModelData || mappedModelData.id !== selectedTable.mappedModelId)) return;
|
|
705
|
+
|
|
706
|
+
if (drawerBootstrapTimerRef.current) {
|
|
707
|
+
window.clearTimeout(drawerBootstrapTimerRef.current);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
setDrawerDraft(buildDrawerDraft(
|
|
711
|
+
tableDetail,
|
|
712
|
+
selectedTable,
|
|
713
|
+
selectedTable.mapped ? mappedModelResponse : undefined,
|
|
714
|
+
));
|
|
715
|
+
drawerBootstrapTimerRef.current = window.setTimeout(() => {
|
|
716
|
+
setDrawerBootstrapping(false);
|
|
717
|
+
}, 150);
|
|
718
|
+
}, [drawerOpen, selectedTable, tableDetail, mappedModelData, mappedModelResponse]);
|
|
719
|
+
|
|
720
|
+
const filteredRecords = useMemo(() => {
|
|
721
|
+
const records = [...(tablesResponse?.records ?? [])];
|
|
722
|
+
const searchNeedle = tableSearch.trim().toLowerCase();
|
|
723
|
+
|
|
724
|
+
const filtered = searchNeedle
|
|
725
|
+
? records.filter((record) => {
|
|
726
|
+
const haystack = `${record.tableName} ${record.schema ?? ""} ${record.mappedModelSingularName ?? ""} ${record.mappedModelDisplayName ?? ""}`.toLowerCase();
|
|
727
|
+
return haystack.includes(searchNeedle);
|
|
728
|
+
})
|
|
729
|
+
: records;
|
|
730
|
+
|
|
731
|
+
if (sessionSettings.prioritizeUnmappedTables) {
|
|
732
|
+
filtered.sort((left, right) => {
|
|
733
|
+
if (left.mapped !== right.mapped) {
|
|
734
|
+
return left.mapped ? 1 : -1;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const leftLabel = `${left.schema ?? ""}.${left.tableName}`.toLowerCase();
|
|
738
|
+
const rightLabel = `${right.schema ?? ""}.${right.tableName}`.toLowerCase();
|
|
739
|
+
return leftLabel.localeCompare(rightLabel);
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return filtered;
|
|
744
|
+
}, [sessionSettings.prioritizeUnmappedTables, tableSearch, tablesResponse?.records]);
|
|
745
|
+
|
|
746
|
+
const drawerUserKeyOptions = useMemo(() => {
|
|
747
|
+
const options = new Map<string, { label: string; value: string }>();
|
|
748
|
+
|
|
749
|
+
tableDetail?.userKeyCandidates?.forEach((candidate) => {
|
|
750
|
+
options.set(candidate.name, {
|
|
751
|
+
label: `${candidate.label} · ${candidate.reason}`,
|
|
752
|
+
value: candidate.name,
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
drawerDraft?.columns
|
|
757
|
+
.filter((column) => !column.handledBySuperclass && column.include)
|
|
758
|
+
.forEach((column) => {
|
|
759
|
+
if (!options.has(column.fieldName)) {
|
|
760
|
+
options.set(column.fieldName, {
|
|
761
|
+
label: `${column.fieldName} · mapped from ${column.columnName}`,
|
|
762
|
+
value: column.fieldName,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
return Array.from(options.values());
|
|
768
|
+
}, [drawerDraft?.columns, tableDetail?.userKeyCandidates]);
|
|
769
|
+
|
|
770
|
+
const settingsTabs = [
|
|
771
|
+
{
|
|
772
|
+
value: "datasource-info",
|
|
773
|
+
label: "Datasource Info",
|
|
774
|
+
content: (
|
|
775
|
+
<div className="sdix-settings-grid">
|
|
776
|
+
<div className="sdix-settings-static">
|
|
777
|
+
<span>Target module</span>
|
|
778
|
+
<strong>{bootstrap?.module?.displayName}</strong>
|
|
779
|
+
</div>
|
|
780
|
+
<div className="sdix-settings-static">
|
|
781
|
+
<span>Default datasource</span>
|
|
782
|
+
<strong>{bootstrap?.module?.defaultDataSource || "Not set"}</strong>
|
|
783
|
+
</div>
|
|
784
|
+
<div className="sdix-settings-static">
|
|
785
|
+
<span>Datasource name</span>
|
|
786
|
+
<strong>{selectedDatasourceRecord?.displayName || "Not selected"}</strong>
|
|
787
|
+
</div>
|
|
788
|
+
<div className="sdix-settings-static">
|
|
789
|
+
<span>Datasource type</span>
|
|
790
|
+
<strong>{selectedDatasourceRecord ? providerLabel(selectedDatasourceRecord.type) : "-"}</strong>
|
|
791
|
+
</div>
|
|
792
|
+
<div className="sdix-settings-static">
|
|
793
|
+
<span>Host</span>
|
|
794
|
+
<strong>{selectedDatasourceRecord?.host || "-"}</strong>
|
|
795
|
+
</div>
|
|
796
|
+
<div className="sdix-settings-static">
|
|
797
|
+
<span>Port</span>
|
|
798
|
+
<strong>{selectedDatasourceRecord?.port ?? "-"}</strong>
|
|
799
|
+
</div>
|
|
800
|
+
<div className="sdix-settings-static">
|
|
801
|
+
<span>Database</span>
|
|
802
|
+
<strong>{selectedDatasourceRecord?.database || "-"}</strong>
|
|
803
|
+
</div>
|
|
804
|
+
<div className="sdix-settings-static">
|
|
805
|
+
<span>Env Prefix</span>
|
|
806
|
+
<strong>{selectedDatasourceRecord?.envPrefix || "-"}</strong>
|
|
807
|
+
</div>
|
|
808
|
+
<div className="sdix-settings-static">
|
|
809
|
+
<span>Synchronize</span>
|
|
810
|
+
<strong>{selectedDatasourceRecord ? (selectedDatasourceRecord.synchronize === true ? "true" : "false") : "-"}</strong>
|
|
811
|
+
</div>
|
|
812
|
+
</div>
|
|
813
|
+
),
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
value: "session-settings",
|
|
817
|
+
label: "Session Settings",
|
|
818
|
+
content: (
|
|
819
|
+
<div className="sdix-settings-grid">
|
|
820
|
+
<div className="sdix-settings-row">
|
|
821
|
+
<div>
|
|
822
|
+
<label>Relation inference</label>
|
|
823
|
+
<p>Keep this off until we deliberately introduce relation discovery across engines.</p>
|
|
824
|
+
</div>
|
|
825
|
+
<SolidSwitch
|
|
826
|
+
checked={draftSessionSettings.relationInferenceEnabled}
|
|
827
|
+
onChange={(checked) => setDraftSessionSettings((current) => ({ ...current, relationInferenceEnabled: checked }))}
|
|
828
|
+
/>
|
|
829
|
+
</div>
|
|
830
|
+
|
|
831
|
+
<div className="sdix-settings-row">
|
|
832
|
+
<div>
|
|
833
|
+
<label>Prioritize unmapped tables</label>
|
|
834
|
+
<p>Bubble unmapped tables to the top of the inventory so the first pass stays focused.</p>
|
|
835
|
+
</div>
|
|
836
|
+
<SolidSwitch
|
|
837
|
+
checked={draftSessionSettings.prioritizeUnmappedTables}
|
|
838
|
+
onChange={(checked) => setDraftSessionSettings((current) => ({ ...current, prioritizeUnmappedTables: checked }))}
|
|
839
|
+
/>
|
|
840
|
+
</div>
|
|
841
|
+
</div>
|
|
842
|
+
),
|
|
843
|
+
},
|
|
844
|
+
];
|
|
845
|
+
|
|
846
|
+
const handleRefresh = async () => {
|
|
847
|
+
await refetchBootstrap();
|
|
848
|
+
if (selectedDatasource) {
|
|
849
|
+
await refetchTables();
|
|
850
|
+
}
|
|
851
|
+
if (selectedTable) {
|
|
852
|
+
await refetchTableDetail();
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
const openTableDrawer = (record: DatasourceIntrospectionTableRecord) => {
|
|
857
|
+
if (drawerBootstrapTimerRef.current) {
|
|
858
|
+
window.clearTimeout(drawerBootstrapTimerRef.current);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
setSelectedTableKey(`${record.schema ?? ""}::${record.tableName}`);
|
|
862
|
+
setDrawerDraft(null);
|
|
863
|
+
setDrawerTab("general-info");
|
|
864
|
+
setDrawerStep("edit");
|
|
865
|
+
setReviewTab("metadata-json");
|
|
866
|
+
setMappingPreview(null);
|
|
867
|
+
setReviewedMetadataJsonValue(null);
|
|
868
|
+
setMetadataJsonText("");
|
|
869
|
+
setMetadataJsonValidationError(null);
|
|
870
|
+
setMetadataJsonResetToken(`table-${record.schema ?? "default"}-${record.tableName}-${Date.now()}`);
|
|
871
|
+
setDrawerBootstrapping(true);
|
|
872
|
+
setDrawerOpen(true);
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
const applyDraftSettings = () => {
|
|
876
|
+
setSessionSettings(draftSessionSettings);
|
|
877
|
+
setSettingsOpen(false);
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
const updateDrawerDraft = (updater: (current: DrawerDraft) => DrawerDraft) => {
|
|
881
|
+
setMappingPreview(null);
|
|
882
|
+
setDrawerDraft((current) => (current ? updater(current) : current));
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
const updateGeneralField = (field: keyof DrawerDraft, value: any) => {
|
|
886
|
+
updateDrawerDraft((current) => ({ ...current, [field]: value }));
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
const updateColumnFieldName = (columnName: string, nextFieldName: string) => {
|
|
890
|
+
updateDrawerDraft((current) => ({
|
|
891
|
+
...current,
|
|
892
|
+
columns: current.columns.map((column) => {
|
|
893
|
+
if (column.columnName !== columnName) return column;
|
|
894
|
+
return { ...column, fieldName: camelCase(nextFieldName) };
|
|
895
|
+
}),
|
|
896
|
+
userKeyField: current.userKeyField === current.columns.find((column) => column.columnName === columnName)?.fieldName
|
|
897
|
+
? camelCase(nextFieldName)
|
|
898
|
+
: current.userKeyField,
|
|
899
|
+
}));
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
const updateColumnSolidFieldType = (columnName: string, nextSolidFieldType: string) => {
|
|
903
|
+
updateDrawerDraft((current) => ({
|
|
904
|
+
...current,
|
|
905
|
+
columns: current.columns.map((column) => {
|
|
906
|
+
if (column.columnName !== columnName) return column;
|
|
907
|
+
if (column.handledBySuperclass) return column;
|
|
908
|
+
|
|
909
|
+
return {
|
|
910
|
+
...column,
|
|
911
|
+
solidFieldType: nextSolidFieldType,
|
|
912
|
+
ormType: getDefaultOrmTypeForSolidFieldType(current.dataSourceType, nextSolidFieldType),
|
|
913
|
+
};
|
|
914
|
+
}),
|
|
915
|
+
}));
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
const setColumnIncluded = (columnName: string, include: boolean) => {
|
|
919
|
+
updateDrawerDraft((current) => ({
|
|
920
|
+
...current,
|
|
921
|
+
columns: current.columns.map((column) => {
|
|
922
|
+
if (column.columnName !== columnName) return column;
|
|
923
|
+
if (column.handledBySuperclass || column.isPrimaryKey) return column;
|
|
924
|
+
return { ...column, include };
|
|
925
|
+
}),
|
|
926
|
+
}));
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
const changeUserKeyField = (fieldName: string) => {
|
|
930
|
+
updateDrawerDraft((current) => ({
|
|
931
|
+
...current,
|
|
932
|
+
userKeyField: fieldName,
|
|
933
|
+
columns: current.columns.map((column) => (
|
|
934
|
+
column.fieldName === fieldName
|
|
935
|
+
? { ...column, include: column.handledBySuperclass ? false : true }
|
|
936
|
+
: column
|
|
937
|
+
)),
|
|
938
|
+
}));
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
const setUnmappedColumnsIncluded = (include: boolean) => {
|
|
942
|
+
updateDrawerDraft((current) => ({
|
|
943
|
+
...current,
|
|
944
|
+
columns: current.columns.map((column) => {
|
|
945
|
+
if (column.handledBySuperclass || column.matched || column.isPrimaryKey) {
|
|
946
|
+
return column;
|
|
947
|
+
}
|
|
948
|
+
return { ...column, include };
|
|
949
|
+
}),
|
|
950
|
+
}));
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
const openMappedModelForm = () => {
|
|
954
|
+
const targetModelId = drawerDraft?.modelId ?? selectedTable?.mappedModelId;
|
|
955
|
+
if (!targetModelId) return;
|
|
956
|
+
window.open(`/admin/core/solid-core/model-metadata/form/${targetModelId}`, "_blank", "noopener,noreferrer");
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
const viewChanges = async () => {
|
|
960
|
+
if (!drawerDraft || !bootstrap?.module?.id) return;
|
|
961
|
+
|
|
962
|
+
const validationErrors = getDrawerValidationErrors(drawerDraft);
|
|
963
|
+
if (validationErrors.length) {
|
|
964
|
+
validationErrors.forEach((detail) => {
|
|
965
|
+
dispatch(showToast({ severity: "error", summary: "Validation", detail }));
|
|
966
|
+
});
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
try {
|
|
971
|
+
const preview = await previewMapping({
|
|
972
|
+
moduleId: bootstrap.module.id,
|
|
973
|
+
payload: buildMappingRequest(drawerDraft),
|
|
974
|
+
}).unwrap();
|
|
975
|
+
|
|
976
|
+
setMappingPreview(preview);
|
|
977
|
+
setReviewedMetadataJsonValue(preview.metadataJson.model);
|
|
978
|
+
setMetadataJsonText(JSON.stringify(preview.metadataJson.model, null, 2));
|
|
979
|
+
setMetadataJsonValidationError(null);
|
|
980
|
+
setMetadataJsonResetToken(`preview-${preview.metadataJson.filePath}-${Date.now()}`);
|
|
981
|
+
setReviewTab("metadata-json");
|
|
982
|
+
setDrawerStep("review");
|
|
983
|
+
} catch (error: any) {
|
|
984
|
+
dispatch(showToast({
|
|
985
|
+
severity: "error",
|
|
986
|
+
summary: "Preview failed",
|
|
987
|
+
detail: readQueryError(error),
|
|
988
|
+
}));
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
const saveMapping = async () => {
|
|
993
|
+
if (!drawerDraft || !bootstrap?.module?.id) return;
|
|
994
|
+
|
|
995
|
+
const validationErrors = getDrawerValidationErrors(drawerDraft);
|
|
996
|
+
if (validationErrors.length) {
|
|
997
|
+
validationErrors.forEach((detail) => {
|
|
998
|
+
dispatch(showToast({ severity: "error", summary: "Validation", detail }));
|
|
999
|
+
});
|
|
1000
|
+
setDrawerStep("edit");
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const reviewedModelValidation = parseAndValidateReviewedModelJson(metadataJsonText, drawerDraft);
|
|
1005
|
+
if (reviewedModelValidation.error || !reviewedModelValidation.parsed) {
|
|
1006
|
+
setMetadataJsonValidationError(reviewedModelValidation.error ?? "Metadata JSON is invalid.");
|
|
1007
|
+
setReviewTab("metadata-json");
|
|
1008
|
+
setDrawerStep("review");
|
|
1009
|
+
dispatch(showToast({
|
|
1010
|
+
severity: "error",
|
|
1011
|
+
summary: "Validation",
|
|
1012
|
+
detail: reviewedModelValidation.error ?? "Metadata JSON is invalid.",
|
|
1013
|
+
}));
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
setMetadataJsonValidationError(null);
|
|
1018
|
+
|
|
1019
|
+
try {
|
|
1020
|
+
const response = await applyMapping({
|
|
1021
|
+
moduleId: bootstrap.module.id,
|
|
1022
|
+
payload: {
|
|
1023
|
+
...buildMappingRequest(drawerDraft, mappingPreview?.migration.timestamp ?? null),
|
|
1024
|
+
reviewedModel: reviewedModelValidation.parsed,
|
|
1025
|
+
},
|
|
1026
|
+
}).unwrap();
|
|
1027
|
+
|
|
1028
|
+
dispatch(showToast({
|
|
1029
|
+
severity: "success",
|
|
1030
|
+
summary: "Saved",
|
|
1031
|
+
detail: response.migration.written
|
|
1032
|
+
? "The mapping, metadata JSON, and migration template were written successfully."
|
|
1033
|
+
: drawerDraft.mapped
|
|
1034
|
+
? "The model mapping was updated and written back to metadata JSON."
|
|
1035
|
+
: "The model mapping was created and written to metadata JSON.",
|
|
1036
|
+
}));
|
|
1037
|
+
|
|
1038
|
+
setDrawerOpen(false);
|
|
1039
|
+
setDrawerDraft(null);
|
|
1040
|
+
setMappingPreview(null);
|
|
1041
|
+
setReviewedMetadataJsonValue(null);
|
|
1042
|
+
setMetadataJsonText("");
|
|
1043
|
+
setMetadataJsonValidationError(null);
|
|
1044
|
+
setMetadataJsonResetToken(`saved-${Date.now()}`);
|
|
1045
|
+
setDrawerStep("edit");
|
|
1046
|
+
await refetchTables();
|
|
1047
|
+
await refetchBootstrap();
|
|
1048
|
+
} catch (error: any) {
|
|
1049
|
+
dispatch(showToast({
|
|
1050
|
+
severity: "error",
|
|
1051
|
+
summary: "Save failed",
|
|
1052
|
+
detail: readQueryError(error),
|
|
1053
|
+
}));
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
if (!isValidModuleId) {
|
|
1058
|
+
return (
|
|
1059
|
+
<div className="sdix-page">
|
|
1060
|
+
<div className="sdix-empty-state">
|
|
1061
|
+
<ShieldAlert size={24} />
|
|
1062
|
+
<h2>Module not found</h2>
|
|
1063
|
+
<p>Open datasource introspection from a saved module so SolidX knows which module should receive the mapped models.</p>
|
|
1064
|
+
</div>
|
|
1065
|
+
</div>
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const selectableUnmappedColumns = drawerDraft?.columns.filter(
|
|
1070
|
+
(column) => !column.handledBySuperclass && !column.matched && !column.isPrimaryKey,
|
|
1071
|
+
) ?? [];
|
|
1072
|
+
const allSelectableUnmappedIncluded = selectableUnmappedColumns.length > 0
|
|
1073
|
+
&& selectableUnmappedColumns.every((column) => column.include);
|
|
1074
|
+
const drawerRawDdl = tableDetail?.rawDdl ?? null;
|
|
1075
|
+
const drawerTabs = drawerDraft ? [
|
|
1076
|
+
{
|
|
1077
|
+
value: "general-info",
|
|
1078
|
+
label: "General Info",
|
|
1079
|
+
content: (
|
|
1080
|
+
<div className="sdix-drawer-form">
|
|
1081
|
+
<section className="sdix-form-section">
|
|
1082
|
+
<h4 className="solid-form-layout-label">Model Info</h4>
|
|
1083
|
+
<div className="sdix-form-grid">
|
|
1084
|
+
<div className="sdix-form-field">
|
|
1085
|
+
<label htmlFor="sdix-singular-name">Singular Name</label>
|
|
1086
|
+
<SolidInput
|
|
1087
|
+
id="sdix-singular-name"
|
|
1088
|
+
value={drawerDraft.singularName}
|
|
1089
|
+
onChange={(event) => updateGeneralField("singularName", event.target.value)}
|
|
1090
|
+
/>
|
|
1091
|
+
</div>
|
|
1092
|
+
<div className="sdix-form-field">
|
|
1093
|
+
<label htmlFor="sdix-plural-name">Plural Name</label>
|
|
1094
|
+
<SolidInput
|
|
1095
|
+
id="sdix-plural-name"
|
|
1096
|
+
value={drawerDraft.pluralName}
|
|
1097
|
+
onChange={(event) => updateGeneralField("pluralName", event.target.value)}
|
|
1098
|
+
/>
|
|
1099
|
+
</div>
|
|
1100
|
+
<div className="sdix-form-field">
|
|
1101
|
+
<label htmlFor="sdix-display-name">Display Name</label>
|
|
1102
|
+
<SolidInput
|
|
1103
|
+
id="sdix-display-name"
|
|
1104
|
+
value={drawerDraft.displayName}
|
|
1105
|
+
onChange={(event) => updateGeneralField("displayName", event.target.value)}
|
|
1106
|
+
/>
|
|
1107
|
+
</div>
|
|
1108
|
+
<div className="sdix-form-field">
|
|
1109
|
+
<label htmlFor="sdix-user-key-field">User Key Field</label>
|
|
1110
|
+
<SolidSelect
|
|
1111
|
+
id="sdix-user-key-field"
|
|
1112
|
+
value={drawerDraft.userKeyField}
|
|
1113
|
+
options={drawerUserKeyOptions}
|
|
1114
|
+
placeholder="Select user key field"
|
|
1115
|
+
onChange={(event) => changeUserKeyField(event.value)}
|
|
1116
|
+
/>
|
|
1117
|
+
</div>
|
|
1118
|
+
<div className="sdix-form-field sdix-form-field--full">
|
|
1119
|
+
<label htmlFor="sdix-description">Description</label>
|
|
1120
|
+
<SolidTextarea
|
|
1121
|
+
id="sdix-description"
|
|
1122
|
+
rows={3}
|
|
1123
|
+
value={drawerDraft.description}
|
|
1124
|
+
onChange={(event) => updateGeneralField("description", event.target.value)}
|
|
1125
|
+
/>
|
|
1126
|
+
</div>
|
|
1127
|
+
</div>
|
|
1128
|
+
</section>
|
|
1129
|
+
|
|
1130
|
+
<section className="sdix-form-section">
|
|
1131
|
+
<h4 className="solid-form-layout-label">Datasource Binding</h4>
|
|
1132
|
+
<div className="sdix-form-grid">
|
|
1133
|
+
<div className="sdix-form-field">
|
|
1134
|
+
<label htmlFor="sdix-table-name">Table Name</label>
|
|
1135
|
+
<SolidInput
|
|
1136
|
+
id="sdix-table-name"
|
|
1137
|
+
value={drawerDraft.tableName}
|
|
1138
|
+
onChange={(event) => updateGeneralField("tableName", event.target.value)}
|
|
1139
|
+
/>
|
|
1140
|
+
</div>
|
|
1141
|
+
<div className="sdix-form-field">
|
|
1142
|
+
<label htmlFor="sdix-schema-name">Schema</label>
|
|
1143
|
+
<SolidInput
|
|
1144
|
+
id="sdix-schema-name"
|
|
1145
|
+
value={drawerDraft.schema ?? "default"}
|
|
1146
|
+
disabled
|
|
1147
|
+
/>
|
|
1148
|
+
</div>
|
|
1149
|
+
<div className="sdix-form-field">
|
|
1150
|
+
<label htmlFor="sdix-datasource-name">Datasource</label>
|
|
1151
|
+
<SolidInput
|
|
1152
|
+
id="sdix-datasource-name"
|
|
1153
|
+
value={drawerDraft.dataSource}
|
|
1154
|
+
disabled
|
|
1155
|
+
/>
|
|
1156
|
+
</div>
|
|
1157
|
+
<div className="sdix-form-field">
|
|
1158
|
+
<label htmlFor="sdix-datasource-type">Provider</label>
|
|
1159
|
+
<SolidInput
|
|
1160
|
+
id="sdix-datasource-type"
|
|
1161
|
+
value={providerLabel(drawerDraft.dataSourceType)}
|
|
1162
|
+
disabled
|
|
1163
|
+
/>
|
|
1164
|
+
</div>
|
|
1165
|
+
<div className="sdix-form-field">
|
|
1166
|
+
<label htmlFor="sdix-legacy-type">Legacy Strategy</label>
|
|
1167
|
+
<SolidInput
|
|
1168
|
+
id="sdix-legacy-type"
|
|
1169
|
+
value={legacyTypeLabel(drawerDraft.legacyTableType)}
|
|
1170
|
+
disabled
|
|
1171
|
+
/>
|
|
1172
|
+
</div>
|
|
1173
|
+
<div className="sdix-form-field">
|
|
1174
|
+
<label htmlFor="sdix-base-class">Superclass</label>
|
|
1175
|
+
<SolidInput
|
|
1176
|
+
id="sdix-base-class"
|
|
1177
|
+
value={drawerDraft.baseClassName}
|
|
1178
|
+
disabled
|
|
1179
|
+
/>
|
|
1180
|
+
</div>
|
|
1181
|
+
</div>
|
|
1182
|
+
</section>
|
|
1183
|
+
</div>
|
|
1184
|
+
),
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
value: "columns-mapping",
|
|
1188
|
+
label: "Columns Mapping",
|
|
1189
|
+
content: (
|
|
1190
|
+
<div className="sdix-columns">
|
|
1191
|
+
<div className="sdix-columns__toolbar">
|
|
1192
|
+
<div className="sdix-columns__legend" aria-label="Column legend">
|
|
1193
|
+
<span className="sdix-status-badge is-superclass">System column</span>
|
|
1194
|
+
<span className="sdix-status-badge is-muted">Application column</span>
|
|
1195
|
+
</div>
|
|
1196
|
+
</div>
|
|
1197
|
+
<div className="sdix-column-table">
|
|
1198
|
+
<div className="sdix-column-row sdix-column-row--head sdix-column-row--mapping">
|
|
1199
|
+
<span>Column</span>
|
|
1200
|
+
<span>Field</span>
|
|
1201
|
+
<span>Database</span>
|
|
1202
|
+
<span>SolidX</span>
|
|
1203
|
+
<span className="sdix-column-select-header">
|
|
1204
|
+
{selectableUnmappedColumns.length ? (
|
|
1205
|
+
<SolidCheckbox
|
|
1206
|
+
checked={allSelectableUnmappedIncluded}
|
|
1207
|
+
onChange={(event) => setUnmappedColumnsIncluded(event.target.checked)}
|
|
1208
|
+
aria-label={allSelectableUnmappedIncluded ? "Deselect all unmapped columns" : "Select all unmapped columns"}
|
|
1209
|
+
/>
|
|
1210
|
+
) : (
|
|
1211
|
+
"Include"
|
|
1212
|
+
)}
|
|
1213
|
+
</span>
|
|
1214
|
+
<span>Status</span>
|
|
1215
|
+
<span>Action</span>
|
|
1216
|
+
</div>
|
|
1217
|
+
{drawerDraft.columns.map((column) => {
|
|
1218
|
+
const isUserKey = drawerDraft.userKeyField === column.fieldName;
|
|
1219
|
+
const statusClassName = column.handledBySuperclass
|
|
1220
|
+
? "is-superclass"
|
|
1221
|
+
: column.include
|
|
1222
|
+
? column.matched
|
|
1223
|
+
? "is-mapped"
|
|
1224
|
+
: "is-unmapped"
|
|
1225
|
+
: "is-muted";
|
|
1226
|
+
|
|
1227
|
+
const statusText = column.handledBySuperclass
|
|
1228
|
+
? "System"
|
|
1229
|
+
: column.include
|
|
1230
|
+
? column.matched
|
|
1231
|
+
? "Mapped"
|
|
1232
|
+
: "Queued for save"
|
|
1233
|
+
: column.matched
|
|
1234
|
+
? "Currently omitted"
|
|
1235
|
+
: "Skipped for now";
|
|
1236
|
+
|
|
1237
|
+
return (
|
|
1238
|
+
<div key={column.columnName} className="sdix-column-row sdix-column-row--mapping">
|
|
1239
|
+
<span className="sdix-column-meta">
|
|
1240
|
+
<strong>{column.columnName}</strong>
|
|
1241
|
+
{column.isAutoGenerated ? <small>Generated id column</small> : null}
|
|
1242
|
+
</span>
|
|
1243
|
+
<span>
|
|
1244
|
+
<SolidInput
|
|
1245
|
+
value={column.fieldName}
|
|
1246
|
+
disabled={column.handledBySuperclass}
|
|
1247
|
+
onChange={(event) => updateColumnFieldName(column.columnName, event.target.value)}
|
|
1248
|
+
/>
|
|
1249
|
+
{isUserKey ? <small className="sdix-inline-note">User key</small> : null}
|
|
1250
|
+
</span>
|
|
1251
|
+
<span className="sdix-column-meta">
|
|
1252
|
+
<strong>{column.dataType}</strong>
|
|
1253
|
+
</span>
|
|
1254
|
+
<span>
|
|
1255
|
+
{column.handledBySuperclass ? (
|
|
1256
|
+
<span>{column.solidFieldType}</span>
|
|
1257
|
+
) : (
|
|
1258
|
+
<SolidSelect
|
|
1259
|
+
value={column.solidFieldType}
|
|
1260
|
+
options={scalarFieldTypeOptions}
|
|
1261
|
+
onChange={(event) => updateColumnSolidFieldType(column.columnName, event.value)}
|
|
1262
|
+
/>
|
|
1263
|
+
)}
|
|
1264
|
+
</span>
|
|
1265
|
+
<span className="sdix-column-include">
|
|
1266
|
+
{column.handledBySuperclass ? (
|
|
1267
|
+
<span className="sdix-column-placeholder">-</span>
|
|
1268
|
+
) : column.matched ? (
|
|
1269
|
+
<span className="sdix-column-placeholder">-</span>
|
|
1270
|
+
) : column.isPrimaryKey ? (
|
|
1271
|
+
<SolidCheckbox checked disabled label="Required" />
|
|
1272
|
+
) : (
|
|
1273
|
+
<SolidCheckbox
|
|
1274
|
+
checked={column.include}
|
|
1275
|
+
onChange={(event) => setColumnIncluded(column.columnName, event.target.checked)}
|
|
1276
|
+
/>
|
|
1277
|
+
)}
|
|
1278
|
+
</span>
|
|
1279
|
+
<span>
|
|
1280
|
+
<span className={`sdix-status-badge ${statusClassName}`}>{statusText}</span>
|
|
1281
|
+
</span>
|
|
1282
|
+
<span className="sdix-column-action">
|
|
1283
|
+
{column.handledBySuperclass ? (
|
|
1284
|
+
<SolidButton size="small" variant="outline" disabled>
|
|
1285
|
+
Managed
|
|
1286
|
+
</SolidButton>
|
|
1287
|
+
) : column.isPrimaryKey ? (
|
|
1288
|
+
<SolidButton size="small" variant="outline" disabled>
|
|
1289
|
+
Required
|
|
1290
|
+
</SolidButton>
|
|
1291
|
+
) : column.matched ? (
|
|
1292
|
+
<SolidButton size="small" variant="outline" onClick={() => setColumnIncluded(column.columnName, false)}>
|
|
1293
|
+
Remove mapping
|
|
1294
|
+
</SolidButton>
|
|
1295
|
+
) : (
|
|
1296
|
+
<span className="sdix-column-placeholder">-</span>
|
|
1297
|
+
)}
|
|
1298
|
+
</span>
|
|
1299
|
+
</div>
|
|
1300
|
+
);
|
|
1301
|
+
})}
|
|
1302
|
+
</div>
|
|
1303
|
+
</div>
|
|
1304
|
+
),
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
value: "raw-ddl",
|
|
1308
|
+
label: "Raw DDL",
|
|
1309
|
+
content: (
|
|
1310
|
+
<div className="sdix-ddl-tab">
|
|
1311
|
+
{drawerRawDdl?.available && drawerRawDdl.statement ? (
|
|
1312
|
+
<pre className="sdix-ddl-code"><code>{drawerRawDdl.statement}</code></pre>
|
|
1313
|
+
) : (
|
|
1314
|
+
<div className="sdix-empty-state is-inline sdix-ddl-empty">
|
|
1315
|
+
<Database size={18} />
|
|
1316
|
+
<p>{drawerRawDdl?.note || "DDL preview is not available for this table."}</p>
|
|
1317
|
+
</div>
|
|
1318
|
+
)}
|
|
1319
|
+
</div>
|
|
1320
|
+
),
|
|
1321
|
+
},
|
|
1322
|
+
] : [];
|
|
1323
|
+
|
|
1324
|
+
const reviewTabs = mappingPreview ? [
|
|
1325
|
+
{
|
|
1326
|
+
value: "metadata-json",
|
|
1327
|
+
label: "Metadata JSON",
|
|
1328
|
+
content: (
|
|
1329
|
+
<div className="sdix-review-panel sdix-review-panel--metadata">
|
|
1330
|
+
<div className="sdix-review-meta sdix-review-meta--compact">
|
|
1331
|
+
<span>Target file</span>
|
|
1332
|
+
<strong>{mappingPreview.metadataJson.filePath}</strong>
|
|
1333
|
+
</div>
|
|
1334
|
+
<SolidJsonEditor
|
|
1335
|
+
className="sdix-json-editor"
|
|
1336
|
+
value={reviewedMetadataJsonValue ?? mappingPreview.metadataJson.model}
|
|
1337
|
+
resetToken={metadataJsonResetToken}
|
|
1338
|
+
onValueChange={(nextValue) => {
|
|
1339
|
+
setReviewedMetadataJsonValue(nextValue);
|
|
1340
|
+
if (metadataJsonValidationError) {
|
|
1341
|
+
setMetadataJsonValidationError(null);
|
|
1342
|
+
}
|
|
1343
|
+
}}
|
|
1344
|
+
onTextChange={(nextText) => {
|
|
1345
|
+
setMetadataJsonText(nextText);
|
|
1346
|
+
}}
|
|
1347
|
+
onErrorChange={setMetadataJsonValidationError}
|
|
1348
|
+
/>
|
|
1349
|
+
{metadataJsonValidationError ? (
|
|
1350
|
+
<div className="sdix-json-validation-error">
|
|
1351
|
+
<span>{metadataJsonValidationError}</span>
|
|
1352
|
+
</div>
|
|
1353
|
+
) : null}
|
|
1354
|
+
</div>
|
|
1355
|
+
),
|
|
1356
|
+
},
|
|
1357
|
+
{
|
|
1358
|
+
value: "migration",
|
|
1359
|
+
label: "Migration",
|
|
1360
|
+
content: (
|
|
1361
|
+
<div className="sdix-review-panel sdix-review-panel--migration">
|
|
1362
|
+
{mappingPreview.migration.willGenerate && mappingPreview.migration.filePath ? (
|
|
1363
|
+
<>
|
|
1364
|
+
<div className="sdix-review-meta sdix-review-meta--compact">
|
|
1365
|
+
<span>Target file</span>
|
|
1366
|
+
<strong>{mappingPreview.migration.filePath}</strong>
|
|
1367
|
+
</div>
|
|
1368
|
+
<p className="sdix-review-note">
|
|
1369
|
+
This migration preview is generated from the current mapping and is read-only here. Saving the mapping will write a new template migration file to the target location.
|
|
1370
|
+
</p>
|
|
1371
|
+
{mappingPreview.migration.content ? (
|
|
1372
|
+
<pre className="sdix-ddl-code sdix-ddl-code--fill"><code>{mappingPreview.migration.content}</code></pre>
|
|
1373
|
+
) : null}
|
|
1374
|
+
</>
|
|
1375
|
+
) : (
|
|
1376
|
+
<div className="sdix-empty-state is-inline sdix-ddl-empty">
|
|
1377
|
+
<Database size={18} />
|
|
1378
|
+
<p>{mappingPreview.migration.note}</p>
|
|
1379
|
+
</div>
|
|
1380
|
+
)}
|
|
1381
|
+
</div>
|
|
1382
|
+
),
|
|
1383
|
+
},
|
|
1384
|
+
] : [];
|
|
1385
|
+
|
|
1386
|
+
return (
|
|
1387
|
+
<div className="sdix-page">
|
|
1388
|
+
<div className="sdix-toolbar">
|
|
1389
|
+
<div className="sdix-toolbar__left">
|
|
1390
|
+
<div className="sdix-toolbar-search">
|
|
1391
|
+
<Search size={15} />
|
|
1392
|
+
<SolidInput
|
|
1393
|
+
value={tableSearch}
|
|
1394
|
+
onChange={(event) => setTableSearch(event.target.value)}
|
|
1395
|
+
placeholder="Search tables"
|
|
1396
|
+
disabled={!selectedDatasource}
|
|
1397
|
+
/>
|
|
1398
|
+
</div>
|
|
1399
|
+
</div>
|
|
1400
|
+
|
|
1401
|
+
<div className="sdix-toolbar__actions">
|
|
1402
|
+
{isBootstrapFetching || isTablesFetching || isTableDetailFetching || isMappedModelFetching ? (
|
|
1403
|
+
<div className="sdix-inline-status">
|
|
1404
|
+
<SolidSpinner size={16} />
|
|
1405
|
+
<span>Refreshing</span>
|
|
1406
|
+
</div>
|
|
1407
|
+
) : null}
|
|
1408
|
+
<SolidSelect
|
|
1409
|
+
value={selectedDatasource}
|
|
1410
|
+
options={datasourceOptions}
|
|
1411
|
+
placeholder="Datasource"
|
|
1412
|
+
className="sdix-toolbar-select sdix-toolbar-select--compact"
|
|
1413
|
+
onChange={(event) => setSelectedDatasource(event.value)}
|
|
1414
|
+
/>
|
|
1415
|
+
<SolidButton
|
|
1416
|
+
size="small"
|
|
1417
|
+
className="sdix-icon-button"
|
|
1418
|
+
leftIcon={<RefreshCw size={16} />}
|
|
1419
|
+
onClick={() => void handleRefresh()}
|
|
1420
|
+
tooltip="Refresh"
|
|
1421
|
+
aria-label="Refresh"
|
|
1422
|
+
/>
|
|
1423
|
+
<SolidButton
|
|
1424
|
+
size="small"
|
|
1425
|
+
className="sdix-icon-button"
|
|
1426
|
+
leftIcon={<Cog size={16} />}
|
|
1427
|
+
onClick={() => {
|
|
1428
|
+
setDraftSessionSettings(sessionSettings);
|
|
1429
|
+
setSettingsTab("datasource-info");
|
|
1430
|
+
setSettingsOpen(true);
|
|
1431
|
+
}}
|
|
1432
|
+
tooltip="Session settings"
|
|
1433
|
+
aria-label="Session settings"
|
|
1434
|
+
/>
|
|
1435
|
+
</div>
|
|
1436
|
+
</div>
|
|
1437
|
+
|
|
1438
|
+
{isBootstrapLoading ? (
|
|
1439
|
+
<div className="sdix-loading">
|
|
1440
|
+
<SolidSpinner size={28} />
|
|
1441
|
+
<p>Loading introspection workspace...</p>
|
|
1442
|
+
</div>
|
|
1443
|
+
) : bootstrapError ? (
|
|
1444
|
+
<div className="sdix-error">
|
|
1445
|
+
<span>{readQueryError(bootstrapError)}</span>
|
|
1446
|
+
<SolidButton size="small" variant="outline" onClick={() => void refetchBootstrap()}>
|
|
1447
|
+
Retry
|
|
1448
|
+
</SolidButton>
|
|
1449
|
+
</div>
|
|
1450
|
+
) : (
|
|
1451
|
+
<>
|
|
1452
|
+
{selectedDatasource && tablesResponse?.synchronizeBlocked ? (
|
|
1453
|
+
<div className="sdix-blocker">
|
|
1454
|
+
<ShieldAlert size={20} />
|
|
1455
|
+
<div>
|
|
1456
|
+
<h3>Synchronize must be disabled before introspection</h3>
|
|
1457
|
+
<p>{tablesResponse.synchronizeMessage}</p>
|
|
1458
|
+
</div>
|
|
1459
|
+
</div>
|
|
1460
|
+
) : null}
|
|
1461
|
+
|
|
1462
|
+
<div className="sdix-workbench">
|
|
1463
|
+
<div className="sdix-table-shell">
|
|
1464
|
+
{!selectedDatasource ? (
|
|
1465
|
+
<div className="sdix-empty-state is-inline">
|
|
1466
|
+
<Database size={22} />
|
|
1467
|
+
<p>Select a datasource to load its tables.</p>
|
|
1468
|
+
</div>
|
|
1469
|
+
) : isTablesLoading ? (
|
|
1470
|
+
<div className="sdix-loading is-inline">
|
|
1471
|
+
<SolidSpinner size={24} />
|
|
1472
|
+
<p>Inspecting datasource tables...</p>
|
|
1473
|
+
</div>
|
|
1474
|
+
) : tablesError ? (
|
|
1475
|
+
<div className="sdix-error is-inline">
|
|
1476
|
+
<span>{readQueryError(tablesError)}</span>
|
|
1477
|
+
<SolidButton size="small" variant="outline" onClick={() => void refetchTables()}>
|
|
1478
|
+
Retry
|
|
1479
|
+
</SolidButton>
|
|
1480
|
+
</div>
|
|
1481
|
+
) : !filteredRecords.length ? (
|
|
1482
|
+
<div className="sdix-empty-state is-inline">
|
|
1483
|
+
<Database size={22} />
|
|
1484
|
+
<p>{tableSearch ? "No tables match the current quick filter." : "No database tables were discovered for this datasource."}</p>
|
|
1485
|
+
</div>
|
|
1486
|
+
) : (
|
|
1487
|
+
<div className="sdix-table-area">
|
|
1488
|
+
<SolidDataTable
|
|
1489
|
+
value={filteredRecords}
|
|
1490
|
+
dataKey="tableName"
|
|
1491
|
+
viewportHeight="calc(100vh - 162px)"
|
|
1492
|
+
size="small"
|
|
1493
|
+
onRowClick={({ data }) => openTableDrawer(data)}
|
|
1494
|
+
rowClassName={(rowData) => `${rowData.schema ?? ""}::${rowData.tableName}` === selectedTableKey && drawerOpen ? "sdix-table-row-selected" : ""}
|
|
1495
|
+
emptyMessage="No datasource tables found"
|
|
1496
|
+
>
|
|
1497
|
+
<Column
|
|
1498
|
+
field="tableName"
|
|
1499
|
+
header="Table"
|
|
1500
|
+
body={(rowData: DatasourceIntrospectionTableRecord) => (
|
|
1501
|
+
<div className="sdix-table-primary-cell">
|
|
1502
|
+
<strong>{rowData.tableName}</strong>
|
|
1503
|
+
</div>
|
|
1504
|
+
)}
|
|
1505
|
+
style={{ minWidth: 240 }}
|
|
1506
|
+
/>
|
|
1507
|
+
<Column field="schema" header="Schema" body={(rowData: DatasourceIntrospectionTableRecord) => rowData.schema || "dbo/default"} style={{ minWidth: 120 }} />
|
|
1508
|
+
<Column
|
|
1509
|
+
header="Status"
|
|
1510
|
+
body={(rowData: DatasourceIntrospectionTableRecord) => (
|
|
1511
|
+
<span
|
|
1512
|
+
className={`sdix-status-badge ${rowData.mapped ? "is-mapped" : "is-unmapped"}`}
|
|
1513
|
+
>
|
|
1514
|
+
{statusLabel(rowData)}
|
|
1515
|
+
</span>
|
|
1516
|
+
)}
|
|
1517
|
+
style={{ minWidth: 150 }}
|
|
1518
|
+
/>
|
|
1519
|
+
<Column
|
|
1520
|
+
header="Superclass"
|
|
1521
|
+
body={(rowData: DatasourceIntrospectionTableRecord) => rowData.suggestedBaseClassName}
|
|
1522
|
+
style={{ minWidth: 220 }}
|
|
1523
|
+
/>
|
|
1524
|
+
<Column field="suggestedUserKeyField" header="User Key" style={{ minWidth: 160 }} />
|
|
1525
|
+
<Column
|
|
1526
|
+
header="Mapped Model"
|
|
1527
|
+
body={(rowData: DatasourceIntrospectionTableRecord) => (
|
|
1528
|
+
rowData.mapped
|
|
1529
|
+
? (rowData.mappedModelDisplayName || rowData.mappedModelSingularName || "-")
|
|
1530
|
+
: <span className="sdix-muted-cell">Not mapped</span>
|
|
1531
|
+
)}
|
|
1532
|
+
style={{ minWidth: 200 }}
|
|
1533
|
+
/>
|
|
1534
|
+
<Column
|
|
1535
|
+
header="System Columns"
|
|
1536
|
+
body={(rowData: DatasourceIntrospectionTableRecord) => Math.max(rowData.columnCount - rowData.physicalColumnCount, 0)}
|
|
1537
|
+
style={{ minWidth: 130 }}
|
|
1538
|
+
/>
|
|
1539
|
+
<Column
|
|
1540
|
+
header="Columns"
|
|
1541
|
+
body={(rowData: DatasourceIntrospectionTableRecord) => rowData.physicalColumnCount}
|
|
1542
|
+
style={{ minWidth: 110 }}
|
|
1543
|
+
/>
|
|
1544
|
+
</SolidDataTable>
|
|
1545
|
+
</div>
|
|
1546
|
+
)}
|
|
1547
|
+
</div>
|
|
1548
|
+
</div>
|
|
1549
|
+
|
|
1550
|
+
<SolidDialog
|
|
1551
|
+
open={settingsOpen}
|
|
1552
|
+
onOpenChange={setSettingsOpen}
|
|
1553
|
+
style={{ width: "min(560px, calc(100vw - 32px))" }}
|
|
1554
|
+
>
|
|
1555
|
+
<SolidDialogHeader>
|
|
1556
|
+
<div>
|
|
1557
|
+
<SolidDialogTitle>Session settings</SolidDialogTitle>
|
|
1558
|
+
<SolidDialogDescription>
|
|
1559
|
+
Review the selected datasource and adjust the session-specific introspection flags.
|
|
1560
|
+
</SolidDialogDescription>
|
|
1561
|
+
</div>
|
|
1562
|
+
<SolidDialogClose />
|
|
1563
|
+
</SolidDialogHeader>
|
|
1564
|
+
<SolidDialogSeparator />
|
|
1565
|
+
<SolidDialogBody>
|
|
1566
|
+
<SolidTabGroup
|
|
1567
|
+
tabs={settingsTabs}
|
|
1568
|
+
value={settingsTab}
|
|
1569
|
+
onValueChange={setSettingsTab}
|
|
1570
|
+
className="sdix-settings-tabs"
|
|
1571
|
+
panelClassName="sdix-settings-tab-panel"
|
|
1572
|
+
/>
|
|
1573
|
+
</SolidDialogBody>
|
|
1574
|
+
<SolidDialogFooter>
|
|
1575
|
+
<SolidButton variant="outline" onClick={() => setSettingsOpen(false)}>
|
|
1576
|
+
Cancel
|
|
1577
|
+
</SolidButton>
|
|
1578
|
+
<SolidButton onClick={applyDraftSettings}>
|
|
1579
|
+
Apply
|
|
1580
|
+
</SolidButton>
|
|
1581
|
+
</SolidDialogFooter>
|
|
1582
|
+
</SolidDialog>
|
|
1583
|
+
|
|
1584
|
+
<SolidDialog
|
|
1585
|
+
open={drawerOpen}
|
|
1586
|
+
onOpenChange={setDrawerOpen}
|
|
1587
|
+
className="sdix-drawer"
|
|
1588
|
+
overlayClassName="sdix-drawer-overlay"
|
|
1589
|
+
dismissible
|
|
1590
|
+
>
|
|
1591
|
+
<SolidDialogHeader className="sdix-drawer-header">
|
|
1592
|
+
<div className="sdix-drawer-header__identity">
|
|
1593
|
+
<SolidDialogTitle>{selectedTable?.tableName || "Table details"}</SolidDialogTitle>
|
|
1594
|
+
<SolidDialogDescription className="sdix-drawer-header__meta">
|
|
1595
|
+
{selectedTable
|
|
1596
|
+
? `${selectedTable.schema || "default"} · ${selectedDatasourceRecord?.database || selectedDatasourceRecord?.displayName || "Datasource"}${selectedDatasourceRecord ? ` · ${providerLabel(selectedDatasourceRecord.type)}` : ""}`
|
|
1597
|
+
: "Review mapping details"}
|
|
1598
|
+
</SolidDialogDescription>
|
|
1599
|
+
</div>
|
|
1600
|
+
<SolidDialogClose />
|
|
1601
|
+
</SolidDialogHeader>
|
|
1602
|
+
<SolidDialogSeparator />
|
|
1603
|
+
<SolidDialogBody className="sdix-drawer-body">
|
|
1604
|
+
{!selectedTable ? (
|
|
1605
|
+
<div className="sdix-empty-state is-inline">
|
|
1606
|
+
<Sparkles size={22} />
|
|
1607
|
+
<p>Select a table row to review its structure.</p>
|
|
1608
|
+
</div>
|
|
1609
|
+
) : drawerLoadingError ? (
|
|
1610
|
+
<div className="sdix-error is-inline">
|
|
1611
|
+
<span>{readQueryError(drawerLoadingError)}</span>
|
|
1612
|
+
<SolidButton size="small" variant="outline" onClick={() => void refetchTableDetail()}>
|
|
1613
|
+
Retry
|
|
1614
|
+
</SolidButton>
|
|
1615
|
+
</div>
|
|
1616
|
+
) : isDrawerBlockingLoading ? (
|
|
1617
|
+
<div className="sdix-loading is-inline">
|
|
1618
|
+
<SolidSpinner size={24} />
|
|
1619
|
+
<p>Preparing column mapping preview...</p>
|
|
1620
|
+
</div>
|
|
1621
|
+
) : (
|
|
1622
|
+
<div className="sdix-drawer-stack">
|
|
1623
|
+
{drawerDraft && !drawerDraft.mapped && drawerStep === "edit" ? (
|
|
1624
|
+
<div className="sdix-detail-callout sdix-system-fields-callout">
|
|
1625
|
+
<h3>SolidX system fields are added later</h3>
|
|
1626
|
+
<p>
|
|
1627
|
+
This preview only lists the legacy table columns that already exist today.
|
|
1628
|
+
{" "}
|
|
1629
|
+
Framework system fields from the selected superclass, including the namespaced
|
|
1630
|
+
{" "}
|
|
1631
|
+
<code>ss_*</code>
|
|
1632
|
+
{" "}
|
|
1633
|
+
columns, are introduced when you save the mapping and then generate/apply the follow-up migrations.
|
|
1634
|
+
</p>
|
|
1635
|
+
</div>
|
|
1636
|
+
) : null}
|
|
1637
|
+
{drawerStep === "edit" ? (
|
|
1638
|
+
<SolidTabGroup
|
|
1639
|
+
tabs={drawerTabs}
|
|
1640
|
+
value={drawerTab}
|
|
1641
|
+
onValueChange={setDrawerTab}
|
|
1642
|
+
className="sdix-drawer-tabs"
|
|
1643
|
+
panelClassName="sdix-drawer-tab-panel"
|
|
1644
|
+
/>
|
|
1645
|
+
) : (
|
|
1646
|
+
<SolidTabGroup
|
|
1647
|
+
tabs={reviewTabs}
|
|
1648
|
+
value={reviewTab}
|
|
1649
|
+
onValueChange={setReviewTab}
|
|
1650
|
+
className="sdix-drawer-tabs"
|
|
1651
|
+
panelClassName="sdix-drawer-tab-panel"
|
|
1652
|
+
/>
|
|
1653
|
+
)}
|
|
1654
|
+
</div>
|
|
1655
|
+
)}
|
|
1656
|
+
</SolidDialogBody>
|
|
1657
|
+
{drawerDraft && !isDrawerBlockingLoading ? (
|
|
1658
|
+
<>
|
|
1659
|
+
<SolidDialogSeparator />
|
|
1660
|
+
<SolidDialogFooter className="sdix-drawer-footer">
|
|
1661
|
+
<div className="sdix-drawer-footer__meta">
|
|
1662
|
+
{drawerStep === "review" && mappingPreview ? (
|
|
1663
|
+
<span>
|
|
1664
|
+
Review the metadata update first, then save to apply
|
|
1665
|
+
{" "}
|
|
1666
|
+
<strong>{mappingPreview.summary.includedColumnNames.length}</strong>
|
|
1667
|
+
{" "}
|
|
1668
|
+
mapped columns
|
|
1669
|
+
{mappingPreview.summary.generatedSystemColumnCount
|
|
1670
|
+
? ` and ${mappingPreview.summary.generatedSystemColumnCount} SolidX system column migration changes`
|
|
1671
|
+
: ""}
|
|
1672
|
+
.
|
|
1673
|
+
</span>
|
|
1674
|
+
) : drawerDraft.mapped ? (
|
|
1675
|
+
<span>
|
|
1676
|
+
Editing mapped model
|
|
1677
|
+
{" "}
|
|
1678
|
+
<strong>{selectedTable?.mappedModelDisplayName || selectedTable?.mappedModelSingularName}</strong>
|
|
1679
|
+
{drawerDraft.mappedModuleLabel ? ` in ${drawerDraft.mappedModuleLabel}` : ""}
|
|
1680
|
+
</span>
|
|
1681
|
+
) : (
|
|
1682
|
+
<span>Saving will create the model metadata and write it to the module JSON.</span>
|
|
1683
|
+
)}
|
|
1684
|
+
</div>
|
|
1685
|
+
<div className="sdix-drawer-footer__actions">
|
|
1686
|
+
{drawerDraft.mapped && drawerDraft.modelId ? (
|
|
1687
|
+
<SolidButton size="small" variant="outline" leftIcon={<ExternalLink size={14} />} onClick={openMappedModelForm}>
|
|
1688
|
+
Open model form
|
|
1689
|
+
</SolidButton>
|
|
1690
|
+
) : null}
|
|
1691
|
+
{drawerStep === "review" ? (
|
|
1692
|
+
<>
|
|
1693
|
+
<SolidButton size="small" variant="outline" onClick={() => setDrawerStep("edit")}>
|
|
1694
|
+
Back to editing
|
|
1695
|
+
</SolidButton>
|
|
1696
|
+
<SolidButton size="small" leftIcon={<Save size={14} />} loading={isSavingMapping} onClick={() => void saveMapping()}>
|
|
1697
|
+
Save mapping
|
|
1698
|
+
</SolidButton>
|
|
1699
|
+
</>
|
|
1700
|
+
) : (
|
|
1701
|
+
<SolidButton
|
|
1702
|
+
size="small"
|
|
1703
|
+
leftIcon={<Save size={14} />}
|
|
1704
|
+
loading={isPreviewMappingLoading}
|
|
1705
|
+
onClick={() => void viewChanges()}
|
|
1706
|
+
>
|
|
1707
|
+
View Changes
|
|
1708
|
+
</SolidButton>
|
|
1709
|
+
)}
|
|
1710
|
+
</div>
|
|
1711
|
+
</SolidDialogFooter>
|
|
1712
|
+
</>
|
|
1713
|
+
) : null}
|
|
1714
|
+
</SolidDialog>
|
|
1715
|
+
</>
|
|
1716
|
+
)}
|
|
1717
|
+
</div>
|
|
1718
|
+
);
|
|
1719
|
+
}
|