@truedat/dq 7.10.4 → 7.11.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.
Files changed (52) hide show
  1. package/package.json +3 -3
  2. package/src/api.js +12 -0
  3. package/src/components/ImplementationSearchResults.js +24 -42
  4. package/src/components/ImplementationUploadJobBreadcrumbs.js +25 -0
  5. package/src/components/Implementations.js +31 -17
  6. package/src/components/ImplementationsRoutes.js +9 -0
  7. package/src/components/ImplementationsUploadButton.js +38 -50
  8. package/src/components/ImplementationsUploadJob.js +217 -0
  9. package/src/components/ImplementationsUploadJobs.js +128 -0
  10. package/src/components/RuleFormImplementations.js +29 -10
  11. package/src/components/RuleImplementationActions.js +10 -20
  12. package/src/components/RuleImplementationsActions.js +15 -37
  13. package/src/components/RuleImplementationsDownload.js +26 -31
  14. package/src/components/RuleImplementationsDownloadXlsx.js +47 -0
  15. package/src/components/RuleImplementationsLabelResults.js +30 -39
  16. package/src/components/RuleImplementationsOptions.js +5 -3
  17. package/src/components/RuleImplementationsTable.js +7 -3
  18. package/src/components/RuleRoutes.js +1 -4
  19. package/src/components/SimpleRuleImplementationsTable.js +68 -0
  20. package/src/components/__tests__/ImplementationSearchResults.spec.js +32 -4
  21. package/src/components/__tests__/ImplementationUploadJobBreadcrumbs.spec.js +28 -0
  22. package/src/components/__tests__/Implementations.spec.js +43 -0
  23. package/src/components/__tests__/ImplementationsUploadButton.spec.js +67 -40
  24. package/src/components/__tests__/ImplementationsUploadJob.spec.js +112 -0
  25. package/src/components/__tests__/ImplementationsUploadJobs.spec.js +60 -0
  26. package/src/components/__tests__/RuleImplementationsActions.spec.js +71 -56
  27. package/src/components/__tests__/RuleImplementationsOptions.spec.js +28 -3
  28. package/src/components/__tests__/RuleImplementationsTable.spec.js +24 -0
  29. package/src/components/__tests__/__snapshots__/ImplementationSearchResults.spec.js.snap +113 -46
  30. package/src/components/__tests__/__snapshots__/ImplementationUploadJobBreadcrumbs.spec.js.snap +42 -0
  31. package/src/components/__tests__/__snapshots__/Implementations.spec.js.snap +125 -24
  32. package/src/components/__tests__/__snapshots__/RuleImplementationsActions.spec.js.snap +4 -8
  33. package/src/components/__tests__/__snapshots__/RuleImplementationsOptions.spec.js.snap +5 -8
  34. package/src/components/__tests__/implementationsUploadJobParser.spec.js +105 -0
  35. package/src/components/implementationsUploadJobParser.js +276 -0
  36. package/src/components/index.js +0 -2
  37. package/src/hooks/useImplementations.js +80 -0
  38. package/src/reducers/index.js +2 -0
  39. package/src/reducers/ruleImplementationSelectedFilter.js +1 -1
  40. package/src/reducers/ruleImplementationsDownloadingXlsx.js +14 -0
  41. package/src/routines.js +3 -0
  42. package/src/sagas/downloadRuleImplementationsXlsx.js +52 -0
  43. package/src/sagas/index.js +3 -0
  44. package/src/components/RuleImplementationFilters.js +0 -25
  45. package/src/components/RuleImplementationSelectedFilters.js +0 -99
  46. package/src/components/RuleImplementationsFromRuleLoader.js +0 -60
  47. package/src/components/RuleImplementationsPagination.js +0 -18
  48. package/src/components/RuleImplementationsSearch.js +0 -39
  49. package/src/components/__tests__/RuleImplementationsFromRuleLoader.spec.js +0 -63
  50. package/src/components/__tests__/RuleImplementationsSearch.spec.js +0 -29
  51. package/src/components/__tests__/__snapshots__/RuleImplementationsFromRuleLoader.spec.js.snap +0 -3
  52. package/src/components/__tests__/__snapshots__/RuleImplementationsSearch.spec.js.snap +0 -50
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/dq",
3
- "version": "7.10.4",
3
+ "version": "7.11.1",
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": "7.10.4",
56
+ "@truedat/test": "7.11.1",
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": "0dcedbfaed3964ee02a3d2c175e0f96e2fbc9316"
89
+ "gitHead": "efc0969708811b5e4fc2ea39e628a1b2691e4042"
90
90
  }
