@truedat/core 8.13.0 → 8.13.2
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/package.json +3 -3
- package/src/components/UploadJob.js +102 -178
- package/src/components/UploadJobBreadcrumbs.js +13 -17
- package/src/components/UploadJobDetails.js +256 -0
- package/src/components/UploadJobHistory.js +90 -0
- package/src/components/UploadJobParser.js +155 -89
- package/src/components/UploadJobResults.js +278 -0
- package/src/components/UploadJobScope.js +37 -0
- package/src/components/UploadJobSummary.js +211 -0
- package/src/components/UploadJobs.js +48 -17
- package/src/components/__tests__/UploadJob.spec.js +118 -42
- package/src/components/__tests__/UploadJobBreadcrumbs.spec.js +3 -0
- package/src/components/__tests__/UploadJobHistory.spec.js +71 -0
- package/src/components/__tests__/UploadJobParser.spec.js +560 -26
- package/src/components/__tests__/UploadJobResults.spec.js +289 -0
- package/src/components/__tests__/UploadJobScope.spec.js +38 -0
- package/src/components/__tests__/UploadJobSummary.spec.js +208 -0
- package/src/components/__tests__/UploadJobs.spec.js +133 -1
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { useId, useState } from "react";
|
|
2
|
+
import PropTypes from "prop-types";
|
|
3
|
+
import { FormattedMessage, useIntl } from "react-intl";
|
|
4
|
+
import { Icon, Label, Table } from "semantic-ui-react";
|
|
5
|
+
import DateTime from "@truedat/core/components/DateTime";
|
|
6
|
+
import { ResultDetailCell, ResultEntityCell } from "./UploadJobParser";
|
|
7
|
+
import { getUploadJobScope } from "./UploadJobScope";
|
|
8
|
+
|
|
9
|
+
const filters = ["all", "created", "updated", "unchanged", "error"];
|
|
10
|
+
|
|
11
|
+
const filterMessages = {
|
|
12
|
+
all: { id: "uploadJob.results.filter.all" },
|
|
13
|
+
created: { id: "uploadJob.results.filter.created" },
|
|
14
|
+
updated: { id: "uploadJob.results.filter.updated" },
|
|
15
|
+
unchanged: { id: "uploadJob.results.filter.unchanged" },
|
|
16
|
+
error: { id: "uploadJob.results.filter.error" },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const getResultEvents = (events = []) =>
|
|
20
|
+
events.filter(({ status }) => status === "INFO" || status === "ERROR");
|
|
21
|
+
|
|
22
|
+
export const getResultOutcome = ({ status, response }) => {
|
|
23
|
+
if (status === "ERROR") return "error";
|
|
24
|
+
if (response?.type === "unchanged") return "unchanged";
|
|
25
|
+
if (response?.type === "created") return "created";
|
|
26
|
+
if (response?.type === "updated") return "updated";
|
|
27
|
+
return "unknown";
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const Outcome = ({ event }) => {
|
|
31
|
+
const outcome = getResultOutcome(event);
|
|
32
|
+
const config = {
|
|
33
|
+
created: {
|
|
34
|
+
className: "upload-job-outcome-created",
|
|
35
|
+
id: "uploadJob.parser.info.created",
|
|
36
|
+
},
|
|
37
|
+
updated: {
|
|
38
|
+
className: "upload-job-outcome-updated",
|
|
39
|
+
id: "uploadJob.parser.info.updated",
|
|
40
|
+
},
|
|
41
|
+
unchanged: {
|
|
42
|
+
className: "upload-job-outcome-unchanged",
|
|
43
|
+
color: "grey",
|
|
44
|
+
id: "uploadJob.parser.info.unchanged",
|
|
45
|
+
},
|
|
46
|
+
error: {
|
|
47
|
+
className: "upload-job-outcome-error",
|
|
48
|
+
color: "red",
|
|
49
|
+
id: "uploadJob.parser.event.status.ERROR",
|
|
50
|
+
},
|
|
51
|
+
unknown: {
|
|
52
|
+
className: "upload-job-outcome-unknown",
|
|
53
|
+
color: "grey",
|
|
54
|
+
id: "uploadJob.parser.event.status.INFO",
|
|
55
|
+
},
|
|
56
|
+
}[outcome];
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Label
|
|
60
|
+
color={config.color}
|
|
61
|
+
className={`upload-job-outcome ${config.className}`}
|
|
62
|
+
size="small"
|
|
63
|
+
>
|
|
64
|
+
<FormattedMessage id={config.id} />
|
|
65
|
+
</Label>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
Outcome.propTypes = {
|
|
70
|
+
event: PropTypes.shape({
|
|
71
|
+
status: PropTypes.string,
|
|
72
|
+
response: PropTypes.shape({ type: PropTypes.string }),
|
|
73
|
+
}).isRequired,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const Origin = ({ response }) => {
|
|
77
|
+
const { formatMessage } = useIntl();
|
|
78
|
+
const sheet = response?.sheet;
|
|
79
|
+
const rowNumber = response?.row_number;
|
|
80
|
+
|
|
81
|
+
if (!sheet && !rowNumber) return null;
|
|
82
|
+
|
|
83
|
+
const origin = [
|
|
84
|
+
sheet,
|
|
85
|
+
rowNumber
|
|
86
|
+
? formatMessage({ id: "uploadJob.results.origin.row" }, { rowNumber })
|
|
87
|
+
: null,
|
|
88
|
+
]
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.join(" · ");
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<Label size="small" className="upload-job-origin" title={origin}>
|
|
94
|
+
{origin}
|
|
95
|
+
</Label>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
Origin.propTypes = {
|
|
100
|
+
response: PropTypes.shape({
|
|
101
|
+
row_number: PropTypes.number,
|
|
102
|
+
sheet: PropTypes.string,
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
Origin.defaultProps = { response: undefined };
|
|
107
|
+
|
|
108
|
+
export const getEntityColumnMessageId = (scope) =>
|
|
109
|
+
getUploadJobScope(scope).entityColumnMessageId;
|
|
110
|
+
|
|
111
|
+
const UploadJobResults = ({
|
|
112
|
+
constrainResults,
|
|
113
|
+
events,
|
|
114
|
+
lifecycleStatus,
|
|
115
|
+
scope,
|
|
116
|
+
}) => {
|
|
117
|
+
const { formatMessage } = useIntl();
|
|
118
|
+
const [expanded, setExpanded] = useState(false);
|
|
119
|
+
const [filter, setFilter] = useState("all");
|
|
120
|
+
const contentId = useId();
|
|
121
|
+
const enabled = lifecycleStatus === "COMPLETED";
|
|
122
|
+
const resultsTitleId = useId();
|
|
123
|
+
const resultEvents = getResultEvents(events);
|
|
124
|
+
const visibleEvents = resultEvents.filter(
|
|
125
|
+
(event) => filter === "all" || getResultOutcome(event) === filter,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const toggleExpanded = () => {
|
|
129
|
+
if (enabled) setExpanded((value) => !value);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<section
|
|
134
|
+
className={`upload-job-results${enabled ? "" : " disabled"}`}
|
|
135
|
+
aria-labelledby={resultsTitleId}
|
|
136
|
+
>
|
|
137
|
+
<div className="upload-job-results-heading">
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
className="upload-job-results-toggle"
|
|
141
|
+
aria-expanded={expanded}
|
|
142
|
+
aria-controls={contentId}
|
|
143
|
+
disabled={!enabled}
|
|
144
|
+
onClick={toggleExpanded}
|
|
145
|
+
>
|
|
146
|
+
<span>
|
|
147
|
+
<strong id={resultsTitleId}>
|
|
148
|
+
<FormattedMessage id="uploadJob.results.title" />
|
|
149
|
+
</strong>
|
|
150
|
+
<small>
|
|
151
|
+
<FormattedMessage id="uploadJob.results.subtitle" />
|
|
152
|
+
</small>
|
|
153
|
+
</span>
|
|
154
|
+
<Icon
|
|
155
|
+
aria-hidden="true"
|
|
156
|
+
className="upload-job-results-chevron"
|
|
157
|
+
name={expanded ? "chevron up" : "chevron down"}
|
|
158
|
+
/>
|
|
159
|
+
</button>
|
|
160
|
+
{expanded ? (
|
|
161
|
+
<div
|
|
162
|
+
className="upload-job-result-filters"
|
|
163
|
+
role="group"
|
|
164
|
+
aria-label={formatMessage({
|
|
165
|
+
id: "uploadJob.results.filter.label",
|
|
166
|
+
})}
|
|
167
|
+
>
|
|
168
|
+
{filters.map((name) => (
|
|
169
|
+
<button
|
|
170
|
+
key={name}
|
|
171
|
+
type="button"
|
|
172
|
+
className={`ui small button upload-job-result-filter upload-job-result-filter-${name}${
|
|
173
|
+
filter === name ? " active" : ""
|
|
174
|
+
}`}
|
|
175
|
+
aria-pressed={filter === name}
|
|
176
|
+
onClick={() => setFilter(name)}
|
|
177
|
+
>
|
|
178
|
+
<FormattedMessage {...filterMessages[name]} />
|
|
179
|
+
</button>
|
|
180
|
+
))}
|
|
181
|
+
</div>
|
|
182
|
+
) : null}
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div
|
|
186
|
+
id={contentId}
|
|
187
|
+
className="upload-job-results-content"
|
|
188
|
+
hidden={!expanded}
|
|
189
|
+
>
|
|
190
|
+
{expanded ? (
|
|
191
|
+
<div
|
|
192
|
+
className={`upload-job-table-wrap${
|
|
193
|
+
constrainResults ? " upload-job-table-wrap-constrained" : ""
|
|
194
|
+
}`}
|
|
195
|
+
style={
|
|
196
|
+
constrainResults
|
|
197
|
+
? { maxHeight: "48vh", overflow: "auto" }
|
|
198
|
+
: undefined
|
|
199
|
+
}
|
|
200
|
+
>
|
|
201
|
+
<Table>
|
|
202
|
+
<Table.Header>
|
|
203
|
+
<Table.Row>
|
|
204
|
+
<Table.HeaderCell>
|
|
205
|
+
<FormattedMessage id="uploadJob.results.column.outcome" />
|
|
206
|
+
</Table.HeaderCell>
|
|
207
|
+
<Table.HeaderCell>
|
|
208
|
+
<FormattedMessage id="uploadJob.results.column.processedAt" />
|
|
209
|
+
</Table.HeaderCell>
|
|
210
|
+
<Table.HeaderCell>
|
|
211
|
+
<FormattedMessage id={getEntityColumnMessageId(scope)} />
|
|
212
|
+
</Table.HeaderCell>
|
|
213
|
+
<Table.HeaderCell>
|
|
214
|
+
<FormattedMessage id="uploadJob.table.detail" />
|
|
215
|
+
</Table.HeaderCell>
|
|
216
|
+
<Table.HeaderCell>
|
|
217
|
+
<FormattedMessage id="uploadJob.results.column.origin" />
|
|
218
|
+
</Table.HeaderCell>
|
|
219
|
+
</Table.Row>
|
|
220
|
+
</Table.Header>
|
|
221
|
+
<Table.Body>
|
|
222
|
+
{visibleEvents.map((event, index) => (
|
|
223
|
+
<Table.Row key={event.id ?? `${event.status}-${index}`}>
|
|
224
|
+
<Table.Cell className="upload-job-result-outcome-cell">
|
|
225
|
+
<Outcome event={event} />
|
|
226
|
+
</Table.Cell>
|
|
227
|
+
<Table.Cell className="upload-job-result-processed-cell">
|
|
228
|
+
<DateTime value={event.inserted_at} />
|
|
229
|
+
</Table.Cell>
|
|
230
|
+
<Table.Cell className="upload-job-result-entity-cell">
|
|
231
|
+
<ResultEntityCell {...event} />
|
|
232
|
+
</Table.Cell>
|
|
233
|
+
<Table.Cell className="upload-job-result-detail-cell">
|
|
234
|
+
<ResultDetailCell {...event} scope={scope} />
|
|
235
|
+
</Table.Cell>
|
|
236
|
+
<Table.Cell className="upload-job-result-origin-cell">
|
|
237
|
+
<Origin response={event.response} />
|
|
238
|
+
</Table.Cell>
|
|
239
|
+
</Table.Row>
|
|
240
|
+
))}
|
|
241
|
+
{visibleEvents.length === 0 ? (
|
|
242
|
+
<Table.Row>
|
|
243
|
+
<Table.Cell colSpan={5} textAlign="center">
|
|
244
|
+
<FormattedMessage id="uploadJob.results.empty" />
|
|
245
|
+
</Table.Cell>
|
|
246
|
+
</Table.Row>
|
|
247
|
+
) : null}
|
|
248
|
+
</Table.Body>
|
|
249
|
+
</Table>
|
|
250
|
+
</div>
|
|
251
|
+
) : null}
|
|
252
|
+
</div>
|
|
253
|
+
</section>
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
UploadJobResults.propTypes = {
|
|
258
|
+
constrainResults: PropTypes.bool,
|
|
259
|
+
events: PropTypes.arrayOf(
|
|
260
|
+
PropTypes.shape({
|
|
261
|
+
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
262
|
+
inserted_at: PropTypes.string,
|
|
263
|
+
response: PropTypes.object,
|
|
264
|
+
status: PropTypes.string,
|
|
265
|
+
}),
|
|
266
|
+
),
|
|
267
|
+
lifecycleStatus: PropTypes.string,
|
|
268
|
+
scope: PropTypes.string,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
UploadJobResults.defaultProps = {
|
|
272
|
+
constrainResults: false,
|
|
273
|
+
events: [],
|
|
274
|
+
lifecycleStatus: undefined,
|
|
275
|
+
scope: "implementations",
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
export default UploadJobResults;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IMPLEMENTATIONS_UPLOAD_JOBS,
|
|
3
|
+
STRUCTURE_NOTES_UPLOAD_JOBS,
|
|
4
|
+
linkTo,
|
|
5
|
+
} from "@truedat/core/routes";
|
|
6
|
+
|
|
7
|
+
export const uploadJobScopes = {
|
|
8
|
+
implementations: {
|
|
9
|
+
listPath: IMPLEMENTATIONS_UPLOAD_JOBS,
|
|
10
|
+
detailPath: (id) => linkTo.IMPLEMENTATIONS_UPLOAD_JOB({ id }),
|
|
11
|
+
listHeaderId: "uploadJobs.implementations.header",
|
|
12
|
+
detailHeaderId: "uploadJob.implementations.header",
|
|
13
|
+
entityColumnMessageId: "uploadJob.results.column.implementation",
|
|
14
|
+
fieldMessagePrefix: "ruleImplementations.props",
|
|
15
|
+
},
|
|
16
|
+
notes: {
|
|
17
|
+
listPath: STRUCTURE_NOTES_UPLOAD_JOBS,
|
|
18
|
+
detailPath: (id) => linkTo.STRUCTURE_NOTES_UPLOAD_JOB({ id }),
|
|
19
|
+
listHeaderId: "uploadJobs.notes.header",
|
|
20
|
+
detailHeaderId: "uploadJob.notes.header",
|
|
21
|
+
entityColumnMessageId: "uploadJob.results.column.structure",
|
|
22
|
+
fieldMessagePrefix: "structures.props",
|
|
23
|
+
fieldMessageOverrides: { status: "structure.notes.status" },
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const unknownScope = (scope) => ({
|
|
28
|
+
listPath: null,
|
|
29
|
+
detailPath: null,
|
|
30
|
+
listHeaderId: `uploadJobs.${scope}.header`,
|
|
31
|
+
detailHeaderId: `uploadJob.${scope}.header`,
|
|
32
|
+
entityColumnMessageId: uploadJobScopes.implementations.entityColumnMessageId,
|
|
33
|
+
fieldMessagePrefix: uploadJobScopes.implementations.fieldMessagePrefix,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const getUploadJobScope = (scope) =>
|
|
37
|
+
uploadJobScopes[scope] ?? unknownScope(scope);
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import _ from "lodash/fp";
|
|
2
|
+
import PropTypes from "prop-types";
|
|
3
|
+
import { FormattedMessage, useIntl } from "react-intl";
|
|
4
|
+
import { Message } from "semantic-ui-react";
|
|
5
|
+
|
|
6
|
+
export const lifecycleStatuses = ["PENDING", "STARTED", "COMPLETED", "FAILED"];
|
|
7
|
+
|
|
8
|
+
const messagePropsByStatus = {
|
|
9
|
+
PENDING: { warning: true },
|
|
10
|
+
STARTED: { info: true },
|
|
11
|
+
COMPLETED: { positive: true },
|
|
12
|
+
FAILED: { negative: true },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const emptySummary = {
|
|
16
|
+
insert_count: 0,
|
|
17
|
+
update_count: 0,
|
|
18
|
+
unchanged_count: 0,
|
|
19
|
+
error_count: 0,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const resultTypeCounters = {
|
|
23
|
+
created: "insert_count",
|
|
24
|
+
updated: "update_count",
|
|
25
|
+
unchanged: "unchanged_count",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const summaryCounterKeys = [
|
|
29
|
+
"insert_count",
|
|
30
|
+
"update_count",
|
|
31
|
+
"unchanged_count",
|
|
32
|
+
"error_count",
|
|
33
|
+
"invalid_sheet_count",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const hasSummaryCounters = (response) =>
|
|
37
|
+
response && summaryCounterKeys.some((key) => key in response);
|
|
38
|
+
|
|
39
|
+
export const getUploadJobStatus = (events = [], latestStatus) =>
|
|
40
|
+
_.flow(
|
|
41
|
+
_.filter(({ status }) => lifecycleStatuses.includes(status)),
|
|
42
|
+
_.last,
|
|
43
|
+
_.getOr(latestStatus, "status"),
|
|
44
|
+
)(events);
|
|
45
|
+
|
|
46
|
+
export const classifyUploadJobErrorMessage = (message) => {
|
|
47
|
+
if (typeof message !== "string" || message.trim() === "") {
|
|
48
|
+
return { type: "unknown" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const normalizedMessage = message.trim();
|
|
52
|
+
const isTechnicalError =
|
|
53
|
+
/^%[A-Z][A-Za-z0-9_.]*\{/.test(normalizedMessage) ||
|
|
54
|
+
/^\{:/.test(normalizedMessage) ||
|
|
55
|
+
/^\*\* \(/.test(normalizedMessage);
|
|
56
|
+
|
|
57
|
+
return isTechnicalError
|
|
58
|
+
? { type: "unexpected_error" }
|
|
59
|
+
: { type: "message", message: normalizedMessage };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const getUploadJobSummary = (events = [], latestResponse) => {
|
|
63
|
+
const completedResponse = _.flow(
|
|
64
|
+
_.filter(({ status }) => status === "COMPLETED"),
|
|
65
|
+
_.last,
|
|
66
|
+
_.get("response"),
|
|
67
|
+
)(events);
|
|
68
|
+
const response = hasSummaryCounters(completedResponse)
|
|
69
|
+
? completedResponse
|
|
70
|
+
: latestResponse;
|
|
71
|
+
|
|
72
|
+
if (hasSummaryCounters(response)) {
|
|
73
|
+
return {
|
|
74
|
+
insert_count: response.insert_count ?? 0,
|
|
75
|
+
update_count: response.update_count ?? 0,
|
|
76
|
+
unchanged_count: response.unchanged_count ?? 0,
|
|
77
|
+
error_count:
|
|
78
|
+
(response.error_count ?? 0) + (response.invalid_sheet_count ?? 0),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return events.reduce((summary, { status, response: eventResponse }) => {
|
|
83
|
+
if (status === "ERROR") {
|
|
84
|
+
return { ...summary, error_count: summary.error_count + 1 };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const counter =
|
|
88
|
+
status === "INFO" && resultTypeCounters[eventResponse?.type];
|
|
89
|
+
return counter ? { ...summary, [counter]: summary[counter] + 1 } : summary;
|
|
90
|
+
}, emptySummary);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const SummaryItem = ({ messageId, value }) => (
|
|
94
|
+
<span>
|
|
95
|
+
<FormattedMessage id={messageId} />: <strong>{value}</strong>
|
|
96
|
+
</span>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
SummaryItem.propTypes = {
|
|
100
|
+
messageId: PropTypes.string.isRequired,
|
|
101
|
+
value: PropTypes.number.isRequired,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const CompletedSummary = ({ summary }) => {
|
|
105
|
+
const total = Object.values(summary).reduce((sum, value) => sum + value, 0);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<>
|
|
109
|
+
<strong>
|
|
110
|
+
<FormattedMessage id="uploadJob.summary.title" />
|
|
111
|
+
</strong>
|
|
112
|
+
<SummaryItem messageId="uploadJob.summary.total" value={total} />
|
|
113
|
+
<SummaryItem
|
|
114
|
+
messageId="uploadJob.parser.result.summary.created"
|
|
115
|
+
value={summary.insert_count}
|
|
116
|
+
/>
|
|
117
|
+
<SummaryItem
|
|
118
|
+
messageId="uploadJob.parser.result.summary.updated"
|
|
119
|
+
value={summary.update_count}
|
|
120
|
+
/>
|
|
121
|
+
<SummaryItem
|
|
122
|
+
messageId="uploadJob.parser.result.summary.unchanged"
|
|
123
|
+
value={summary.unchanged_count}
|
|
124
|
+
/>
|
|
125
|
+
<SummaryItem
|
|
126
|
+
messageId="uploadJob.parser.result.summary.errors"
|
|
127
|
+
value={summary.error_count}
|
|
128
|
+
/>
|
|
129
|
+
</>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
CompletedSummary.propTypes = {
|
|
134
|
+
summary: PropTypes.shape({
|
|
135
|
+
insert_count: PropTypes.number.isRequired,
|
|
136
|
+
update_count: PropTypes.number.isRequired,
|
|
137
|
+
unchanged_count: PropTypes.number.isRequired,
|
|
138
|
+
error_count: PropTypes.number.isRequired,
|
|
139
|
+
}).isRequired,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const UploadJobSummary = ({ events, latestStatus, latestResponse }) => {
|
|
143
|
+
const { formatMessage } = useIntl();
|
|
144
|
+
const status = getUploadJobStatus(events, latestStatus);
|
|
145
|
+
const summary = getUploadJobSummary(events, latestResponse);
|
|
146
|
+
const eventFailedResponse = _.flow(
|
|
147
|
+
_.filter(({ status: eventStatus }) => eventStatus === "FAILED"),
|
|
148
|
+
_.last,
|
|
149
|
+
_.get("response"),
|
|
150
|
+
)(events);
|
|
151
|
+
const failedResponse = eventFailedResponse?.message
|
|
152
|
+
? eventFailedResponse
|
|
153
|
+
: latestResponse;
|
|
154
|
+
const error = classifyUploadJobErrorMessage(failedResponse?.message);
|
|
155
|
+
const detail =
|
|
156
|
+
error.type === "message"
|
|
157
|
+
? formatMessage({
|
|
158
|
+
id: `uploadJob.parser.error.${error.message}`,
|
|
159
|
+
defaultMessage: error.message,
|
|
160
|
+
})
|
|
161
|
+
: formatMessage({ id: `uploadJob.parser.error.${error.type}` });
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<Message
|
|
165
|
+
{...messagePropsByStatus[status]}
|
|
166
|
+
aria-label={formatMessage({ id: "uploadJob.summary.title" })}
|
|
167
|
+
className="upload-job-summary"
|
|
168
|
+
>
|
|
169
|
+
<div className="upload-job-summary-content">
|
|
170
|
+
{status === "COMPLETED" ? <CompletedSummary summary={summary} /> : null}
|
|
171
|
+
{status === "FAILED" ? (
|
|
172
|
+
<strong>
|
|
173
|
+
<FormattedMessage
|
|
174
|
+
id="uploadJob.summary.failed"
|
|
175
|
+
values={{ detail }}
|
|
176
|
+
/>
|
|
177
|
+
</strong>
|
|
178
|
+
) : null}
|
|
179
|
+
{status === "PENDING" || status === "STARTED" ? (
|
|
180
|
+
<>
|
|
181
|
+
<strong>
|
|
182
|
+
<FormattedMessage id="uploadJob.summary.title" />
|
|
183
|
+
</strong>
|
|
184
|
+
<FormattedMessage
|
|
185
|
+
id={`uploadJob.summary.${status.toLowerCase()}`}
|
|
186
|
+
/>
|
|
187
|
+
</>
|
|
188
|
+
) : null}
|
|
189
|
+
</div>
|
|
190
|
+
</Message>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
UploadJobSummary.propTypes = {
|
|
195
|
+
events: PropTypes.arrayOf(
|
|
196
|
+
PropTypes.shape({
|
|
197
|
+
status: PropTypes.string,
|
|
198
|
+
response: PropTypes.object,
|
|
199
|
+
}),
|
|
200
|
+
),
|
|
201
|
+
latestResponse: PropTypes.object,
|
|
202
|
+
latestStatus: PropTypes.string,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
UploadJobSummary.defaultProps = {
|
|
206
|
+
events: [],
|
|
207
|
+
latestResponse: undefined,
|
|
208
|
+
latestStatus: undefined,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export default UploadJobSummary;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import _ from "lodash/fp";
|
|
2
2
|
import moment from "moment";
|
|
3
|
+
import PropTypes from "prop-types";
|
|
4
|
+
import { useState } from "react";
|
|
3
5
|
import { FormattedMessage, useIntl } from "react-intl";
|
|
4
|
-
import { useNavigate } from "react-router";
|
|
5
6
|
import {
|
|
6
7
|
Header,
|
|
7
8
|
Icon,
|
|
@@ -10,27 +11,27 @@ import {
|
|
|
10
11
|
Loader,
|
|
11
12
|
Table,
|
|
12
13
|
} from "semantic-ui-react";
|
|
13
|
-
import { linkTo } from "@truedat/core/routes";
|
|
14
14
|
import { useUploadJobs } from "../hooks/useUploadJobs";
|
|
15
|
+
import { TrueModal } from "./TrueModal";
|
|
15
16
|
import { StatusPill, ResponseCell } from "./UploadJobParser";
|
|
16
17
|
import UploadJobBreadcrumbs from "./UploadJobBreadcrumbs";
|
|
18
|
+
import { UploadJobModalContent } from "./UploadJob";
|
|
19
|
+
import { getUploadJobScope } from "./UploadJobScope";
|
|
17
20
|
|
|
18
21
|
export default function UploadJobs({ scope }) {
|
|
19
22
|
const { formatMessage } = useIntl();
|
|
20
23
|
const { data, loading } = useUploadJobs(scope);
|
|
21
24
|
const jobs = data?.data;
|
|
22
|
-
const
|
|
25
|
+
const [selectedJobId, setSelectedJobId] = useState(null);
|
|
26
|
+
const scopeConfig = getUploadJobScope(scope);
|
|
23
27
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
break;
|
|
32
|
-
default:
|
|
33
|
-
break;
|
|
28
|
+
const openUploadJob = ({ id }) => setSelectedJobId(id);
|
|
29
|
+
const closeUploadJob = () => setSelectedJobId(null);
|
|
30
|
+
|
|
31
|
+
const handleRowKeyDown = (job) => (e) => {
|
|
32
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
openUploadJob(job);
|
|
34
35
|
}
|
|
35
36
|
};
|
|
36
37
|
|
|
@@ -47,7 +48,7 @@ export default function UploadJobs({ scope }) {
|
|
|
47
48
|
})}
|
|
48
49
|
/>
|
|
49
50
|
<Header.Content>
|
|
50
|
-
<FormattedMessage id={
|
|
51
|
+
<FormattedMessage id={scopeConfig.listHeaderId} />
|
|
51
52
|
<Header.Subheader>
|
|
52
53
|
<FormattedMessage id="uploadJobs.subheader" />
|
|
53
54
|
</Header.Subheader>
|
|
@@ -87,11 +88,14 @@ export default function UploadJobs({ scope }) {
|
|
|
87
88
|
</Table.Header>
|
|
88
89
|
<Table.Body>
|
|
89
90
|
{_.isArray(jobs) && !_.isEmpty(jobs) ? (
|
|
90
|
-
jobs.map((job
|
|
91
|
+
jobs.map((job) => (
|
|
91
92
|
<Table.Row
|
|
92
|
-
key={
|
|
93
|
+
key={job.id}
|
|
93
94
|
style={{ cursor: "pointer" }}
|
|
94
|
-
|
|
95
|
+
tabIndex={0}
|
|
96
|
+
aria-label={job.filename}
|
|
97
|
+
onClick={() => openUploadJob(job)}
|
|
98
|
+
onKeyDown={handleRowKeyDown(job)}
|
|
95
99
|
>
|
|
96
100
|
<Table.Cell>{job.filename}</Table.Cell>
|
|
97
101
|
<Table.Cell>
|
|
@@ -131,6 +135,33 @@ export default function UploadJobs({ scope }) {
|
|
|
131
135
|
</Segment>
|
|
132
136
|
</Dimmer.Dimmable>
|
|
133
137
|
</Segment>
|
|
138
|
+
<TrueModal
|
|
139
|
+
open={selectedJobId != null}
|
|
140
|
+
onClose={closeUploadJob}
|
|
141
|
+
size="large"
|
|
142
|
+
className="upload-job-modal"
|
|
143
|
+
icon={formatMessage({
|
|
144
|
+
id: "uploadJob.header.icon",
|
|
145
|
+
defaultMessage: "cogs",
|
|
146
|
+
})}
|
|
147
|
+
title={formatMessage({
|
|
148
|
+
id: scopeConfig.detailHeaderId,
|
|
149
|
+
defaultMessage: "Upload detail",
|
|
150
|
+
})}
|
|
151
|
+
actions={[]}
|
|
152
|
+
>
|
|
153
|
+
{selectedJobId != null ? (
|
|
154
|
+
<UploadJobModalContent id={selectedJobId} scope={scope} />
|
|
155
|
+
) : null}
|
|
156
|
+
</TrueModal>
|
|
134
157
|
</>
|
|
135
158
|
);
|
|
136
159
|
}
|
|
160
|
+
|
|
161
|
+
UploadJobs.propTypes = {
|
|
162
|
+
scope: PropTypes.string,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
UploadJobs.defaultProps = {
|
|
166
|
+
scope: "implementations",
|
|
167
|
+
};
|