@truedat/dq 8.1.3 → 8.1.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/dq",
3
- "version": "8.1.3",
3
+ "version": "8.1.5",
4
4
  "description": "Truedat Web Data Quality Module",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -53,7 +53,7 @@
53
53
  "@testing-library/jest-dom": "^6.6.3",
54
54
  "@testing-library/react": "^16.3.0",
55
55
  "@testing-library/user-event": "^14.6.1",
56
- "@truedat/test": "8.1.3",
56
+ "@truedat/test": "8.1.5",
57
57
  "identity-obj-proxy": "^3.0.0",
58
58
  "jest": "^29.7.0",
59
59
  "redux-saga-test-plan": "^4.0.6"
@@ -86,5 +86,5 @@
86
86
  "semantic-ui-react": "^3.0.0-beta.2",
87
87
  "swr": "^2.3.3"
88
88
  },
89
- "gitHead": "0eaf0c4b1342771cddb87d09ed78a9e4c65a5ca2"
89
+ "gitHead": "a9bc192d725eff352d0088d29d7b9b7ebea29254"
90
90
  }
@@ -52,8 +52,8 @@ import RuleResultDetails from "./RuleResultDetails";
52
52
  import RuleResultRemediationLoader from "./RuleResultRemediationLoader";
53
53
  import RuleResultsRoutes from "./RuleResultsRoutes";
54
54
  import RuleSubscriptionLoader from "./RuleSubscriptionLoader";
55
- import ImplementationsUploadJobs from "./ImplementationsUploadJobs";
56
- import ImplementationsUploadJob from "./ImplementationsUploadJob";
55
+ import UploadJobs from "@truedat/core/components/UploadJobs";
56
+ import UploadJob from "@truedat/core/components/UploadJob";
57
57
 