package/src/api.js CHANGED
@@ -14,6 +14,14 @@ const API_RULE_FILTERS_SEARCH = "/api/rule_filters/search";
14
14
  const API_RULE_IMPLEMENTATION = "/api/rule_implementations/:id";
15
15
  const API_RULE_IMPLEMENTATIONS = "/api/rule_implementations";
16
16
  const API_RULE_IMPLEMENTATIONS_CSV = "/api/rule_implementations/csv";
17
+ const API_RULE_IMPLEMENTATIONS_XLSX_DOWNLOAD =
18
+ "/api/rule_implementations/xlsx/download";
19
+ const API_RULE_IMPLEMENTATIONS_XLSX_UPLOAD =
20
+ "/api/rule_implementations/xlsx/upload";
21
+ const API_RULE_IMPLEMENTATIONS_XLSX_UPLOAD_JOBS =
22
+ "/api/rule_implementations/xlsx/upload_jobs";
23
+ const API_RULE_IMPLEMENTATIONS_XLSX_UPLOAD_JOB =
24
+ "/api/rule_implementations/xlsx/upload_jobs/:id";
17
25
  const API_RULE_IMPLEMENTATIONS_FROM_RULE =
18
26
  "/api/rules/:id/rule_implementations";
19
27
  const API_RULE_IMPLEMENTATIONS_SEARCH = "/api/rule_implementations/search";
