@truedat/core 8.1.1 → 8.1.4

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 (28) hide show
  1. package/package.json +3 -3
  2. package/src/api.js +2 -0
  3. package/src/components/CatalogMenu.js +2 -2
  4. package/src/components/GlossaryMenu.js +11 -15
  5. package/src/components/QualityMenu.js +34 -9
  6. package/src/components/Submenu.js +48 -7
  7. package/src/components/UploadJob.js +217 -0
  8. package/src/components/UploadJobBreadcrumbs.js +38 -0
  9. package/src/components/UploadJobParser.js +370 -0
  10. package/src/components/UploadJobs.js +136 -0
  11. package/src/components/__tests__/Submenu.spec.js +13 -0
  12. package/src/components/__tests__/UploadJob.spec.js +112 -0
  13. package/src/components/__tests__/UploadJobBreadcrumbs.spec.js +37 -0
  14. package/src/components/__tests__/UploadJobParser.spec.js +103 -0
  15. package/src/components/__tests__/UploadJobs.spec.js +60 -0
  16. package/src/components/__tests__/__snapshots__/AdminMenu.spec.js.snap +10 -0
  17. package/src/components/__tests__/__snapshots__/CatalogMenu.spec.js.snap +1 -1
  18. package/src/components/__tests__/__snapshots__/SideMenu.spec.js.snap +1 -1
  19. package/src/components/__tests__/__snapshots__/Submenu.spec.js.snap +37 -0
  20. package/src/components/__tests__/__snapshots__/UploadJobBreadcrumbs.spec.js.snap +42 -0
  21. package/src/hooks/__tests__/useActiveRoutes.spec.js +83 -0
  22. package/src/hooks/index.js +1 -0
  23. package/src/hooks/useActiveRoutes.js +27 -21
  24. package/src/hooks/useUploadJobs.js +24 -0
  25. package/src/routes.js +6 -2
  26. package/src/selectors/__tests__/getRiSubscopes.spec.js +53 -0
  27. package/src/selectors/getRiSubscopes.js +8 -0
  28. package/src/selectors/index.js +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/core",
3
- "version": "8.1.1",
3
+ "version": "8.1.4",
4
4
  "description": "Truedat Web Core",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -48,7 +48,7 @@
48
48
  "@testing-library/jest-dom": "^6.6.3",
49
49
  "@testing-library/react": "^16.3.0",
50
50
  "@testing-library/user-event": "^14.6.1",
51
- "@truedat/test": "8.1.1",
51
+ "@truedat/test": "8.1.4",
52
52
  "identity-obj-proxy": "^3.0.0",
53
53
  "jest": "^29.7.0",
54
54
  "redux-saga-test-plan": "^4.0.6"
@@ -85,5 +85,5 @@
85
85
  "slate-react": "^0.22.10",
86
86
  "swr": "^2.3.3"
87
87
  },
88
- "gitHead": "26ff4767ae2a60605d4958208ca73d91cfe7f81a"
88
+ "gitHead": "49d0d43e081d0f631f221d8f5992e390098b3630"
89
89
  }
package/src/api.js CHANGED
@@ -14,3 +14,5 @@ export const API_ACL_RESOURCE_ENTRIES = "/api/acl_entries/:type/:id";
14
14
  export const API_HIERARCHIES = "/api/hierarchies";
15
15
  export const API_HIERARCHY = "/api/hierarchies/:id";
16
16
  export const API_SYSTEMS = "/api/systems";
