@truedat/core 8.13.0 → 8.13.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.
@@ -0,0 +1,90 @@
1
+ import PropTypes from "prop-types";
2
+ import { FormattedMessage } from "react-intl";
3
+ import { Table } from "semantic-ui-react";
4
+ import DateTime from "@truedat/core/components/DateTime";
5
+ import { StatusPill } from "./UploadJobParser";
6
+ import { lifecycleStatuses } from "./UploadJobSummary";
7
+
8
+ export const getLifecycleEvents = (events = []) =>
9
+ events.filter(({ status }) => lifecycleStatuses.includes(status));
10
+
11
+ const UploadJobHistory = ({ events }) => {
12
+ const lifecycleEvents = getLifecycleEvents(events);
13
+
14
+ return (
15
+ <section
16
+ className="upload-job-history"
17
+ aria-labelledby="upload-job-history-title"
18
+ >
19
+ <h3 id="upload-job-history-title">
20
+ <FormattedMessage id="uploadJob.history.title" />
21
+ </h3>
22
+ <Table
23
+ basic="very"
24
+ className="upload-job-history-table"
25
+ verticalAlign="middle"
26
+ >
27
+ <Table.Header>
28
+ <Table.Row>
29
+ <Table.HeaderCell className="upload-job-visually-hidden">
30
+ <FormattedMessage id="uploadJob.table.status" />
31
+ </Table.HeaderCell>
32
+ <Table.HeaderCell className="upload-job-visually-hidden">
33
+ <FormattedMessage id="uploadJob.table.inserted_at" />
34
+ </Table.HeaderCell>
35
+ <Table.HeaderCell className="upload-job-visually-hidden">
36
+ <FormattedMessage id="uploadJob.table.response" />
37
+ </Table.HeaderCell>
38
+ </Table.Row>
39
+ </Table.Header>
40
+ <Table.Body>
41
+ {lifecycleEvents.map((event, index) => (
42
+ <Table.Row
43
+ key={event.id ?? `${event.status}-${index}`}
44
+ className={
45
+ index === lifecycleEvents.length - 1
46
+ ? "upload-job-history-final"
47
+ : "upload-job-history-intermediate"
48
+ }
49
+ >
50
+ <Table.Cell textAlign="center">
51
+ <span className="upload-job-history-pill">
52
+ <StatusPill status={event.status} />
53
+ </span>
54
+ </Table.Cell>
55
+ <Table.Cell>
56
+ <DateTime value={event.inserted_at} />
57
+ </Table.Cell>
58
+ <Table.Cell>
59
+ <FormattedMessage id={`uploadJob.history.${event.status}`} />
60
+ </Table.Cell>
61
+ </Table.Row>
62
+ ))}
63
+ {lifecycleEvents.length === 0 ? (
64
+ <Table.Row>
65
+ <Table.Cell colSpan={3}>
66
+ <FormattedMessage id="uploadJob.history.empty" />
67
+ </Table.Cell>
68
+ </Table.Row>
69
+ ) : null}
70
+ </Table.Body>
71
+ </Table>
72
+ </section>
73
+ );
74
+ };
75
+
76
+ UploadJobHistory.propTypes = {
77
+ events: PropTypes.arrayOf(
78
+ PropTypes.shape({
79
+ id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
80
+ inserted_at: PropTypes.string,
81
+ status: PropTypes.string,
82
+ }),
83
+ ),
84
+ };
85
+
86
+ UploadJobHistory.defaultProps = {
87
+ events: [],
88
+ };
89
+
90
+ export default UploadJobHistory;
@@ -1,15 +1,19 @@
1
1
  import _ from "lodash/fp";
2
- import { useState } from "react";
2
+ import PropTypes from "prop-types";
3
3
  import { FormattedMessage, useIntl } from "react-intl";
4
4
  import Label from "semantic-ui-react/dist/commonjs/elements/Label";
5
5
  import RuleImplementationLink from "@truedat/dq/components/RuleImplementationLink";
6
6
  import StructureNoteLink from "@truedat/dd/components/StructureNoteLink";
7
+ import { ChangesDetail, ErrorDetailsDisclosure } from "./UploadJobDetails";
8
+ import { classifyUploadJobErrorMessage } from "./UploadJobSummary";
7
9
 