58
58
  const TemplatesLoader = React.lazy(
59
59
  () => import("@truedat/core/components/TemplatesLoader")
@@ -111,8 +111,8 @@ export const ImplementationsRoutes = ({
111
111
  // IMPLEMENTATIONS_UPLOAD_JOBS = "/implementations/uploadJobs";
112
112
  path="/uploadJobs"
113
113
  >
114
- <Route index element={<ImplementationsUploadJobs />} />
115
- <Route path=":id" element={<ImplementationsUploadJob />} />
114
+ <Route index element={<UploadJobs scope="implementations" />} />
115
+ <Route path=":id" element={<UploadJob scope="implementations" />} />
116
116
  </Route>
117
117
 
118
118
  <Route
@@ -289,8 +289,8 @@ export const ImplementationsRoutes = ({
289
289
  />
290
290
  ) : null}
291
291
  {!structuresAliasesLoading &&
292
- ruleImplementationLoaded &&
293
- implementationStructuresLoaded ? (
292
+ ruleImplementationLoaded &&
293
+ implementationStructuresLoaded ? (
294
294
  isRuleImplemenationBasic ? (
295
295
  <NewBasicRuleImplementation edition clone />
296
296
  ) : (
@@ -316,8 +316,8 @@ export const ImplementationsRoutes = ({
316
316
  />
317
317
  ) : null}
318
318
  {!structuresAliasesLoading &&
319
- ruleImplementationLoaded &&
320
- implementationStructuresLoaded ? (
319
+ ruleImplementationLoaded &&
320
+ implementationStructuresLoaded ? (
321
321
  isRuleImplemenationBasic ? (
322
322
  <NewBasicRuleImplementation edition />
323
323
  ) : (
@@ -343,8 +343,8 @@ export const ImplementationsRoutes = ({
343
343
  />
344
344
  ) : null}
345
345
  {!structuresAliasesLoading &&
346
- ruleImplementationLoaded &&
347
- implementationStructuresLoaded ? (
346
+ ruleImplementationLoaded &&
347
+ implementationStructuresLoaded ? (
348
348
  <NewRuleImplementation edition implementationType="default" />
349
349
  ) : null}
350
350
  </>
@@ -366,8 +366,8 @@ export const ImplementationsRoutes = ({
366
366
  />
367
367
  ) : null}
368
368
  {!structuresAliasesLoading &&
369
- ruleImplementationLoaded &&
370
- implementationStructuresLoaded ? (
369
+ ruleImplementationLoaded &&
370
+ implementationStructuresLoaded ? (
371
371
  <NewRuleImplementation edition implementationType="raw" />
372
372
  ) : null}
373
373
  </>
@@ -1,25 +0,0 @@
1
- import PropTypes from "prop-types";
2
- import { Breadcrumb } from "semantic-ui-react";
3
- import { Link } from "react-router";
4
- import { FormattedMessage } from "react-intl";
5
- import { IMPLEMENTATIONS_UPLOAD_JOBS } from "@truedat/core/routes";
6
-
7
- export const ImplementationUploadJobBreadcrumbs = ({ filename }) => (
8
- <Breadcrumb>
9
- <Breadcrumb.Section as={Link} to={IMPLEMENTATIONS_UPLOAD_JOBS}>
10
- <FormattedMessage id="sidemenu.implementations_upload_jobs" />
11
- </Breadcrumb.Section>
12
- {filename ? (
13
- <>
14
- <Breadcrumb.Divider icon="right angle" />
15
- <Breadcrumb.Section active>{filename}</Breadcrumb.Section>
16
- </>
17
- ) : null}
18
- </Breadcrumb>
19
- );
20
-
21
- ImplementationUploadJobBreadcrumbs.propTypes = {
22
- filename: PropTypes.string,
23
- };
24
-
25
- export default ImplementationUploadJobBreadcrumbs;
@@ -1,217 +0,0 @@
1
- import _ from "lodash/fp";
2
- import { useState } from "react";
3
- import {
4
- Header,
5
- Icon,
6
- Segment,
7
- Dimmer,
8
- Loader,
9
- Table,
10
- Label,
11
- Button,
12
- } from "semantic-ui-react";
13
- import { FormattedMessage, useIntl } from "react-intl";
14
- import { useParams } from "react-router";
15
- import { DateTime } from "@truedat/core/components";
16
- import { useImplementationsUploadJob } from "../hooks/useImplementations";
17
- import { StatusPill, ResponseCell } from "./implementationsUploadJobParser";
18
- import ImplementationUploadJobBreadcrumbs from "./ImplementationUploadJobBreadcrumbs";
19
-
20
- export default function ImplementationsUploadJob() {
21
- const { id } = useParams();
22
- const { data, loading } = useImplementationsUploadJob(id);
23
- const job = data?.data;
24
-
25
- const { formatMessage } = useIntl();
26
- const [expandedGroups, setExpandedGroups] = useState({});
27
-
28
- // Group events by status
29
- const groupedEvents = job?.events ? _.groupBy("status", job.events) : {};
30
-
31
- const toggleGroup = (status) => {
32
- setExpandedGroups((prev) => ({
33
- ...prev,
34
- [status]: !prev[status],
35
- }));
36
- };
37
-
38
- const renderEventGroup = (status, events) => {
39
- const isExpanded = expandedGroups[status];
40
- const shouldCollapse = status === "INFO" || status === "ERROR";
41
-
42
- if (!shouldCollapse) {
43
- // Render non-collapsible events normally
44
- return events.map((event, idx) => (
45
- <Table.Row key={`${status}-${idx}`}>
46
- <Table.Cell>
47
- <StatusPill status={event.status} />
48
- </Table.Cell>
49
- <Table.Cell>
50
- <DateTime value={event.inserted_at} />
51
- </Table.Cell>
52
- <Table.Cell>
53
- <ResponseCell {...event} />
54
- </Table.Cell>
55
- </Table.Row>
56
- ));
57
- }
58
-
59
- // Render collapsible groups for INFO and ERROR
60
- const groupRows = [];
61
-
62
- // Add group header row
63
- groupRows.push(
64
- <Table.Row
65
- key={`${status}-header`}
66
- style={{ backgroundColor: "#f8f9fa" }}
67
- >
68
- <Table.Cell>
69
- <Button
70
- basic
71
- size="mini"
72
- icon={isExpanded ? "chevron down" : "chevron right"}
73
- content={` ${formatMessage({ id: `implementations.bulkUpload.event.status.${status}` })} (${events.length})`}
74
- onClick={() => toggleGroup(status)}
75
- style={{ padding: "0.5em" }}
76
- />
77
- </Table.Cell>
78
- <Table.Cell>
79
- <DateTime value={events[0]?.inserted_at} />
80
- </Table.Cell>
81
- <Table.Cell></Table.Cell>
82
- </Table.Row>
83
- );
84
-
85
- // Add expanded events if group is expanded
86
- if (isExpanded) {
87
- events.forEach((event, idx) => {
88
- groupRows.push(
89
- <Table.Row key={`${status}-${idx}`} style={{ paddingLeft: "2em" }}>
90
- <Table.Cell style={{ paddingLeft: "2em" }}>
91
- <StatusPill status={event.status} />
92
- </Table.Cell>
93
- <Table.Cell>
94
- <DateTime value={event.inserted_at} />
95
- </Table.Cell>
96
- <Table.Cell>
97
- <ResponseCell {...event} />
98
- </Table.Cell>
99
- </Table.Row>
100
- );
101
- });
102
- }
103
-
104
- return groupRows;
105
- };
106
-
107
- return (
108
- <>
109
- <ImplementationUploadJobBreadcrumbs filename={job?.filename} />
110
- <Segment>
111
- <Header as="h2">
112
- <Icon
113
- circular
114
- name={formatMessage({
115
- id: "implementations.bulkUpload.job.header.icon",
116
- defaultMessage: "cogs",
117
- })}
118
- />
119
- <Header.Content>
120
- <FormattedMessage id="implementations.bulkUpload.job.header" />
121
- </Header.Content>
122
- </Header>
123
- <Dimmer.Dimmable dimmed={loading}>
124
- <Segment attached="bottom">
125
- {job ? (
126
- <>
127
- <div
128
- style={{
129
- display: "flex",
130
- alignItems: "center",
131
- gap: "1.5em",
132
- marginBottom: "1.5em",
133
- flexWrap: "wrap",
134
- }}
135
- >
136
- <div style={{ display: "flex", flexDirection: "column" }}>
137
- <span style={{ fontWeight: 600, fontSize: "1.3em" }}>
138
- {job.filename}
139
- </span>
140
- <span style={{ color: "gray", fontSize: "0.7em" }}>
141
- {job.hash}
142
- </span>
143
- </div>
144
- <span>
145
- <Label basic size="small" style={{ marginRight: 4 }}>
146
- <FormattedMessage
147
- id="implementations.bulkUpload.job.table.status"
148
- defaultMessage="Status"
149
- />
150
- </Label>
151
- <StatusPill status={job.latest_status} />
152
- </span>
153
- <span>
154
- <Label basic size="small" style={{ marginRight: 4 }}>
155
- <FormattedMessage
156
- id="implementations.bulkUpload.job.table.inserted_at"
157
- defaultMessage="Inserted At"
158
- />
159
- </Label>
160
- <DateTime value={job.latest_event_at} />
161
- </span>
162
- </div>
163
- <Table>
164
- <Table.Header>
165
- <Table.Row>
166
- <Table.HeaderCell>
167
- <FormattedMessage
168
- id="implementations.bulkUpload.job.table.status"
169
- defaultMessage="Status"
170
- />
171
- </Table.HeaderCell>
172
- <Table.HeaderCell>
173
- <FormattedMessage
174
- id="implementations.bulkUpload.job.table.inserted_at"
175
- defaultMessage="Inserted At"
176
- />
177
- </Table.HeaderCell>
178
- <Table.HeaderCell>
179
- <FormattedMessage
180
- id="implementations.bulkUpload.job.table.detail"
181
- defaultMessage="Detail"
182
- />
183
- </Table.HeaderCell>
184
- </Table.Row>
185
- </Table.Header>
186
- <Table.Body>
187
- {_.isArray(job.events) && !_.isEmpty(job.events) ? (
188
- Object.entries(groupedEvents)
189
- .map(([status, events]) =>
190
- renderEventGroup(status, events)
191
- )
192
- .flat()
193
- ) : (
194
- <Table.Row>
195
- <Table.Cell colSpan={3}>
196
- <FormattedMessage
197
- id="implementations.bulkUpload.job.table.no_events"
198
- defaultMessage="No events found."
199
- />
200
- </Table.Cell>
201
- </Table.Row>
202
- )}
203
- </Table.Body>
204
- </Table>
205
- </>
206
- ) : null}
207
- {loading ? (
208
- <Dimmer active inverted>
209
- <Loader />
210
- </Dimmer>
211
- ) : null}
212
- </Segment>
213
- </Dimmer.Dimmable>
214
- </Segment>
215
- </>
216
- );
217
- }
@@ -1,128 +0,0 @@
1
- import _ from "lodash/fp";
2
- import moment from "moment";
3
- import {
4
- Header,
5
- Icon,
6
- Segment,
7
- Dimmer,
8
- Loader,
9
- Table,
10
- } from "semantic-ui-react";
11
- import { FormattedMessage, useIntl } from "react-intl";
12
- import { useNavigate } from "react-router";
13
- import { useImplementationsUploadJobs } from "../hooks/useImplementations";
14
- import { linkTo } from "@truedat/core/routes";
15
- import { StatusPill, ResponseCell } from "./implementationsUploadJobParser";
16
-
17
- import ImplementationUploadJobBreadcrumbs from "./ImplementationUploadJobBreadcrumbs";
18
-
19
- export default function ImplementationsUploadJobs() {
20
- const { formatMessage } = useIntl();
21
- const { data, loading } = useImplementationsUploadJobs();
22
- const jobs = data?.data;
23
- const navigate = useNavigate();
24
-
25
- return (
26
- <>
27
- <ImplementationUploadJobBreadcrumbs />
28
- <Segment>
29
- <Header as="h2">
30
- <Icon
31
- circular
32
- name={formatMessage({
33
- id: "implementations.bulkUpload.jobs.header.icon",
34
- defaultMessage: "cogs",
35
- })}
36
- />
37
- <Header.Content>
38
- <FormattedMessage id="implementations.bulkUpload.jobs.header" />
39
- <Header.Subheader>
40
- <FormattedMessage id="implementations.bulkUpload.jobs.subheader" />
41
- </Header.Subheader>
42
- </Header.Content>
43
- </Header>
44
- <Dimmer.Dimmable dimmed={loading}>
45
- <Segment attached="bottom">
46
- {jobs ? (
47
- <Table selectable>
48
- <Table.Header>
49
- <Table.Row>
50
- <Table.HeaderCell>
51
- <FormattedMessage
52
- id="implementations.bulkUpload.job.table.filename"
53
- defaultMessage="Filename"
54
- />
55
- </Table.HeaderCell>
56
- <Table.HeaderCell>
57
- <FormattedMessage
58
- id="implementations.bulkUpload.job.table.status"
59
- defaultMessage="Status"
60
- />
61
- </Table.HeaderCell>
62
- <Table.HeaderCell>
63
- <FormattedMessage
64
- id="implementations.bulkUpload.job.table.response"
65
- defaultMessage="Response"
66
- />
67
- </Table.HeaderCell>
68
- <Table.HeaderCell>
69
- <FormattedMessage
70
- id="implementations.bulkUpload.job.table.latest_event_at"
71
- defaultMessage="Latest Event"
72
- />
73
- </Table.HeaderCell>
74
- </Table.Row>
75
- </Table.Header>
76
- <Table.Body>
77
- {_.isArray(jobs) && !_.isEmpty(jobs) ? (
78
- jobs.map((job, idx) => (
79
- <Table.Row
80
- key={idx}
81
- style={{ cursor: "pointer" }}
82
- onClick={() =>
83
- navigate(
84
- linkTo.IMPLEMENTATIONS_UPLOAD_JOB({ id: job.id })
85
- )
86
- }
87
- >
88
- <Table.Cell>{job.filename}</Table.Cell>
89
- <Table.Cell>
90
- <StatusPill status={job.latest_status} />
91
- </Table.Cell>
92
- <Table.Cell>
93
- <ResponseCell
94
- response={job.latest_event_response}
95
- status={job.latest_status}
96
- />
97
- </Table.Cell>
98
- <Table.Cell>
99
- {job.latest_event_at
100
- ? moment(job.latest_event_at).fromNow()
101
- : ""}
102
- </Table.Cell>
103
- </Table.Row>
104
- ))
105
- ) : (
106
- <Table.Row>
107
- <Table.Cell colSpan={5}>
108
- <FormattedMessage
109
- id="implementations.bulkUpload.job.table.no_jobs"
110
- defaultMessage="No jobs found."
111
- />
112
- </Table.Cell>
113
- </Table.Row>
114
- )}
115
- </Table.Body>
116
- </Table>
117
- ) : null}
118
- {loading ? (
119
- <Dimmer active inverted>
120
- <Loader />
121
- </Dimmer>
122
- ) : null}
123
- </Segment>
124
- </Dimmer.Dimmable>
125
- </Segment>
126
- </>
127
- );
128
- }
@@ -1,28 +0,0 @@
1
- import { render, waitForLoad } from "@truedat/test/render";
2
- import { ImplementationUploadJobBreadcrumbs } from "../ImplementationUploadJobBreadcrumbs";
3
-
4
- describe("ImplementationUploadJobBreadcrumbs", () => {
5
- it("renders breadcrumbs without filename", async () => {
6
- const rendered = render(<ImplementationUploadJobBreadcrumbs />);
7
- await waitForLoad(rendered);
8
-
9
- expect(
10
- rendered.getByText(/sidemenu.implementations_upload_jobs/i)
11
- ).toBeInTheDocument();
12
- expect(rendered.container).toMatchSnapshot();
13
- });
14
-
15
- it("renders breadcrumbs with filename", async () => {
16
- const filename = "test.csv";
17
- const rendered = render(
18
- <ImplementationUploadJobBreadcrumbs filename={filename} />
19
- );
20
- await waitForLoad(rendered);
21
-
22
- expect(
23
- rendered.getByText(/sidemenu.implementations_upload_jobs/i)
24
- ).toBeInTheDocument();
25
- expect(rendered.getByText(filename)).toBeInTheDocument();
26
- expect(rendered.container).toMatchSnapshot();
27
- });
28
- });
@@ -1,112 +0,0 @@
1
- import { render, waitForLoad } from "@truedat/test/render";
2
- import userEvent from "@testing-library/user-event";
3
- import { waitFor } from "@testing-library/react";
4
-
5
- import { useImplementationsUploadJob } from "../../hooks/useImplementations";
6
- import ImplementationsUploadJob from "../ImplementationsUploadJob";
7
-
8
- jest.mock("../../hooks/useImplementations", () => ({
9
- useImplementationsUploadJob: jest.fn(),
10
- }));
11
- jest.mock("react-router", () => ({
12
- ...jest.requireActual("react-router"),
13
- useParams: () => ({ id: "123" }),
14
- }));
15
-
16
- describe("<ImplementationsUploadJob />", () => {
17
- const user = userEvent.setup({ delay: null });
18
-
19
- beforeEach(() => {
20
- useImplementationsUploadJob.mockReset();
21
- });
22
-
23
- it("renders loading state", async () => {
24
- useImplementationsUploadJob.mockReturnValue({
25
- loading: true,
26
- data: null,
27
- });
28
-
29
- const rendered = render(<ImplementationsUploadJob />);
30
-
31
- expect(rendered.container.querySelector(".ui.loader")).toBeInTheDocument();
32
- });
33
-
34
- it("renders empty state when no events", async () => {
35
- useImplementationsUploadJob.mockReturnValue({
36
- loading: false,
37
- data: {
38
- data: {
39
- filename: "test.xlsx",
40
- hash: "abc123",
41
- latest_status: "COMPLETED",
42
- latest_event_at: "2023-01-01T00:00:00Z",
43
- events: [],
44
- },
45
- },
46
- });
47
-
48
- const rendered = render(<ImplementationsUploadJob />);
49
- await waitForLoad(rendered);
50
-
51
- expect(rendered.getAllByText(/test.xlsx/i)).toHaveLength(2);
52
- expect(rendered.getByText(/abc123/i)).toBeInTheDocument();
53
- expect(rendered.getByText(/no events found/i)).toBeInTheDocument();
54
- });
55
-
56
- it("renders events grouped by status", async () => {
57
- useImplementationsUploadJob.mockReturnValue({
58
- loading: false,
59
- data: {
60
- data: {
61
- filename: "test.xlsx",
62
- hash: "abc123",
63
- latest_status: "COMPLETED",
64
- latest_event_at: "2023-01-01T00:00:00Z",
65
- events: [
66
- {
67
- status: "INFO",
68
- inserted_at: "2023-01-01T00:00:00Z",
69
- response: { type: "info_message" },
70
- },
71
- {
72
- status: "ERROR",
73
- inserted_at: "2023-01-01T00:00:00Z",
74
- response: { type: "error_message" },
75
- },
76
- {
77
- status: "COMPLETED",
78
- inserted_at: "2023-01-01T00:00:00Z",
79
- response: { type: "completed_message" },
80
- },
81
- ],
82
- },
83
- },
84
- });
85
-
86
- const rendered = render(<ImplementationsUploadJob />);
87
- await waitForLoad(rendered);
88
-
89
- // Check group headers exist
90
- expect(rendered.getByText(/info \(1\)/i)).toBeInTheDocument();
91
- expect(rendered.getByText(/error \(1\)/i)).toBeInTheDocument();
92
-
93
- // Expand INFO group
94
- await user.click(rendered.getByText(/info \(1\)/i));
95
- await waitFor(() => {
96
- expect(rendered.getByText(/info_message/i)).toBeInTheDocument();
97
- });
98
-
99
- // Expand ERROR group
100
- await user.click(rendered.getByText(/error \(1\)/i));
101
- await waitFor(() => {
102
- expect(rendered.getByText(/error_message/i)).toBeInTheDocument();
103
- });
104
-
105
- // COMPLETED events are always visible
106
- expect(
107
- rendered.getAllByText(
108
- /implementations.bulkUpload.event.status.COMPLETED/i
109
- )
110
- ).toHaveLength(2);
111
- });
112
- });
@@ -1,60 +0,0 @@
1
- import { render, waitForLoad } from "@truedat/test/render";
2
- import { useImplementationsUploadJobs } from "../../hooks/useImplementations";
3
- import ImplementationsUploadJobs from "../ImplementationsUploadJobs";
4
-
5
- jest.mock("../../hooks/useImplementations", () => ({
6
- useImplementationsUploadJobs: jest.fn(),
7
- }));
8
-
9
- describe("<ImplementationsUploadJobs />", () => {
10
- it("renders loading state", async () => {
11
- useImplementationsUploadJobs.mockReturnValue({
12
- loading: true,
13
- data: null,
14
- });
15
-
16
- const rendered = render(<ImplementationsUploadJobs />);
17
-
18
- expect(rendered.container.querySelector(".ui.loader")).toBeInTheDocument();
19
- });
20
-
21
- it("renders empty state", async () => {
22
- useImplementationsUploadJobs.mockReturnValue({
23
- loading: false,
24
- data: {
25
- data: [],
26
- },
27
- });
28
-
29
- const rendered = render(<ImplementationsUploadJobs />);
30
- await waitForLoad(rendered);
31
-
32
- expect(rendered.getByText(/No jobs found./i)).toBeInTheDocument();
33
- });
34
-
35
- it("renders jobs list", async () => {
36
- const mockJobs = [
37
- {
38
- id: 1,
39
- filename: "test.csv",
40
- latest_status: "completed",
41
- latest_event_response: "Success",
42
- latest_event_at: "2023-01-01T00:00:00Z",
43
- },
44
- ];
45
-
46
- useImplementationsUploadJobs.mockReturnValue({
47
- loading: false,
48
- data: {
49
- data: mockJobs,
50
- },
51
- });
52
-
53
- const rendered = render(<ImplementationsUploadJobs />);
54
- await waitForLoad(rendered);
55
-
56
- expect(rendered.getByText(/test.csv/i)).toBeInTheDocument();
57
- expect(rendered.getByText(/completed/i)).toBeInTheDocument();
58
- expect(rendered.getByText(/success/i)).toBeInTheDocument();
59
- });
60
- });
@@ -1,42 +0,0 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
- exports[`ImplementationUploadJobBreadcrumbs renders breadcrumbs with filename 1`] = `
4
- <div>
5
- <div
6
- class="ui breadcrumb"
7
- >
8
- <a
9
- class="section"
10
- data-discover="true"
11
- href="/implementations/uploadJobs"
12
- >
13
- sidemenu.implementations_upload_jobs
14
- </a>
15
- <i
16
- aria-hidden="true"
17
- class="right angle icon divider"
18
- />
19
- <div
20
- class="active section"
21
- >
22
- test.csv
23
- </div>
24
- </div>
25
- </div>
26
- `;
27
-
28
- exports[`ImplementationUploadJobBreadcrumbs renders breadcrumbs without filename 1`] = `
29
- <div>
30
- <div
31
- class="ui breadcrumb"
32
- >
33
- <a
34
- class="section"
35
- data-discover="true"
36
- href="/implementations/uploadJobs"
37
- >
38
- sidemenu.implementations_upload_jobs
39
- </a>
40
- </div>
41
- </div>
42
- `;
@@ -1,105 +0,0 @@
1
- import { render } from "@truedat/test/render";
2
- import { StatusPill, ResponseCell } from "../implementationsUploadJobParser";
3
-
4
- describe("<StatusPill />", () => {
5
- it("renders with correct color based on status", async () => {
6
- const statuses = ["COMPLETED", "FAILED", "ERROR", "INFO", "OTHER"];
7
- const expectedColors = ["green", "red", "red", "blue", "grey"];
8
-
9
- statuses.forEach((status, index) => {
10
- const rendered = render(<StatusPill status={status} />);
11
- const label = rendered.container.querySelector(".ui.label");
12
- expect(label).toHaveClass(expectedColors[index]);
13
- expect(
14
- rendered.getByText(
15
- new RegExp(`implementations.bulkUpload.event.status.${status}`, "i")
16
- )
17
- ).toBeInTheDocument();
18
- });
19
- });
20
- });
21
-
22
- describe("<ResponseCell />", () => {
23
- it("returns null when response is empty", () => {
24
- const rendered = render(
25
- <ResponseCell response={null} status="COMPLETED" />
26
- );
27
- expect(rendered.container.firstChild).toBeNull();
28
-
29
- const rendered2 = render(<ResponseCell response={{}} status="COMPLETED" />);
30
- expect(rendered2.container.firstChild).toBeNull();
31
- });
32
-
33
- it("renders FAILED status with message", () => {
34
- const response = { message: "missing_required_headers" };
35
- const rendered = render(
36
- <ResponseCell response={response} status="FAILED" />
37
- );
38
- expect(
39
- rendered.getByText(
40
- /implementations.bulkUpload.error.missing_required_headers/i
41
- )
42
- ).toBeInTheDocument();
43
- });
44
-
45
- it("renders ERROR status with details", () => {
46
- const response = {
47
- type: "missing_required_headers",
48
- details: {
49
- missing_headers: ["header1", "header2"],
50
- },
51
- };
52
- const rendered = render(
53
- <ResponseCell response={response} status="ERROR" />
54
- );
55
- expect(
56
- rendered.getByText(
57
- /implementations.bulkUpload.error.missing_required_headers/i
58
- )
59
- ).toBeInTheDocument();
60
- expect(rendered.getByText(/header1, header2/i)).toBeInTheDocument();
61
- });
62
-
63
- it("renders COMPLETED status with summary", () => {
64
- const response = {
65
- insert_count: 2,
66
- update_count: 1,
67
- error_count: 0,
68
- };
69
- const rendered = render(
70
- <ResponseCell response={response} status="COMPLETED" />
71
- );
72
- expect(
73
- rendered.getByText(/implementations.bulkUpload.result.summary.created/i)
74
- ).toBeInTheDocument();
75
- expect(
76
- rendered.getByText(/implementations.bulkUpload.result.summary.updated/i)
77
- ).toBeInTheDocument();
78
- });
79
-
80
- it("renders INFO status with implementation details", () => {
81
- const response = {
82
- type: "implementation_updated",
83
- details: {
84
- id: 123,
85
- changes: {
86
- name: "new name",
87
- },
88
- },
89
- };
90
- const rendered = render(<ResponseCell response={response} status="INFO" />);
91
- expect(
92
- rendered.getByText(
93
- /implementations.bulkUpload.info.implementation_updated/i
94
- )
95
- ).toBeInTheDocument();
96
- });
97
-
98
- it("renders default case for unknown status", () => {
99
- const response = "some text";
100
- const rendered = render(
101
- <ResponseCell response={response} status="UNKNOWN" />
102
- );
103
- expect(rendered.getByText(/some text/i)).toBeInTheDocument();
104
- });
105
- });
@@ -1,292 +0,0 @@
1
- import _ from "lodash/fp";
2
- import { useState } from "react";
3
- import { FormattedMessage, useIntl } from "react-intl";
4
- import { Label } from "semantic-ui-react";
5
- import { RichTextEditor } from "@truedat/core/components";
6
-
7
- import RuleImplementationLink from "./RuleImplementationLink";
8
-
9
- export const StatusPill = ({ status }) => (
10
- <Label
11
- color={_.cond([
12
- [_.eq("COMPLETED"), _.constant("green")],
13
- [_.eq("FAILED"), _.constant("red")],
14
- [_.eq("ERROR"), _.constant("red")],
15
- [_.eq("INFO"), _.constant("blue")],
16
- [_.stubTrue, _.constant("grey")],
17
- ])(status)}
18
- size="small"
19
- >
20
- <FormattedMessage
21
- id={`implementations.bulkUpload.event.status.${status}`}
22
- />
23
- </Label>
24
- );
25
-
26
- export const ResponseCell = ({ response, status }) => {
27
- if (!response || (_.isObject(response) && _.isEmpty(response))) return null;
28
-
29
- switch (status) {
30
- case "FAILED":
31
- return (
32
- <FormattedMessage
33
- id={`implementations.bulkUpload.error.${response.message}`}
34
- />
35
- );
36
- case "ERROR":
37
- return <ErrorDetail response={response} />;
38
- case "COMPLETED":
39
- return <CompletedDetail response={response} />;
40
- case "INFO":
41
- return <InfoDetail response={response} />;
42
- default:
43
- return (
44
- <pre
45
- style={{
46
- whiteSpace: "pre-wrap",
47
- wordBreak: "break-all",
48
- margin: 0,
49
- }}
50
- >
51
- {typeof response === "string"
52
- ? response
53
- : JSON.stringify(response, null, 2)}
54
- </pre>
55
- );
56
- }
57
- };
58
-
59
- const SheetAndLine = ({ response }) => (
60
- <Label size="small">
61
- <span>{response.sheet}</span>
62
- {response.row_number ? (
63
- <span>
64
- {" "}
65
- -{" "}
66
- <FormattedMessage id="implementations.bulkUpload.result.prop.row_number" />
67
- : {response.row_number}
68
- </span>
69
- ) : null}
70
- </Label>
71
- );
72
-
73
- const CellHeader = ({ header, response }) => (
74
- <div style={{ display: "flex", justifyContent: "space-between" }}>
75
- <b>
76
- <FormattedMessage id={header} />
77
- </b>
78
- <div>
79
- <SheetAndLine response={response} />
80
- </div>
81
- </div>
82
- );
83
-
84
- const ErrorDetail = ({ response }) => {
85
- const { formatMessage } = useIntl();
86
-
87
- if (!response || !response.type) {
88
- return null;
89
- }
90
- const { type } = response;
91
- const messagePrefix = "implementations.bulkUpload.detail";
92
-
93
- const detailBuilders = {
94
- missing_required_headers: (details) => [
95
- [
96
- formatMessage({ id: `${messagePrefix}.headers` }),
97
- details.missing_headers?.join(", "),
98
- ],
99
- ],
100
- invalid_template_name: (details) => [
101
- [
102
- formatMessage({ id: `${messagePrefix}.templateName` }),
103
- details.template_name,
104
- ],
105
- ],
106
- invalid_domain_external_id: (details) => [
107
- [
108
- formatMessage({ id: `${messagePrefix}.domainExternalId` }),
109
- details.domain_external_id,
110
- ],
111
- ],
112
- invalid_associated_rule: (details) => [
113
- [formatMessage({ id: `${messagePrefix}.ruleName` }), details.rule_name],
114
- ],
115
- deprecated_implementation: (details) => [
116
- [formatMessage({ id: `${messagePrefix}.implementation`, }), details.implementation_key],
117
- ],
118
- implementation_creation_error: (details) =>
119
- details?.map(([field, [error]]) => [field, error]),
120
- duplicate_field_names: (details) => [
121
- [formatMessage({ id: `${messagePrefix}.duplicatedNames` }), details?.duplicate_fields?.join(", ")],
122
- ],
123
- };
124
-
125
- const detailBuilder = _.propOr(() => { }, type)(detailBuilders);
126
-
127
- return (
128
- <div>
129
- <CellHeader
130
- header={`implementations.bulkUpload.error.${type}`}
131
- response={response}
132
- />
133
-
134
- {response.details
135
- ? detailBuilder(response.details).map(([key, value], idx) => (
136
- <div key={idx} style={{ paddingLeft: "10px" }}>
137
- <span
138
- style={{
139
- fontSize: "1em",
140
- fontWeight: "bold",
141
- paddingRight: "5px",
142
- }}
143
- >
144
- {key}:
145
- </span>
146
- {value}
147
- </div>
148
- ))
149
- : null}
150
- </div>
151
- );
152
- };
153
- const summaryItems = [
154
- {
155
- key: "invalid_sheet_count",
156
- messageId: "implementations.bulkUpload.result.summary.invalid_sheets",
157
- },
158
- {
159
- key: "insert_count",
160
- messageId: "implementations.bulkUpload.result.summary.created",
161
- },
162
- {
163
- key: "update_count",
164
- messageId: "implementations.bulkUpload.result.summary.updated",
165
- },
166
- {
167
- key: "unchanged_count",
168
- messageId: "implementations.bulkUpload.result.summary.unchanged",
169
- },
170
- {
171
- key: "error_count",
172
- messageId: "implementations.bulkUpload.result.summary.errors",
173
- },
174
- ];
175
- const CompletedDetail = ({ response }) => {
176
- const items = _.flow([
177
- _.filter((item) => _.get(item.key, response) > 0),
178
- (items) => {
179
- const result = [];
180
- for (let i = 0; i < items.length; i++) {
181
- const item = items[i];
182
- result.push(
183
- <span key={item.key}>
184
- <FormattedMessage id={item.messageId} />
185
- {`: ${response[item.key]}`}
186
- {i < items.length - 1 && <span>&nbsp;|&nbsp;</span>}
187
- </span>
188
- );
189
- }
190
- return result;
191
- }
192
- ])(summaryItems);
193
-
194
- if (items.length === 0) return null;
195
-
196
- return (
197
- <span>&nbsp;|&nbsp;{items}&nbsp;|&nbsp;</span>
198
- );
199
- };
200
-
201
- const InfoDetail = ({ response }) =>
202
- !response || !response.type ? null : (
203
- <div>
204
- <CellHeader
205
- header={`implementations.bulkUpload.info.${response.type}`}
206
- response={response}
207
- />
208
- {response.details ? (
209
- <div>
210
- <RuleImplementationLink {...response.details} />{" "}
211
- </div>
212
- ) : null}
213
- <ChangesDetail changes={response?.details?.changes} />
214
- </div>
215
- );
216
- const ChangesDetail = ({
217
- changes,
218
- header = "implementations.bulkUpload.result.prop.changes",
219
- }) => {
220
- const [isExpanded, setIsExpanded] = useState(false);
221
- if (!changes) return null;
222
- const changesList = _.toPairs(changes);
223
-
224
- return (
225
- <div>
226
- <div
227
- onClick={() => setIsExpanded(!isExpanded)}
228
- style={{
229
- cursor: "pointer",
230
- display: "flex",
231
- gap: "5px",
232
- alignItems: "center",
233
- }}
234
- >
235
- <span>{isExpanded ? "▼" : "▶"}</span>
236
- <span>
237
- <FormattedMessage id={header} />
238
- </span>
239
- <span>({_.size(changesList)}) :</span>
240
- </div>
241
- {isExpanded &&
242
- changesList.map(([key, value]) => (
243
- <div key={key} style={{ paddingLeft: "10px" }}>
244
- {key == "df_content" ? (
245
- <ChangesDetail
246
- changes={value}
247
- header={`ruleImplementations.props.${key}`}
248
- />
249
- ) : (
250
- <>
251
- <span
252
- style={{
253
- fontSize: "1em",
254
- fontWeight: "bold",
255
- paddingRight: "5px",
256
- }}
257
- >
258
- <FormattedMessage
259
- id={`ruleImplementations.props.${key}`}
260
- defaultMessage={key}
261
- />
262
- :
263
- </span>
264
- {formatValue(value, key)}
265
- </>
266
- )}
267
- </div>
268
- ))}
269
- </div>
270
- );
271
- };
272
-
273
-
274
- const formatValue = (value, key) => {
275
- if (_.has("value")(value)) return formatValue(value.value, key);
276
- if (_.isArray(value)) {
277
- return _.flow(_.map((v) => formatValue(v, key)), _.join(", "))(value);
278
- }
279
- if (_.isObject(value)) {
280
- if (_.has("document")(value))
281
- return <RichTextEditor readOnly value={value} />;
282
- if (_.has("url_value")(value))
283
- return `[${value.url_name}] (${value.url_value})`;
284
- if (_.has("name")(value))
285
- return value.name;
286
- if (_.has("external_id")(value))
287
- return value.external_id;
288
-
289
- return _.flow(_.keys, _.join(", "))(value);
290
- }
291
- return value;
292
- };