17
+ export const API_UPLOAD_JOBS = "/api/upload_jobs";
18
+ export const API_UPLOAD_JOB = "/api/upload_jobs/:id";
@@ -10,7 +10,7 @@ import {
10
10
  PENDING_STRUCTURE_NOTES,
11
11
  REFERENCE_DATASETS,
12
12
  STRUCTURES,
13
- STRUCTURES_UPLOAD_EVENTS,
13
+ STRUCTURE_NOTES_UPLOAD_JOBS,
14
14
  STRUCTURE_TYPES,
15
15
  SYSTEMS,
16
16
  STRUCTURE_TAGS,
@@ -29,7 +29,7 @@ const adminItems = [
29
29
 
30
30
  const structureNoteItems = [
31
31
  { name: "pending_structure_notes", routes: [PENDING_STRUCTURE_NOTES] },
32
- { name: "structures_upload_events", routes: [STRUCTURES_UPLOAD_EVENTS] },
32
+ { name: "structures_upload_events", routes: [STRUCTURE_NOTES_UPLOAD_JOBS] },
33
33
  ];
34
34
 
35
35
  const catalogViewConfigItems = (formatMessage) =>
@@ -26,20 +26,17 @@ import {
26
26
  import Submenu from "./Submenu";
27
27
 
28
28
  function isMenuSubscope(pathname, subscope) {
29
- // const match = matchPath(
30
- // {
31
- // path: [
32
- // CONCEPTS_SUBSCOPE,
33
- // CONCEPTS_SIDEMENU_SUBSCOPE,
34
- // CONCEPTS_SIDEMENU_SUBSCOPE_DEPRECATED,
35
- // CONCEPTS_SIDEMENU_SUBSCOPE_PENDING,
36
- // ],
37
- // },
38
- // pathname
39
- // );
40
-
41
- // return !!match?.params?.subscope && subscope === match.params.subscope;
42
- return false;
29
+ const pathsToCheck = [
30
+ CONCEPTS_SUBSCOPE,
31
+ CONCEPTS_SIDEMENU_SUBSCOPE,
32
+ CONCEPTS_SIDEMENU_SUBSCOPE_DEPRECATED,
33
+ CONCEPTS_SIDEMENU_SUBSCOPE_PENDING,
34
+ ];
35
+
36
+ return pathsToCheck.some(path => {
37
+ const match = matchPath({ path }, pathname);
38
+ return match?.params?.subscope && subscope === decodeURIComponent(match.params.subscope);
39
+ });
43
40
  }
44
41
 
45
42
  function mainMenuSubscopes(allSubscopes, rootSubscopes) {
@@ -247,7 +244,6 @@ export const GlossaryMenu = ({ bgSubscopes, rootSubscopes }) => {
247
244
 
248
245
  const isActiveSubscope = () =>
249
246
  conceptSubscope === name || isMenuSubscope(location.pathname, name);
250
-
251
247
  return (
252
248
  <Submenu
253
249
  key={name}
@@ -6,16 +6,17 @@ import { useAuthorized } from "../hooks";
6
6
  import {
7
7
  EXECUTION_GROUPS,
8
8
  IMPLEMENTATIONS,
9
+ IMPLEMENTATIONS_BY_SUBSCOPE,
9
10
  IMPLEMENTATIONS_DEPRECATED,
10
11
  IMPLEMENTATIONS_PENDING,
11
12
  IMPLEMENTATIONS_UPLOAD_JOBS,
12
13
  QUALITY_DASHBOARD,
13
14
  RULES,
14
15
  } from "../routes";
15
- import { getQualityDashboardConfig } from "../selectors";
16
+ import { getQualityDashboardConfig, getRiSubscopes } from "../selectors";
16
17
  import Submenu from "./Submenu";
17
18
 
18
- export const ITEMS = [
19
+ export const BASE_ITEMS = [
19
20
  { name: "rules", routes: [RULES], groups: ["quality"] },
20
21
  { name: "implementations", routes: [IMPLEMENTATIONS], groups: ["quality"] },
21
22
  {
@@ -45,7 +46,7 @@ export const ITEMS = [
45
46
  },
46
47
  ];
47
48
 
48
- export const QualityMenu = ({ dashboardConfig }) => {
49
+ export const QualityMenu = ({ dashboardConfig, riSubscopes }) => {
49
50
  const { formatMessage } = useIntl();
50
51
  const iconQuality = formatMessage({
51
52
  id: "sidemenu.quality.icon",
@@ -57,21 +58,45 @@ export const QualityMenu = ({ dashboardConfig }) => {
57
58
  "quality_implementation_additional_actions"
58
59
  );
59
60
 
60
- const filteredItems = _.filter(
61
+ if (!authorized) return null;
62
+
63
+ const filteredBaseItems = _.filter(
61
64
  ({ name }) => name != "quality_dashboard" || !_.isEmpty(dashboardConfig)
62
- )(ITEMS);
65
+ )(BASE_ITEMS);
66
+
67
+ const extendedItems = filteredBaseItems.reduce((acc, item) => {
68
+ acc.push(item);
69
+
70
+ if (item.name === "implementations") {
71
+ riSubscopes.forEach(subscope => {
72
+ acc.push({
73
+ name: subscope,
74
+ routes: [IMPLEMENTATIONS_BY_SUBSCOPE.replace(':subscope', subscope)],
75
+ groups: ["quality"],
76
+ });
77
+ });
78
+ }
79
+
80
+ return acc;
81
+ }, []);
63
82
 
64
- return authorized ? (
65
- <Submenu items={filteredItems} icon={iconQuality} name="quality" />
66
- ) : null;
83
+ return (
84
+ <Submenu items={extendedItems} icon={iconQuality} name="quality" />
85
+ );
67
86
  };
68
87
 
69
88
  QualityMenu.propTypes = {
70
89
  dashboardConfig: PropTypes.object,
90
+ riSubscopes: PropTypes.array,
91
+ };
92
+
93
+ QualityMenu.defaultProps = {
94
+ riSubscopes: [],
71
95
  };
72
96
 
73
97
  export const mapStateToProps = (state) => ({
74
98
  dashboardConfig: getQualityDashboardConfig(state),
99
+ riSubscopes: getRiSubscopes(state),
75
100
  });
76
101
 
77
- export default connect(mapStateToProps)(QualityMenu);
102
+ export default connect(mapStateToProps)(QualityMenu);
@@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
3
3
  import PropTypes from "prop-types";
4
4
  import { useIntl, FormattedMessage } from "react-intl";
5
5
  import { connect } from "react-redux";
6
- import { useLocation, Link, useNavigate } from "react-router";
6
+ import { useLocation, Link, useNavigate, matchPath } from "react-router";
7
7
  import { Dropdown, Icon, Menu } from "semantic-ui-react";
8
8
  import { useAuthorizedItems, useActiveRoutes } from "@truedat/core/hooks";
9
9
  import { clearNavFilter as clearBucketFilterRoutine } from "@truedat/core/routines";
@@ -84,14 +84,49 @@ export const Submenu = ({
84
84
  }, [location]);
85
85
 
86
86
  const primaryRoute = _.flow(_.flatMap("routes"), _.head)(filteredItems);
87
+ const pathname = location.pathname;
88
+ const decodedPathname = decodeURIComponent(pathname);
89
+ const routes = _.flatMap("routes")(filteredItems);
90
+
91
+ const pathParts = pathname.split('/').filter(part => part !== '');
92
+ const hasSubscopeInPath = pathname.includes('/subscope/');
93
+ let subscopeName = null;
94
+ if (hasSubscopeInPath && pathParts.length >= 3 && pathParts[1] === 'subscope') {
95
+ subscopeName = decodeURIComponent(pathParts[2]).replace(/%20/g, ' ');
96
+ }
97
+
98
+ const isSubmenuActiveForSubscope = hasSubscopeInPath && subscopeName &&
99
+ _.some(item =>
100
+ item.name.toLowerCase() === subscopeName.toLowerCase()
101
+ )(filteredItems);
102
+
103
+ const isSubmenuActiveForRoutes = _.some(item =>
104
+ _.some(route => {
105
+ const decodedPathname = decodeURIComponent(pathname);
106
+ return decodedPathname === route ||
107
+ decodedPathname.startsWith(`${route}/`) ||
108
+ matchPath({ path: route }, decodedPathname);
109
+ })(item.routes)
110
+ )(filteredItems);
111
+
112
+ // Determine if submenu should be active, with special handling for management URLs
113
+ const submenuIsActive = (isActive || isSubmenuActiveForSubscope || isSubmenuActiveForRoutes) &&
114
+ !(name === 'glossary' && location.pathname.startsWith('/concepts/management/'));
115
+
87
116
 
88
- if (isActive && sidebarVisible) {
117
+ if (submenuIsActive && sidebarVisible) {
89
118
  const menuItems = filteredItems.map((item, i) => {
90
119
  const isItemActive =
91
- isActive &&
120
+ submenuIsActive &&
92
121
  (item.isActive
93
122
  ? item.isActive()
94
- : _.includes(activeRoute, item.routes));
123
+ : hasSubscopeInPath && subscopeName
124
+ ? item.name.toLowerCase() === subscopeName.toLowerCase()
125
+ : item.routes.some(route =>
126
+ route === activeRoute ||
127
+ route === pathname || route === decodedPathname ||
128
+ matchPath({ path: route }, pathname) || matchPath({ path: route }, decodedPathname) // Pattern match
129
+ ));
95
130
 
96
131
  return (
97
132
  <MenuItem
@@ -120,7 +155,7 @@ export const Submenu = ({
120
155
  </Menu.Item>
121
156
  );
122
157
  } else {
123
- const className = isActive ? "active" : null;
158
+ const className = submenuIsActive ? "active" : null;
124
159
 
125
160
  const trigger = (
126
161
  <Link to={primaryRoute} className="ui">
@@ -132,10 +167,16 @@ export const Submenu = ({
132
167
  );
133
168
  const dropdownItems = filteredItems.map((item, i) => {
134
169
  const isItemActive =
135
- isActive &&
170
+ submenuIsActive &&
136
171
  (item.isActive
137
172
  ? item.isActive()
138
- : _.includes(activeRoute, item.routes));
173
+ : hasSubscopeInPath && subscopeName
174
+ ? item.name.toLowerCase() === subscopeName.toLowerCase()
175
+ : item.routes.some(route =>
176
+ route === activeRoute ||
177
+ route === pathname || route === decodedPathname ||
178
+ matchPath({ path: route }, pathname) || matchPath({ path: route }, decodedPathname)
179
+ ));
139
180
 
140
181
  return (
141
182
  <DropdownItem
@@ -0,0 +1,217 @@
1
+ import _ from "lodash/fp";
2
+ import { useState } from "react";
3
+ import { FormattedMessage, useIntl } from "react-intl";
4
+ import { useParams } from "react-router";
5
+ import {
6
+ Header,
7
+ Icon,
8
+ Segment,
9
+ Dimmer,
10
+ Loader,
11
+ Table,
12
+ Label,
13
+ Button,
14
+ } from "semantic-ui-react";
15
+ import { DateTime } from "@truedat/core/components";
16
+ import { useUploadJob } from "../hooks/useUploadJobs";
17
+ import { StatusPill, ResponseCell } from "./UploadJobParser";
18
+ import UploadJobBreadcrumbs from "./UploadJobBreadcrumbs";
19
+
20
+ export default function UploadJob({ scope }) {
21
+ const { id } = useParams();
22
+ const { data, loading } = useUploadJob(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: `uploadJob.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
+ <UploadJobBreadcrumbs filename={job?.filename} scope={scope} />
110
+ <Segment>
111
+ <Header as="h2">
112
+ <Icon
113
+ circular
114
+ name={formatMessage({
115
+ id: "uploadJob.header.icon",
116
+ defaultMessage: "cogs",
117
+ })}
118
+ />
119
+ <Header.Content>
120
+ <FormattedMessage id={`uploadJob.${scope}.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="uploadJob.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="uploadJob.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="uploadJob.table.status"
169
+ defaultMessage="Status"
170
+ />
171
+ </Table.HeaderCell>
172
+ <Table.HeaderCell>
173
+ <FormattedMessage
174
+ id="uploadJob.table.inserted_at"
175
+ defaultMessage="Inserted At"
176
+ />
177
+ </Table.HeaderCell>
178
+ <Table.HeaderCell>
179
+ <FormattedMessage
180
+ id="uploadJob.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="uploadJob.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
+ }
@@ -0,0 +1,38 @@
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, STRUCTURE_NOTES_UPLOAD_JOBS } from "@truedat/core/routes";
6
+
7
+ export const UploadJobBreadcrumbs = ({ filename, scope }) => {
8
+ const uploadJobsPath = (scope) => {
9
+ switch (scope) {
10
+ case "implementations":
11
+ return IMPLEMENTATIONS_UPLOAD_JOBS;
12
+ case "notes":
13
+ return STRUCTURE_NOTES_UPLOAD_JOBS;
14
+ default:
15
+ return null;
16
+ }
17
+ };
18
+ return (
19
+ <Breadcrumb>
20
+ <Breadcrumb.Section as={Link} to={uploadJobsPath(scope)}>
21
+ <FormattedMessage id={`uploadJobs.${scope}.header`} />
22
+ </Breadcrumb.Section>
23
+ {filename ? (
24
+ <>
25
+ <Breadcrumb.Divider icon="right angle" />
26
+ <Breadcrumb.Section active>{filename}</Breadcrumb.Section>
27
+ </>
28
+ ) : null}
29
+ </Breadcrumb>
30
+ );
31
+ };
32
+
33
+ UploadJobBreadcrumbs.propTypes = {
34
+ filename: PropTypes.string,
35
+ scope: PropTypes.string.isRequired,
36
+ };
37
+
38
+ export default UploadJobBreadcrumbs;