8
10
  export const StatusPill = ({ status }) => (
9
11
  <Label
10
12
  color={_.cond([
11
13
  [_.eq("COMPLETED"), _.constant("green")],
12
14
  [_.eq("FAILED"), _.constant("red")],
15
+ [_.eq("PENDING"), _.constant("yellow")],
16
+ [_.eq("STARTED"), _.constant("blue")],
13
17
  [_.eq("ERROR"), _.constant("red")],
14
18
  [_.eq("INFO"), _.constant("blue")],
15
19
  [_.stubTrue, _.constant("grey")],
@@ -20,23 +24,34 @@ export const StatusPill = ({ status }) => (
20
24
  </Label>
21
25
  );
22
26
 
23
- export const ResponseCell = ({ response, status }) => {
27
+ export const ResponseCell = ({
28
+ response,
29
+ status,
30
+ showOrigin = true,
31
+ scope,
32
+ }) => {
24
33
  if (!response || (_.isObject(response) && _.isEmpty(response))) return null;
25
34
 
26
35
  switch (status) {
27
- case "FAILED":
36
+ case "FAILED": {
37
+ const error = classifyUploadJobErrorMessage(response.message);
28
38
  return (
29
39
  <FormattedMessage
30
- id={`uploadJob.parser.error.${response.message}`}
31
- defaultMessage={response.message}
40
+ id={`uploadJob.parser.error.${
41
+ error.type === "message" ? error.message : error.type
42
+ }`}
43
+ defaultMessage={error.message}
32
44
  />
33
45
  );
46
+ }
34
47
  case "ERROR":
35
- return <ErrorDetail response={response} />;
48
+ return <ErrorDetail response={response} showOrigin={showOrigin} />;
36
49
  case "COMPLETED":
37
50
  return <CompletedDetail response={response} />;
38
51
  case "INFO":
39
- return <InfoDetail response={response} />;
52
+ return (
53
+ <InfoDetail response={response} showOrigin={showOrigin} scope={scope} />
54
+ );
40
55
  default:
41
56
  return (
42
57
  <pre
@@ -54,6 +69,86 @@ export const ResponseCell = ({ response, status }) => {
54
69
  }
55
70
  };
56
71
 
72
+ export const ResultEntityCell = ({ response, status }) => {
73
+ if (!response?.details || !["INFO", "ERROR"].includes(status)) return null;
74
+
75
+ if (status === "ERROR" && response.type === "data_structure_not_found") {
76
+ return null;
77
+ }
78
+
79
+ return response.details.implementation_key != null ? (
80
+ <RuleImplementationLink {...response.details} />
81
+ ) : status === "INFO" &&
82
+ (response.details.data_structure_id != null ||
83
+ response.details.external_id != null) ? (
84
+ <StructureNoteLink
85
+ data_structure_id={response.details.data_structure_id}
86
+ external_id={response.details.external_id}
87
+ alias={response.details.alias}
88
+ name={response.details.name}
89
+ />
90
+ ) : status === "ERROR" &&
91
+ response.details.data_structure_id != null &&
92
+ response.details.external_id != null ? (
93
+ <StructureNoteLink
94
+ data_structure_id={response.details.data_structure_id}
95
+ external_id={response.details.external_id}
96
+ alias={response.details.alias}
97
+ name={response.details.name}
98
+ />
99
+ ) : null;
100
+ };
101
+
102
+ ResultEntityCell.propTypes = {
103
+ response: PropTypes.shape({
104
+ details: PropTypes.shape({
105
+ data_structure_id: PropTypes.oneOfType([
106
+ PropTypes.number,
107
+ PropTypes.string,
108
+ ]),
109
+ external_id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
110
+ alias: PropTypes.string,
111
+ implementation_key: PropTypes.string,
112
+ implementation_ref: PropTypes.oneOfType([
113
+ PropTypes.number,
114
+ PropTypes.string,
115
+ ]),
116
+ name: PropTypes.string,
117
+ }),
118
+ }),
119
+ status: PropTypes.string,
120
+ };
121
+
122
+ export const ResultDetailCell = ({ response, status, scope }) => {
123
+ if (status === "ERROR")
124
+ return <ErrorDetail response={response} showOrigin={false} />;
125
+ if (
126
+ status !== "INFO" ||
127
+ !["updated", "implementation_updated"].includes(response?.type)
128
+ )
129
+ return null;
130
+
131
+ return (
132
+ <ChangesDetail
133
+ changes={response?.details?.changes}
134
+ changeDetails={response?.details?.change_details}
135
+ scope={scope}
136
+ />
137
+ );
138
+ };
139
+
140
+ ResultDetailCell.propTypes = {
141
+ response: PropTypes.shape({
142
+ details: PropTypes.shape({
143
+ change_details: PropTypes.object,
144
+ changes: PropTypes.object,
145
+ }),
146
+ type: PropTypes.string,
147
+ }),
148
+ scope: PropTypes.string,
149
+ status: PropTypes.string,
150
+ };
151
+
57
152
  const SheetAndLine = ({ response }) => (
58
153
  <Label size="small">
59
154
  <span>{response.sheet}</span>
@@ -67,14 +162,16 @@ const SheetAndLine = ({ response }) => (
67
162
  </Label>
68
163
  );
69
164
 
70
- const CellHeader = ({ header, response }) => (
165
+ const CellHeader = ({ header, response, showOrigin = true }) => (
71
166
  <div style={{ display: "flex", justifyContent: "space-between" }}>
72
167
  <b>
73
168
  <FormattedMessage id={header} />
74
169
  </b>
75
- <div>
76
- <SheetAndLine response={response} />
77
- </div>
170
+ {showOrigin ? (
171
+ <div>
172
+ <SheetAndLine response={response} />
173
+ </div>
174
+ ) : null}
78
175
  </div>
79
176
  );
80
177
 
@@ -83,12 +180,14 @@ const externalIdDetailValue = (details) =>
83
180
  <StructureNoteLink
84
181
  data_structure_id={details.data_structure_id}
85
182
  external_id={details.external_id}
183
+ alias={details.alias}
184
+ name={details.name}
86
185
  />
87
186
  ) : (
88
187
  (details?.external_id ?? "")
89
188
  );
90
189
 
91
- const ErrorDetail = ({ response }) => {
190
+ const ErrorDetail = ({ response, showOrigin }) => {
92
191
  const { formatMessage } = useIntl();
93
192
 
94
193
  if (!response || !response.type) {
@@ -97,6 +196,14 @@ const ErrorDetail = ({ response }) => {
97
196
  const { type } = response;
98
197
  const messagePrefix = "uploadJob.parser.detail";
99
198
 
199
+ if (!showOrigin && type === "data_structure_not_found") {
200
+ return (
201
+ <span className="upload-job-simple-error">
202
+ <FormattedMessage id="uploadJob.parser.error.data_structure_not_found" />
203
+ </span>
204
+ );
205
+ }
206
+
100
207
  const detailBuilders = {
101
208
  missing_required_headers: (details) => [
102
209
  [
@@ -225,11 +332,29 @@ const ErrorDetail = ({ response }) => {
225
332
  details,
226
333
  );
227
334
 
335
+ const resultDetails =
336
+ !showOrigin &&
337
+ _.isEmpty(visibleDetails) &&
338
+ response.details != null &&
339
+ (typeof response.details !== "object" || !_.isEmpty(response.details))
340
+ ? [
341
+ [
342
+ formatMessage({ id: `${messagePrefix}.message` }),
343
+ formatErrorValue(response.details),
344
+ ],
345
+ ]
346
+ : visibleDetails;
347
+
348
+ if (!showOrigin && !_.isEmpty(resultDetails)) {
349
+ return <ErrorDetailsDisclosure details={resultDetails} />;
350
+ }
351
+
228
352
  return (
229
353
  <div>
230
354
  <CellHeader
231
355
  header={`uploadJob.parser.error.${type}`}
232
356
  response={response}
357
+ showOrigin={showOrigin}
233
358
  />
234
359
 
235
360
  {_.isEmpty(visibleDetails)
@@ -253,6 +378,15 @@ const ErrorDetail = ({ response }) => {
253
378
  </div>
254
379
  );
255
380
  };
381
+
382
+ const formatErrorValue = (value) => {
383
+ if (typeof value === "string" || typeof value === "number") return value;
384
+ if (typeof value === "boolean") return String(value);
385
+ if (_.isArray(value)) return value.map(formatErrorValue).join(", ");
386
+ if (_.isObject(value)) return JSON.stringify(value);
387
+ return String(value ?? "");
388
+ };
389
+
256
390
  const summaryItems = [
257
391
  {
258
392
  key: "invalid_sheet_count",
@@ -301,7 +435,7 @@ const CompletedDetail = ({ response }) => {
301
435
  return <span>&nbsp;|&nbsp;{items}&nbsp;|&nbsp;</span>;
302
436
  };
303
437
 
304
- const InfoDetail = ({ response }) =>
438
+ const InfoDetail = ({ response, showOrigin, scope }) =>
305
439
  !response || !response.type ? null : (
306
440
  <div>
307
441
  <CellHeader
@@ -311,6 +445,7 @@ const InfoDetail = ({ response }) =>
311
445
  : `uploadJob.parser.info.${response.type}`
312
446
  }
313
447
  response={response}
448
+ showOrigin={showOrigin}
314
449
  />
315
450
  {response.details ? (
316
451
  <div>
@@ -321,85 +456,16 @@ const InfoDetail = ({ response }) =>
321
456
  <StructureNoteLink
322
457
  data_structure_id={response.details.data_structure_id}
323
458
  external_id={response.details.external_id}
459
+ alias={response.details.alias}
460
+ name={response.details.name}
324
461
  />
325
462
  ) : null}{" "}
326
463
  </div>
327
464
  ) : null}
328
- <ChangesDetail changes={response?.details?.changes} />
329
- </div>
330
- );
331
- const ChangesDetail = ({
332
- changes,
333
- header = "uploadJob.parser.result.prop.changes",
334
- }) => {
335
- const [isExpanded, setIsExpanded] = useState(false);
336
- if (!changes) return null;
337
- const changesList = _.toPairs(changes);
338
-
339
- return (
340
- <div>
341
- <div
342
- onClick={() => setIsExpanded(!isExpanded)}
343
- style={{
344
- cursor: "pointer",
345
- display: "flex",
346
- gap: "5px",
347
- alignItems: "center",
348
- }}
349
- >
350
- <span>{isExpanded ? "▼" : "▶"}</span>
351
- <span>
352
- <FormattedMessage id={header} />
353
- </span>
354
- <span>({_.size(changesList)}) :</span>
355
- </div>
356
- {isExpanded &&
357
- changesList.map(([key, value]) => (
358
- <div key={key} style={{ paddingLeft: "10px" }}>
359
- {key == "df_content" ? (
360
- <ChangesDetail
361
- changes={value}
362
- header={`ruleImplementations.props.${key}`}
363
- />
364
- ) : (
365
- <>
366
- <span
367
- style={{
368
- fontSize: "1em",
369
- fontWeight: "bold",
370
- paddingRight: "5px",
371
- }}
372
- >
373
- <FormattedMessage
374
- id={`ruleImplementations.props.${key}`}
375
- defaultMessage={key}
376
- />
377
- :
378
- </span>
379
- {formatValue(value, key)}
380
- </>
381
- )}
382
- </div>
383
- ))}
465
+ <ChangesDetail
466
+ changes={response?.details?.changes}
467
+ changeDetails={response?.details?.change_details}
468
+ scope={scope}
469
+ />
384
470
  </div>
385
471
  );
386
- };
387
-
388
- const formatValue = (value, key) => {
389
- if (_.has("value")(value)) return formatValue(value.value, key);
390
- if (_.isArray(value)) {
391
- return _.flow(
392
- _.map((v) => formatValue(v, key)),
393
- _.join(", "),
394
- )(value);
395
- }
396
- if (_.isObject(value)) {
397
- if (_.has("url_value")(value))
398
- return `[${value.url_name}] (${value.url_value})`;
399
- if (_.has("name")(value)) return value.name;
400
- if (_.has("external_id")(value)) return value.external_id;
401
-
402
- return _.flow(_.keys, _.join(", "))(value);
403
- }
404
- return value;
405
- };