@@ -42,6 +50,10 @@ export {
42
50
  API_RULE_IMPLEMENTATION,
43
51
  API_RULE_IMPLEMENTATIONS,
44
52
  API_RULE_IMPLEMENTATIONS_CSV,
53
+ API_RULE_IMPLEMENTATIONS_XLSX_DOWNLOAD,
54
+ API_RULE_IMPLEMENTATIONS_XLSX_UPLOAD,
55
+ API_RULE_IMPLEMENTATIONS_XLSX_UPLOAD_JOBS,
56
+ API_RULE_IMPLEMENTATIONS_XLSX_UPLOAD_JOB,
45
57
  API_RULE_IMPLEMENTATIONS_FROM_RULE,
46
58
  API_RULE_IMPLEMENTATIONS_SEARCH,
47
59
  API_RULE_IMPLEMENTATION_FILTERS_SEARCH,
@@ -5,40 +5,33 @@ import { connect } from "react-redux";
5
5
  import { Dimmer, Loader, Segment } from "semantic-ui-react";
6
6
  import { useActiveRoute } from "@truedat/core/hooks";
7
7
  import { IMPLEMENTATIONS_PENDING } from "@truedat/core/routes";
8
- import { getExecutionQuery, getRuleImplementationColumns } from "../selectors";
8
+ import SearchWidget from "@truedat/core/search/SearchWidget";
9
+ import { useSearchContext } from "@truedat/core/search/SearchContext";
10
+ import Pagination from "@truedat/core/search/Pagination";
11
+ import { getRuleImplementationColumns } from "../selectors";
9
12
  import RuleImplementationsActions from "./RuleImplementationsActions";
10
13
  import RuleImplementationsLabelResults from "./RuleImplementationsLabelResults";
11
- import RuleImplementationsPagination from "./RuleImplementationsPagination";
12
- import RuleImplementationsSearch from "./RuleImplementationsSearch";
13
14
  import RuleImplementationsTable from "./RuleImplementationsTable";
14
- import RuleImplementationSelectedFilters from "./RuleImplementationSelectedFilters";
15
15
 
16
- const usePrevious = (value) => {
17
- const ref = useRef();
18
- useEffect(() => {
19
- ref.current = value;
20
- });
21
- return ref.current;
22
- };
16
+ export const ImplementationSearchResults = ({ embedded, role, columns }) => {
17
+ const { searchData, loading, toggleHiddenFilterValue, setOnSearchChange } =
18
+ useSearchContext();
23
19
 
24
- export const ImplementationSearchResults = ({
25
- embedded,
26
- implementationQuery,
27
- role,
28
- loading,
29
- ruleImplementations,
30
- columns,
31
- }) => {
32
- const [executeImplementationsOn, setMode] = useState(
33
- _.matches({ filters: { executable: [true] } })(implementationQuery)
34
- );
35
- const [selectedImplementations, setSelectedImplementations] = useState([]);
36
- const previousQuery = usePrevious(implementationQuery);
37
- const clean = !_.isEqual(implementationQuery)(previousQuery);
20
+ const ruleImplementations = searchData?.data;
38
21
 
22
+ const [executeImplementationsOn, setExecuteImplementationsOn] =
23
+ useState(false);
24
+ const setMode = (mode) => {
25
+ setExecuteImplementationsOn(mode);
26
+ toggleHiddenFilterValue({
27
+ filter: "executable",
28
+ value: mode ? [true] : [],
29
+ });
30
+ };
31
+ const [selectedImplementations, setSelectedImplementations] = useState([]);
39
32
  useEffect(() => {
40
- setSelectedImplementations([]);
41
- }, [clean]);
33
+ setOnSearchChange(() => () => setSelectedImplementations([]));
34
+ }, []);
42
35
 
43
36
  const allChecked = () => {
44
37
  const ids = _.map(_.prop("id"))(ruleImplementations);
@@ -86,18 +79,16 @@ export const ImplementationSearchResults = ({
86
79
  : _.reject(({ name }) => name === "status")(columns);
87
80
 
88
81
  return (
89
- <Segment attached="bottom">
82
+ <Segment border="1px red" attached="bottom">
90
83
  {embedded ? null : (
91
84
  <RuleImplementationsActions
92
85
  executeImplementationsOn={executeImplementationsOn}
93
- implementationQuery={implementationQuery}
94
86
  role={role}
95
87
  setMode={setMode}
96
88
  selectedImplementations={selectedImplementations}
97
89
  />
98
90
  )}
99
- <RuleImplementationsSearch />
100
- <RuleImplementationSelectedFilters />
91
+ <SearchWidget />
101
92
  <Dimmer.Dimmable dimmed={loading}>
102
93
  {loading ? (
103
94
  <Dimmer active inverted>
@@ -117,30 +108,21 @@ export const ImplementationSearchResults = ({
117
108
  selectedImplementations={selectedImplementations}
118
109
  columns={filteredColumns}
119
110
  />
120
- <RuleImplementationsPagination />
111
+ <Pagination />
121
112
  </Dimmer.Dimmable>
122
113
  </Segment>
123
114
  );
124
115
  };
125
116
 
126
117
  ImplementationSearchResults.propTypes = {
127
- implementationQuery: PropTypes.object,
128
- role: PropTypes.string,
129
- loading: PropTypes.bool,
130
- ruleImplementations: PropTypes.array,
131
118
  embedded: PropTypes.bool,
119
+ role: PropTypes.string,
132
120
  columns: PropTypes.array,
133
121
  };
134
122
 
135
123
  const mapStateToProps = (state, props) => ({
136
124
  columns: getRuleImplementationColumns(state),
137
- implementationQuery: getExecutionQuery({
138
- ...state,
139
- ruleImplementationDefaultFilters: _.propOr({}, "defaultFilters")(props),
140
- }),
141
125
  role: state.authentication?.role,
142
- loading: state.ruleImplementationsLoading,
143
- ruleImplementations: state.ruleImplementations,
144
126
  });
145
127
 
146
128
  export default connect(mapStateToProps)(ImplementationSearchResults);
@@ -0,0 +1,25 @@
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,28 +1,42 @@
1
1
  import { lazy } from "react";
2
2
  import PropTypes from "prop-types";
3
3
  import { Segment } from "semantic-ui-react";
4
+ import { SearchContextProvider } from "@truedat/core/search/SearchContext";
5
+ import {
6
+ useRuleImplementationFilters,
7
+ useRuleImplementationSearch,
8
+ } from "../hooks/useImplementations";
9
+
4
10
  import ImplementationsHeader from "./ImplementationsHeader";
5
11
  import ImplementationFiltersLoader from "./ImplementationFiltersLoader";
6
12
  import ImplementationSearchResults from "./ImplementationSearchResults";
7
- import RuleImplementationsLoader from "./RuleImplementationsLoader";
8
13
 
9
- const UserSearchFiltersLoader = lazy(
10
- () => import("@truedat/dd/components/UserSearchFiltersLoader")
11
- );
14
+ export const Implementations = ({ defaultFilters }) => {
15
+ const searchProps = {
16
+ initialSortColumn: "implementation_key.raw",
17
+ initialSortDirection: "ascending",
18
+ useSearch: useRuleImplementationSearch,
19
+ useFilters: useRuleImplementationFilters,
20
+ pageSize: 20,
21
+ userFiltersType: "user_search_filters",
22
+ userFilterScope: "rule_implementation",
23
+ defaultFilters,
24
+ };
12
25
 
13
- export const Implementations = ({ defaultFilters }) => (
14
- <>
15
- <ImplementationFiltersLoader defaultFilters={defaultFilters} />
16
- <UserSearchFiltersLoader scope="rule_implementation" />
17
- <RuleImplementationsLoader defaultFilters={defaultFilters} />
18
- <Segment>
19
- <ImplementationsHeader />
20
- <Segment attached="bottom">
21
- <ImplementationSearchResults defaultFilters={defaultFilters} />
22
- </Segment>
23
- </Segment>
24
- </>
25
- );
26
+ return (
27
+ <>
28
+ <SearchContextProvider {...searchProps}>
29
+ <ImplementationFiltersLoader defaultFilters={defaultFilters} />
30
+ <Segment>
31
+ <ImplementationsHeader />
32
+ <Segment attached="bottom">
33
+ <ImplementationSearchResults defaultFilters={defaultFilters} />
34
+ </Segment>
35
+ </Segment>
36
+ </SearchContextProvider>
37
+ </>
38
+ );
39
+ };
26
40
 
27
41
  Implementations.propTypes = {
28
42
  defaultFilters: PropTypes.object,
@@ -51,6 +51,8 @@ import RuleResultDetails from "./RuleResultDetails";
51
51
  import RuleResultRemediationLoader from "./RuleResultRemediationLoader";
52
52
  import RuleResultsRoutes from "./RuleResultsRoutes";
53
53
  import RuleSubscriptionLoader from "./RuleSubscriptionLoader";
54
+ import ImplementationsUploadJobs from "./ImplementationsUploadJobs";
55
+ import ImplementationsUploadJob from "./ImplementationsUploadJob";
54
56
 
55
57
  const TemplatesLoader = React.lazy(
56
58
  () => import("@truedat/core/components/TemplatesLoader")
@@ -104,6 +106,13 @@ export const ImplementationsRoutes = ({
104
106
  index
105
107
  element={<Implementations defaultFilters={{ status: ["published"] }} />}
106
108
  />
109
+ <Route
110
+ // IMPLEMENTATIONS_UPLOAD_JOBS = "/implementations/uploadJobs";
111
+ path="/uploadJobs"
112
+ >
113
+ <Route index element={<ImplementationsUploadJobs />} />
114
+ <Route path=":id" element={<ImplementationsUploadJob />} />
115
+ </Route>
107
116
 
108
117
  <Route
109
118
  // IMPLEMENTATIONS_PENDING = "/implementations/pending";
@@ -1,42 +1,52 @@
1
- import _ from "lodash/fp";
2
- import PropTypes from "prop-types";
3
- import { connect } from "react-redux";
1
+ import { useWebContext } from "@truedat/core/webContext";
4
2
  import { Dropdown } from "semantic-ui-react";
5
3
  import { FormattedMessage, useIntl } from "react-intl";
6
4
  import { UploadModal } from "@truedat/core/components";
7
- import { API_RULE_IMPLEMENTATIONS_UPLOAD } from "../api";
8
- import { uploadImplementations } from "../routines";
5
+ import { linkTo } from "@truedat/core/routes";
6
+ import { useSearchContext } from "@truedat/core/search/SearchContext";
7
+ import { useImplementationsUpload } from "../hooks/useImplementations";
9
8
 
10
- const uploadAction = {
11
- method: "POST",
12
- href: API_RULE_IMPLEMENTATIONS_UPLOAD,
13
- };
9
+ const buildAlertResult =
10
+ (setAlertMessage) =>
11
+ ({ data }) => {
12
+ setAlertMessage({
13
+ error: false,
14
+ header: "implementations.bulkUpload.alert.success.header",
15
+ icon: "check",
16
+ color: "blue",
17
+ text: "",
18
+ anchor: {
19
+ reference: linkTo.IMPLEMENTATIONS_UPLOAD_JOB({ id: data.job_id }),
20
+ label: "implementations.bulkUpload.job.header",
21
+ },
22
+ });
23
+ };
14
24
 
15
- export const ImplementationsUploadButton = ({
16
- canAutoPublish,
17
- uploadImplementations,
18
- loading,
19
- }) => {
25
+ export const ImplementationsUploadButton = () => {
20
26
  const { formatMessage, locale } = useIntl();
27
+
28
+ const { trigger: triggerUpload, isMutating: loading } =
29
+ useImplementationsUpload();
30
+ const { setAlertMessage } = useWebContext();
31
+ const { searchData } = useSearchContext();
32
+
33
+ const alertResult = buildAlertResult(setAlertMessage);
34
+ const canAutoPublish = !!searchData?._actions?.autoPublish;
35
+
21
36
  const extraAction = {
22
37
  key: "yesWithAutoPublish",
23
38
  primary: true,
24
39
  content: formatMessage({ id: "uploadModal.accept.publish" }),
25
- onClick: (formData) => {
26
- // UploadModal is a Modal, not a Form, but sends FormData
27
- formData.append("auto_publish", true);
28
- return uploadImplementations({
29
- action: "upload",
30
- formData,
31
- lang: locale,
32
- ...uploadAction,
33
- });
40
+ onClick: (data) => {
41
+ data.append("auto_publish", canAutoPublish);
42
+ data.append("lang", locale);
43
+ triggerUpload(data).then(alertResult);
34
44
  },
35
45
  };
36
46
  return (
37
47
  <UploadModal
38
48
  icon="upload"
39
- extraAction={canAutoPublish ? extraAction : undefined}
49
+ extraAction={canAutoPublish && extraAction}
40
50
  trigger={
41
51
  <Dropdown.Item
42
52
  icon="upload"
@@ -53,34 +63,12 @@ export const ImplementationsUploadButton = ({
53
63
  <FormattedMessage id="ruleImplementations.actions.upload.confirmation.content" />
54
64
  }
55
65
  param="implementations"
56
- // handleSubmit is the onClick for the "yes" action
57
- handleSubmit={(formData) => {
58
- return uploadImplementations({
59
- action: "upload",
60
- formData,
61
- lang: locale,
62
- ...uploadAction,
63
- });
66
+ handleSubmit={(data) => {
67
+ data.append("lang", locale);
68
+ triggerUpload(data).then(alertResult);
64
69
  }}
65
70
  />
66
71
  );
67
72
  };
68
73
 
69
- ImplementationsUploadButton.propTypes = {
70
- canAutoPublish: PropTypes.bool,
71
- uploadImplementations: PropTypes.func,
72
- loading: PropTypes.bool,
73
- };
74
-
75
- const mapStateToProps = ({
76
- uploadingImplementationsFile,
77
- implementationsActions,
78
- }) => ({
79
- canAutoPublish: _.has("autoPublish")(implementationsActions),
80
- loading: uploadingImplementationsFile,
81
- implementationsActions,
82
- });
83
-
84
- export default connect(mapStateToProps, { uploadImplementations })(
85
- ImplementationsUploadButton
86
- );
74
+ export default ImplementationsUploadButton;
@@ -0,0 +1,217 @@
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
+ }