@truedat/dd 7.1.4 → 7.1.6

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/dd",
3
- "version": "7.1.4",
3
+ "version": "7.1.6",
4
4
  "description": "Truedat Web Data Dictionary",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -34,7 +34,7 @@
34
34
  "@testing-library/jest-dom": "^5.16.5",
35
35
  "@testing-library/react": "^12.0.0",
36
36
  "@testing-library/user-event": "^13.2.1",
37
- "@truedat/test": "7.1.4",
37
+ "@truedat/test": "7.1.6",
38
38
  "babel-jest": "^28.1.0",
39
39
  "babel-plugin-dynamic-import-node": "^2.3.3",
40
40
  "babel-plugin-lodash": "^3.3.4",
@@ -88,9 +88,9 @@
88
88
  },
89
89
  "dependencies": {
90
90
  "@apollo/client": "^3.7.1",
91
- "@truedat/auth": "7.1.4",
92
- "@truedat/core": "7.1.4",
93
- "@truedat/df": "7.1.4",
91
+ "@truedat/auth": "7.1.6",
92
+ "@truedat/core": "7.1.6",
93
+ "@truedat/df": "7.1.6",
94
94
  "lodash": "^4.17.21",
95
95
  "moment": "^2.29.4",
96
96
  "path-to-regexp": "^1.7.0",
@@ -115,5 +115,5 @@
115
115
  "react-dom": ">= 16.8.6 < 17",
116
116
  "semantic-ui-react": ">= 2.0.3 < 2.2"
117
117
  },
118
- "gitHead": "4b951559f6e84f85ed054bf9afe8699dc57f7b8f"
118
+ "gitHead": "0d15ef9da4d597618a5e3543ad2173e3d76f258b"
119
119
  }
@@ -535,3 +535,62 @@ export const CATALOG_VIEW_CONFIG_QUERY = gql`
535
535
  }
536
536
  }
537
537
  `;
538
+
539
+ export const DATA_FIELDS_QUERY = gql`
540
+ query DataFields(
541
+ $dataStructureId: ID!
542
+ $version: String!
543
+ $note_fields: [String]
544
+ $first: Int
545
+ $last: Int
546
+ $search: String
547
+ $before: Cursor
548
+ $after: Cursor
549
+ $filters: DataFieldsFilter
550
+ ) {
551
+ dataFields(
552
+ dataStructureId: $dataStructureId
553
+ version: $version
554
+ first: $first
555
+ last: $last
556
+ before: $before
557
+ after: $after
558
+ search: $search
559
+ filters: $filters
560
+ ) {
561
+ page {
562
+ alias
563
+ classes
564
+ data_structure_id
565
+ deleted_at
566
+ description
567
+ degree {
568
+ in
569
+ out
570
+ }
571
+ links
572
+ id
573
+ metadata
574
+ name
575
+ type
576
+ profile {
577
+ max
578
+ min
579
+ most_frequent
580
+ null_count
581
+ patterns
582
+ total_count
583
+ unique_count
584
+ }
585
+ has_note
586
+ note(select_fields: $note_fields)
587
+ }
588
+ pageInfo {
589
+ startCursor
590
+ endCursor
591
+ hasNextPage
592
+ hasPreviousPage
593
+ }
594
+ }
595
+ }
596
+ `;
@@ -0,0 +1,73 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import PropTypes from "prop-types";
4
+ import queryString from "query-string";
5
+ import { useLocation, Link } from "react-router-dom";
6
+ import { Menu } from "semantic-ui-react";
7
+
8
+ /*
9
+ This component is duplicated from packages/core/src/components/CursorPagination.js
10
+ to accommodate poorly implemented pagination logic on the back end.
11
+ We have created a task to improve the pagination system and streamline GraphQL query handling in the future.
12
+ */
13
+ const CursorPagination = ({ pageInfo, className }) => {
14
+ const { search, ...location } = useLocation();
15
+ const { q } = queryString.parse(search);
16
+ const { endCursor, startCursor, hasPreviousPage, hasNextPage } =
17
+ pageInfo || {};
18
+
19
+ const next =
20
+ hasNextPage && endCursor
21
+ ? {
22
+ ...location,
23
+ search: "?" + queryString.stringify({ after: endCursor, q }),
24
+ }
25
+ : null;
26
+ const prev =
27
+ hasPreviousPage && startCursor
28
+ ? {
29
+ ...location,
30
+ search: "?" + queryString.stringify({ before: startCursor, q }),
31
+ }
32
+ : null;
33
+ const first = { ...location, search: "?" + queryString.stringify({ q }) };
34
+
35
+ return (
36
+ <Menu className={className} pagination role="navigation">
37
+ <Menu.Item
38
+ as={first ? Link : null}
39
+ link={!!first}
40
+ content="«"
41
+ disabled={!hasPreviousPage}
42
+ to={first}
43
+ replace={first ? true : null}
44
+ aria-label="First"
45
+ />
46
+ <Menu.Item
47
+ as={prev ? Link : null}
48
+ link={!!prev}
49
+ content="⟨"
50
+ disabled={!hasPreviousPage}
51
+ to={prev}
52
+ replace={prev ? true : null}
53
+ aria-label="Later"
54
+ />
55
+ <Menu.Item
56
+ as={next ? Link : null}
57
+ link={!!next}
58
+ content="⟩"
59
+ disabled={!hasNextPage}
60
+ to={next}
61
+ replace={next ? true : null}
62
+ aria-label="Earlier"
63
+ />
64
+ </Menu>
65
+ );
66
+ };
67
+
68
+ CursorPagination.propTypes = {
69
+ className: PropTypes.string,
70
+ pageInfo: PropTypes.object,
71
+ };
72
+
73
+ export default CursorPagination;
@@ -1,10 +1,24 @@
1
1
  import React from "react";
2
2
  import PropTypes from "prop-types";
3
- import { Table } from "semantic-ui-react";
3
+ import { Checkbox, Table } from "semantic-ui-react";
4
4
  import { columnDecorator } from "@truedat/core/services";
5
5
 
6
- export const StructureFieldRow = ({ field, columns }) => (
6
+ export const StructureFieldRow = ({
7
+ field,
8
+ columns,
9
+ profilingMode,
10
+ onCheckedRow,
11
+ checked,
12
+ }) => (
7
13
  <Table.Row key={field.data_structure_id} warning={!!field.deleted_at}>
14
+ {profilingMode && (
15
+ <Table.HeaderCell textAlign="center" width={1}>
16
+ <Checkbox
17
+ onChange={() => onCheckedRow(field.data_structure_id)}
18
+ checked={checked}
19
+ />
20
+ </Table.HeaderCell>
21
+ )}
8
22
  {columns
9
23
  ? columns.map((column, key) => (
10
24
  <Table.Cell
@@ -20,6 +34,9 @@ export const StructureFieldRow = ({ field, columns }) => (
20
34
  StructureFieldRow.propTypes = {
21
35
  field: PropTypes.object.isRequired,
22
36
  columns: PropTypes.array,
37
+ profilingMode: PropTypes.bool,
38
+ checked: PropTypes.bool,
39
+ onCheckedRow: PropTypes.func,
23
40
  };
24
41
 
25
42
  export default StructureFieldRow;
@@ -1,12 +1,28 @@
1
1
  import _ from "lodash/fp";
2
- import React from "react";
2
+ import React, { useState, useCallback } from "react";
3
3
  import PropTypes from "prop-types";
4
- import { connect } from "react-redux";
5
- import { Table } from "semantic-ui-react";
6
- import { FormattedMessage } from "react-intl";
4
+ import queryString from "query-string";
5
+ import { useLocation, useParams, useHistory } from "react-router-dom";
6
+ import { useQuery } from "@apollo/client";
7
+ import {
8
+ Button,
9
+ Checkbox,
10
+ Grid,
11
+ Icon,
12
+ Input,
13
+ Label,
14
+ Message,
15
+ Table,
16
+ } from "semantic-ui-react";
17
+ import { FormattedMessage, useIntl } from "react-intl";
7
18
  import { columnPredicate } from "@truedat/core/services";
8
- import { getSortedFields, getStructureFieldColumns } from "../selectors";
19
+ import { Loading } from "@truedat/core/components";
20
+ import { connect, useSelector } from "react-redux";
21
+ import { DATA_FIELDS_QUERY } from "../api/queries";
22
+ import { getStructureFieldColumns } from "../selectors";
23
+ import { createProfileGroup } from "../routines";
9
24
  import StructureFieldRow from "./StructureFieldRow";
25
+ import CursorPagination from "./CursorPagination";
10
26
 
11
27
  const columnPred = (c) =>
12
28
  c?.name === "degree" ? _.prop("degree") : columnPredicate(c);
@@ -14,10 +30,71 @@ const columnPred = (c) =>
14
30
  const getColumns = (columns, fields) =>
15
31
  _.filter((c) => _.any(columnPred(c))(fields))(columns);
16
32
 
17
- export const StructureFields = ({ columns, fields, ...rest }) => (
33
+ const PAGE_SIZE = 50;
34
+
35
+ const ProfileActions = ({
36
+ emptyFields,
37
+ handleSubmit,
38
+ onProfileCheck,
39
+ profilingMode,
40
+ }) => {
41
+ return (
42
+ <Grid.Column
43
+ textAlign="right"
44
+ style={{
45
+ display: "flex",
46
+ alignItems: "center",
47
+ justifyContent: "flex-end",
48
+ }}
49
+ >
50
+ <Checkbox
51
+ disabled={emptyFields}
52
+ checked={profilingMode}
53
+ className="bgOrange"
54
+ toggle
55
+ onChange={() => onProfileCheck(!profilingMode)}
56
+ />
57
+ <Button
58
+ style={{ marginLeft: "10px" }}
59
+ icon
60
+ labelPosition="left"
61
+ disabled={!profilingMode || emptyFields}
62
+ onClick={handleSubmit}
63
+ >
64
+ <Icon name="tachometer alternate" />
65
+ <FormattedMessage id="structure.execute_profile" />
66
+ </Button>
67
+ </Grid.Column>
68
+ );
69
+ };
70
+
71
+ ProfileActions.propTypes = {
72
+ emptyFields: PropTypes.bool,
73
+ handleSubmit: PropTypes.func,
74
+ onProfileCheck: PropTypes.func,
75
+ profilingMode: PropTypes.bool,
76
+ };
77
+
78
+ const StructureFieldsTable = ({
79
+ allCheckedInPage,
80
+ profilingMode,
81
+ columns,
82
+ fields,
83
+ onCheckedRow,
84
+ onPageCheck,
85
+ checkedFields,
86
+ }) => (
18
87
  <Table>
19
88
  <Table.Header>
20
89
  <Table.Row>
90
+ {profilingMode && (
91
+ <Table.HeaderCell textAlign="center" width={1}>
92
+ <Checkbox
93
+ checked={allCheckedInPage}
94
+ onChange={() => onPageCheck()}
95
+ />
96
+ </Table.HeaderCell>
97
+ )}
21
98
  {columns.map((column, i) => (
22
99
  <Table.HeaderCell
23
100
  key={i}
@@ -29,26 +106,200 @@ export const StructureFields = ({ columns, fields, ...rest }) => (
29
106
  </Table.Header>
30
107
  <Table.Body>
31
108
  {fields.map((f, i) => (
32
- <StructureFieldRow key={i} columns={columns} field={f} {...rest} />
109
+ <StructureFieldRow
110
+ key={i}
111
+ columns={columns}
112
+ field={f}
113
+ profilingMode={profilingMode}
114
+ onCheckedRow={onCheckedRow}
115
+ checked={_.contains(f?.data_structure_id)(checkedFields)}
116
+ />
33
117
  ))}
34
118
  </Table.Body>
35
119
  </Table>
36
120
  );
37
121
 
122
+ StructureFieldsTable.propTypes = {
123
+ allCheckedInPage: PropTypes.bool,
124
+ profilingMode: PropTypes.bool,
125
+ fields: PropTypes.arrayOf(PropTypes.object),
126
+ columns: PropTypes.arrayOf(PropTypes.object),
127
+ onCheckedRow: PropTypes.func,
128
+ onPageCheck: PropTypes.func,
129
+ checkedFields: PropTypes.arrayOf(PropTypes.object),
130
+ };
131
+
132
+ export const StructureFields = ({
133
+ createProfileGroup,
134
+ canExecuteProfiling,
135
+ }) => {
136
+ const { id: dataStructureId, version: version } = useParams();
137
+ const { search, pathname } = useLocation();
138
+ const { before, after, q } = queryString.parse(search);
139
+ const history = useHistory();
140
+ const { formatMessage } = useIntl();
141
+ const [profilingMode, setProfilingMode] = useState(false);
142
+ const [checkedFields, setCheckedFields] = useState([]);
143
+ const [searchQuery, setSearchQuery] = useState(q || "");
144
+
145
+ const structureVariables = _.isNil(version)
146
+ ? { dataStructureId, version: "latest" }
147
+ : { dataStructureId, version };
148
+ const searchVariables = before
149
+ ? { after, before, last: PAGE_SIZE, search: q }
150
+ : { after, before, first: PAGE_SIZE, search: q };
151
+ const variables = { ...structureVariables, ...searchVariables };
152
+ const { data, loading } = useQuery(DATA_FIELDS_QUERY, { variables });
153
+ const fields = data?.dataFields?.page || [];
154
+ const pageInfo = data?.dataFields?.pageInfo || {};
155
+ const pageStructureIds = _.map("data_structure_id")(fields);
156
+ const emptyFields = _.isEmpty(fields);
157
+
158
+ const columns = useSelector((state) =>
159
+ getColumns(getStructureFieldColumns(state), fields)
160
+ );
161
+
162
+ const allCheckedInPage = _.every((structureId) =>
163
+ _.contains(structureId)(checkedFields)
164
+ )(pageStructureIds);
165
+
166
+ const debounceSearch = useCallback(
167
+ _.debounce(300)((value) => {
168
+ history.replace({
169
+ pathname: pathname,
170
+ search: queryString.stringify({ q: value }),
171
+ });
172
+ }),
173
+ []
174
+ );
175
+
176
+ const handleInputChange = (e) => {
177
+ const value = e.target.value;
178
+ setCheckedFields([]);
179
+ setSearchQuery(value);
180
+ debounceSearch(value);
181
+ };
182
+
183
+ const handleCheckedRow = (id) => {
184
+ const updatedSelection = _.includes(id)(checkedFields)
185
+ ? _.without([id])(checkedFields)
186
+ : [id, ...checkedFields];
187
+ setCheckedFields(updatedSelection);
188
+ };
189
+
190
+ const handleCheckedPage = () => {
191
+ const updatedSelection = allCheckedInPage
192
+ ? _.without(pageStructureIds)(checkedFields)
193
+ : _.union(checkedFields)(pageStructureIds);
194
+ setCheckedFields(updatedSelection);
195
+ };
196
+
197
+ const handleCheckedProfileExecution = (checked) => {
198
+ !checked && !_.isEmpty(checkedFields) && setCheckedFields([]);
199
+ setProfilingMode(checked);
200
+ };
201
+
202
+ const handleSubmit = (e) => {
203
+ _.isEmpty(checkedFields)
204
+ ? createProfileGroup({ parent_structure_id: dataStructureId })
205
+ : createProfileGroup({ data_structure_ids: checkedFields });
206
+ };
207
+
208
+ return (
209
+ <>
210
+ {loading ? <Loading /> : null}
211
+ <>
212
+ <>
213
+ <Grid>
214
+ <Grid.Row columns={2}>
215
+ <Grid.Column>
216
+ <Input
217
+ icon={{ name: "search", link: true }}
218
+ iconPosition="left"
219
+ loading={loading}
220
+ placeholder={formatMessage({
221
+ id: "structure.search.placeholder",
222
+ })}
223
+ onChange={handleInputChange}
224
+ value={searchQuery}
225
+ />
226
+ </Grid.Column>
227
+ {canExecuteProfiling ? (
228
+ <ProfileActions
229
+ emptyFields={emptyFields}
230
+ profilingMode={profilingMode}
231
+ onProfileCheck={handleCheckedProfileExecution}
232
+ handleSubmit={handleSubmit}
233
+ />
234
+ ) : null}
235
+ </Grid.Row>
236
+ <Grid.Row
237
+ style={{
238
+ minHeight: "40px",
239
+ paddingTop: "2px",
240
+ paddingBottom: "2px",
241
+ }}
242
+ >
243
+ <Grid.Column textAlign="right">
244
+ {!loading &&
245
+ profilingMode &&
246
+ !emptyFields &&
247
+ _.isEmpty(checkedFields) && (
248
+ <Label>
249
+ <FormattedMessage id="structure.profile.tab.execute_all" />
250
+ </Label>
251
+ )}
252
+ {!loading && profilingMode && !_.isEmpty(checkedFields) && (
253
+ <Label>
254
+ <FormattedMessage
255
+ id="structure.profile.tab.execute"
256
+ values={{ count: checkedFields.length }}
257
+ />
258
+ </Label>
259
+ )}
260
+ </Grid.Column>
261
+ </Grid.Row>
262
+ </Grid>
263
+ </>
264
+ {!_.isEmpty(fields) && !loading && (
265
+ <>
266
+ <StructureFieldsTable
267
+ allCheckedInPage={allCheckedInPage}
268
+ onPageCheck={handleCheckedPage}
269
+ profilingMode={profilingMode}
270
+ fields={fields}
271
+ columns={columns}
272
+ onCheckedRow={handleCheckedRow}
273
+ checkedFields={checkedFields}
274
+ />
275
+ <CursorPagination pageInfo={pageInfo} />
276
+ </>
277
+ )}
278
+ {_.isEmpty(fields) && !loading && (
279
+ <Message icon info>
280
+ <Icon name="search" />
281
+ <Message.Content>
282
+ <Message.Header>
283
+ <FormattedMessage id={`structures.not_found.header`} />
284
+ </Message.Header>
285
+ <FormattedMessage id="structures.not_found.body" />
286
+ </Message.Content>
287
+ </Message>
288
+ )}
289
+ </>
290
+ </>
291
+ );
292
+ };
293
+
38
294
  StructureFields.propTypes = {
39
- columns: PropTypes.array,
40
- fields: PropTypes.array,
41
- version: PropTypes.string,
295
+ canExecuteProfiling: PropTypes.bool,
296
+ createProfileGroup: PropTypes.func,
42
297
  };
43
298
 
44
- const mapStateToProps = (state, ownProps) => ({
45
- structureId: _.path("structure.id")(state),
46
- version: state.structureVersion,
47
- fields: getSortedFields(state),
48
- columns: getColumns(
49
- _.propOr(getStructureFieldColumns(state), "columns")(ownProps),
50
- state.structureFields
51
- ),
299
+ const mapStateToProps = ({ userPermissions }) => ({
300
+ canExecuteProfiling: _.propOr(false, "profile_permission")(userPermissions),
52
301
  });
53
302
 
54
- export default connect(mapStateToProps)(StructureFields);
303
+ const mapDispatchToProps = { createProfileGroup };
304
+
305
+ export default connect(mapStateToProps, mapDispatchToProps)(StructureFields);
@@ -74,9 +74,9 @@ const mapStateToProps = (
74
74
  structure: {
75
75
  ...structure,
76
76
  id: structure.id + "",
77
- hasDataFields: _.negate(_.isEmpty)(structure.data_fields),
77
+ hasDataFields: _.negate(_.isEmpty)(structure?.data_fields),
78
78
  structureFields: _.flow(
79
- _.prop("data_fields"),
79
+ _.get("data_fields"),
80
80
  _.map((dataField) => ({ ...dataField, data_structure_id: dataField.id }))
81
81
  )(structure),
82
82
  },
@@ -12,7 +12,6 @@ import StructureConfidentialButton from "./StructureConfidentialButton";
12
12
  import StructureDeleteButton from "./StructureDeleteButton";
13
13
  import StructureGrantSummaryButton from "./StructureGrantSummaryButton";
14
14
  import StructureProfileButton from "./StructureProfileButton";
15
- import StructuresProfileButton from "./StructuresProfileButton";
16
15
  import StructureTags from "./StructureTags";
17
16
 
18
17
  const Date = ({ className, icon, label, date }) => {
@@ -100,10 +99,6 @@ export const StructureSummary = ({
100
99
  structureClass === "field" ? (
101
100
  <StructureProfileButton />
102
101
  ) : null}
103
- {userPermissions.profile_permission &&
104
- structureClass !== "field" ? (
105
- <StructuresProfileButton />
106
- ) : null}
107
102
  {userPermissions.confidential ? (
108
103
  <StructureConfidentialButton />
109
104
  ) : null}
@@ -9,7 +9,10 @@ import {
9
9
  STRUCTURE_NOTES_EDIT,
10
10
  } from "@truedat/core/routes";
11
11
  import { getActiveTab } from "../selectors/getActiveTab";
12
- import { getTabVisibility } from "../selectors/getTabVisibility";
12
+ import {
13
+ getTabVisibility,
14
+ getLinkedImplementationsToStructuresColumns,
15
+ } from "../selectors";
13
16
  import StructureChildrenRelations from "./StructureChildrenRelations";
14
17
  import StructureEvents from "./StructureEvents";
15
18
  import StructureGrants from "./StructureGrants";
@@ -40,7 +43,7 @@ const RuleImplementationsTable = React.lazy(() =>
40
43
  import("@truedat/dq/components/RuleImplementationsTable")
41
44
  );
42
45
 
43
- export const StructureTabPane = ({ activeTab, tabVisibility }) => {
46
+ export const StructureTabPane = ({ activeTab, tabVisibility, columns }) => {
44
47
  return (
45
48
  <ErrorBoundary>
46
49
  {activeTab === "fields" && <StructureFields />}
@@ -82,7 +85,7 @@ export const StructureTabPane = ({ activeTab, tabVisibility }) => {
82
85
  {activeTab === "events" && <StructureEvents />}
83
86
  {activeTab === "roles" && <StructureRoles />}
84
87
  {activeTab === "rules" && tabVisibility.rules && (
85
- <RuleImplementationsTable withoutColumns={["status"]} />
88
+ <RuleImplementationsTable columns={columns} />
86
89
  )}
87
90
  {activeTab === "metadata" && tabVisibility.metadata && (
88
91
  <StructureMetadata />
@@ -96,6 +99,7 @@ export const StructureTabPane = ({ activeTab, tabVisibility }) => {
96
99
  StructureTabPane.propTypes = {
97
100
  activeTab: PropTypes.string,
98
101
  tabVisibility: PropTypes.object,
102
+ columns: PropTypes.array,
99
103
  };
100
104
 
101
105
  const mapStateToProps = (
@@ -107,6 +111,7 @@ const mapStateToProps = (
107
111
  return {
108
112
  tabVisibility,
109
113
  activeTab,
114
+ columns: getLinkedImplementationsToStructuresColumns(state),
110
115
  };
111
116
  };
112
117
 
@@ -1,41 +1,87 @@
1
1
  import React from "react";
2
2
  import { render } from "@truedat/test/render";
3
+ import { useQuery } from "@apollo/client";
3
4
  import StructureFields from "../StructureFields";
4
5
 
5
- const state = {
6
- structureFields: [
7
- {
8
- id: 123,
9
- data_structure_id: 1,
10
- name: "field1",
11
- description: "desc1",
12
- links: [{ resource_id: 123 }],
13
- metadata: {
14
- nullable: true,
15
- precision: "22",
16
- type: "varchar",
17
- },
6
+ jest.mock("@apollo/client", () => ({
7
+ ...jest.requireActual("@apollo/client"),
8
+ useQuery: jest.fn(),
9
+ }));
10
+
11
+ const page = [
12
+ {
13
+ id: 123,
14
+ data_structure_id: 1,
15
+ name: "foo",
16
+ description: "desc1",
17
+ links: [{ resource_id: 123 }],
18
+ metadata: {
19
+ nullable: true,
20
+ precision: "22",
21
+ type: "varchar",
18
22
  },
19
- {
20
- id: 122,
21
- data_structure_id: 2,
22
- name: "field2",
23
- description: "desc2",
24
- external_id: "foo",
25
- metadata: {
26
- nullable: true,
27
- precision: "22",
28
- type: "varchar",
23
+ },
24
+ {
25
+ id: 122,
26
+ data_structure_id: 2,
27
+ name: "bar",
28
+ description: "desc2",
29
+ external_id: "foo",
30
+ metadata: {
31
+ nullable: true,
32
+ precision: "22",
33
+ type: "varchar",
34
+ },
35
+ },
36
+ ];
37
+
38
+ const mockData = {
39
+ data: {
40
+ dataFields: {
41
+ page,
42
+ pageInfo: {
43
+ hasNextPage: true,
44
+ hasPreviousPage: false,
29
45
  },
30
46
  },
31
- ],
47
+ },
48
+ loading: false,
49
+ error: null,
32
50
  };
33
51
 
34
- const renderOpts = { state };
52
+ const renderProps = {
53
+ messages: {
54
+ en: {
55
+ "structure.field.metadata.nullable.true": "Yes",
56
+ "structure.field.links": "Concepts",
57
+ "structure.field.description": "Description",
58
+ "structure.field.metadata.nullable": "Nullable",
59
+ "structure.field.metadata.precision": "Precision",
60
+ "structure.field.metadata.type": "Type",
61
+ "structure.field.name": "Field",
62
+ "structure.execute_profile": "Execute profile",
63
+ "structure.search.placeholder": "Enter a search...",
64
+ },
65
+ },
66
+ state: {
67
+ userPermissions: { profile_permission: true },
68
+ },
69
+ };
35
70
 
36
71
  describe("<StructureFields />", () => {
37
72
  it("matches the latest snapshot", () => {
38
- const { container } = render(<StructureFields />, renderOpts);
73
+ useQuery.mockImplementation(() => mockData);
74
+ const { container } = render(<StructureFields />, renderProps);
39
75
  expect(container).toMatchSnapshot();
40
76
  });
77
+
78
+ it("no render profile button if dont have permissions", () => {
79
+ useQuery.mockImplementation(() => mockData);
80
+ const newRenderProps = {
81
+ ...renderProps,
82
+ state: {},
83
+ };
84
+ const { queryByText } = render(<StructureFields />, newRenderProps);
85
+ expect(queryByText("Execute profile")).not.toBeInTheDocument();
86
+ });
41
87
  });
@@ -2,6 +2,68 @@
2
2
 
3
3
  exports[`<StructureFields /> matches the latest snapshot 1`] = `
4
4
  <div>
5
+ <div
6
+ class="ui grid"
7
+ >
8
+ <div
9
+ class="two column row"
10
+ >
11
+ <div
12
+ class="column"
13
+ >
14
+ <div
15
+ class="ui left icon input"
16
+ >
17
+ <input
18
+ placeholder="Enter a search..."
19
+ type="text"
20
+ value=""
21
+ />
22
+ <i
23
+ aria-hidden="true"
24
+ class="search link icon"
25
+ />
26
+ </div>
27
+ </div>
28
+ <div
29
+ class="right aligned column"
30
+ style="display: flex; align-items: center; justify-content: flex-end;"
31
+ >
32
+ <div
33
+ class="ui fitted toggle checkbox bgOrange"
34
+ >
35
+ <input
36
+ class="hidden"
37
+ readonly=""
38
+ tabindex="0"
39
+ type="checkbox"
40
+ value=""
41
+ />
42
+ <label />
43
+ </div>
44
+ <button
45
+ class="ui icon disabled left labeled button"
46
+ disabled=""
47
+ style="margin-left: 10px;"
48
+ tabindex="-1"
49
+ >
50
+ <i
51
+ aria-hidden="true"
52
+ class="tachometer alternate icon"
53
+ />
54
+ Execute profile
55
+ </button>
56
+ </div>
57
+ </div>
58
+ <div
59
+ class="row"
60
+ style="min-height: 40px; padding-top: 2px; padding-bottom: 2px;"
61
+ >
62
+ <div
63
+ class="right aligned column"
64
+ />
65
+ </div>
66
+ </div>
5
67
  <table
6
68
  class="ui table"
7
69
  >
@@ -55,7 +117,7 @@ exports[`<StructureFields /> matches the latest snapshot 1`] = `
55
117
  <a
56
118
  href="/structures/1"
57
119
  >
58
- field1
120
+ foo
59
121
  </a>
60
122
  </td>
61
123
  <td
@@ -105,7 +167,7 @@ exports[`<StructureFields /> matches the latest snapshot 1`] = `
105
167
  <a
106
168
  href="/structures/2"
107
169
  >
108
- field2
170
+ bar
109
171
  </a>
110
172
  </td>
111
173
  <td
@@ -134,5 +196,29 @@ exports[`<StructureFields /> matches the latest snapshot 1`] = `
134
196
  </tr>
135
197
  </tbody>
136
198
  </table>
199
+ <div
200
+ class="ui pagination menu"
201
+ role="navigation"
202
+ >
203
+ <a
204
+ aria-label="First"
205
+ class="disabled link item"
206
+ href="/"
207
+ >
208
+ «
209
+ </a>
210
+ <div
211
+ aria-label="Later"
212
+ class="disabled item"
213
+ >
214
+
215
+ </div>
216
+ <div
217
+ aria-label="Earlier"
218
+ class="item"
219
+ >
220
+
221
+ </div>
222
+ </div>
137
223
  </div>
138
224
  `;
@@ -1,4 +1,5 @@
1
1
  import CatalogCustomViewCards from "./CatalogCustomViewCards";
2
+ import CursorPagination from "./CursorPagination";
2
3
  import DictionaryRoutes from "./DictionaryRoutes";
3
4
  import FilteredNav from "./FilteredNav";
4
5
  import GrantRoutes from "./GrantRoutes";
@@ -53,6 +54,7 @@ import ValueConditionStructure from "./ValueConditionStructure";
53
54
 
54
55
  export {
55
56
  CatalogCustomViewCards,
57
+ CursorPagination,
56
58
  DictionaryRoutes,
57
59
  FilteredNav,
58
60
  GrantRequestBulkActions,
@@ -29,7 +29,7 @@ describe("reducers: structureFields", () => {
29
29
  expect(
30
30
  structureFields(fooState, {
31
31
  type: fetchStructure.SUCCESS,
32
- payload: { data }
32
+ payload: { data },
33
33
  })
34
34
  ).toMatchObject(data_fields);
35
35
  });
@@ -60,7 +60,7 @@ describe("reducers: structuresFields", () => {
60
60
  expect(
61
61
  structuresFields(fooState, {
62
62
  type: fetchStructure.SUCCESS,
63
- payload: { data }
63
+ payload: { data },
64
64
  })
65
65
  ).toMatchObject({ 2: data_fields });
66
66
  });
@@ -0,0 +1,20 @@
1
+ import {
2
+ getLinkedImplementationsToStructuresColumns,
3
+ defaultImplementationToStructuresColumns,
4
+ } from "..";
5
+
6
+ describe("selectors: getLinkedImplementationsToStructuresColumns", () => {
7
+ it("should return custom ruleColumns when present", () => {
8
+ const ruleImplementationToStructuresColumns = [{ name: "test" }];
9
+ const res = getLinkedImplementationsToStructuresColumns({
10
+ ruleImplementationToStructuresColumns,
11
+ });
12
+ expect(res).toHaveLength(1);
13
+ expect(res).toEqual(ruleImplementationToStructuresColumns);
14
+ });
15
+
16
+ it("should return default defaultImplementationToStructuresColumns when no customized", () => {
17
+ const res = getLinkedImplementationsToStructuresColumns({});
18
+ expect(res).toHaveLength(defaultImplementationToStructuresColumns.length);
19
+ });
20
+ });
@@ -0,0 +1,95 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import { createSelector } from "reselect";
4
+ import { FormattedMessage } from "react-intl";
5
+ import DateTime from "@truedat/core/components/DateTime";
6
+ import { formatNumber } from "@truedat/core/services/format";
7
+ import ConceptsLinkDecorator from "@truedat/bg/concepts/components/ConceptsLinkDecorator";
8
+ import RuleResultDecorator from "@truedat/dq/components/RuleResultDecorator";
9
+ import RuleImplementationLink from "@truedat/dq/components/RuleImplementationLink";
10
+ import RuleLink from "@truedat/dq/components/RuleLink";
11
+
12
+ const translateDecorator = (id) =>
13
+ id ? <FormattedMessage id={id} defaultMessage={id} /> : null;
14
+
15
+ const resultTypeDecorator = (result, result_type, resultType) =>
16
+ _.defaultTo(result_type)(resultType) === "errors_number"
17
+ ? formatNumber(result)
18
+ : `${result}%`;
19
+
20
+ export const defaultImplementationToStructuresColumns = [
21
+ {
22
+ name: "implementation_key",
23
+ fieldSelector: _.pick(["id", "implementation_key", "rule_id"]),
24
+ fieldDecorator: RuleImplementationLink,
25
+ sort: { name: "implementation_key.raw" },
26
+ width: 2,
27
+ },
28
+ {
29
+ name: "rule",
30
+ fieldSelector: ({ rule, rule_id: id }) => ({ id, name: rule?.name }),
31
+ sort: { name: "rule.name.raw" },
32
+ fieldDecorator: RuleLink,
33
+ width: 2,
34
+ },
35
+ {
36
+ name: "business_concepts",
37
+ fieldSelector: _.path("concepts"),
38
+ fieldDecorator: ConceptsLinkDecorator,
39
+ width: 2,
40
+ },
41
+ {
42
+ name: "last_execution_at",
43
+ fieldSelector: ({ execution_result_info }) => ({
44
+ value: execution_result_info?.date,
45
+ }),
46
+ fieldDecorator: DateTime,
47
+ sort: { name: "execution_result_info.date" },
48
+ width: 2,
49
+ },
50
+ {
51
+ name: "result_type",
52
+ fieldDecorator: (value) =>
53
+ _.isNil(value)
54
+ ? null
55
+ : translateDecorator(`ruleImplementations.props.result_type.${value}`),
56
+ sort: { name: "result_type.raw" },
57
+ width: 2,
58
+ },
59
+ {
60
+ name: "minimum",
61
+ fieldSelector: _.pick(["minimum", "result_type"]),
62
+ fieldDecorator: (field) =>
63
+ resultTypeDecorator(field.minimum, field.result_type),
64
+ sort: { name: "minimum" },
65
+ textAlign: "right",
66
+ width: 1,
67
+ },
68
+ {
69
+ name: "goal",
70
+ fieldSelector: _.pick(["goal", "result_type"]),
71
+ fieldDecorator: (field) =>
72
+ resultTypeDecorator(field.goal, field.result_type),
73
+ sort: { name: "goal" },
74
+ textAlign: "right",
75
+ width: 1,
76
+ },
77
+ {
78
+ name: "result",
79
+ fieldSelector: (ruleImplementation) => ({
80
+ ruleResult: ruleImplementation?.execution_result_info,
81
+ ruleImplementation,
82
+ }),
83
+ fieldDecorator: RuleResultDecorator,
84
+ textAlign: "center",
85
+ sort: { name: "execution_result_info.result.sort" },
86
+ width: 2,
87
+ },
88
+ ];
89
+
90
+ const getColumns = (state) => state.ruleImplementationToStructuresColumns;
91
+
92
+ export const getLinkedImplementationsToStructuresColumns = createSelector(
93
+ [getColumns],
94
+ _.defaultTo(defaultImplementationToStructuresColumns)
95
+ );
@@ -31,8 +31,6 @@ export {
31
31
  getSortedChildrenRelations,
32
32
  getSortedParentRelations,
33
33
  } from "./getSortedRelations";
34
- export { getSortedFields } from "./getSortedFields";
35
- export { getSortedStructureChildren } from "./getSortedStructureChildren";
36
34
  export { getStructureDeletedAt } from "./getStructureDeletedAt";
37
35
  export {
38
36
  getStructureFieldColumns,
@@ -55,6 +53,10 @@ export { getSystemTemplate } from "./getSystemTemplate";
55
53
  export { getTabVisibility } from "./getTabVisibility";
56
54
  export { groupOptionsSelector } from "./groupOptionsSelector";
57
55
  export { resourceOptionsSelector } from "./resourceOptionsSelector";
56
+ export {
57
+ getLinkedImplementationsToStructuresColumns,
58
+ defaultImplementationToStructuresColumns,
59
+ } from "./getLinkedImplementationsToStructuresColumns";
58
60
  export * from "./templateNamesSelector";
59
61
  export * from "./getGrantsColumns";
60
62
  export * from "./getLineageLevels";
@@ -1,28 +0,0 @@
1
- import React from "react";
2
- import { useIntl } from "react-intl";
3
- import { Button, Popup, Icon } from "semantic-ui-react";
4
- import StructuresProfileForm from "./StructuresProfileForm";
5
-
6
- export const StructuresProfileButton = () => {
7
- const { formatMessage } = useIntl();
8
- const trigger = (
9
- <Button
10
- icon={<Icon name="tachometer alternate" />}
11
- className="button basic icon group-actions profile structureButton"
12
- data-tooltip={formatMessage({ id: "structure.execute_profile" })}
13
- />
14
- );
15
- return (
16
- <Popup
17
- className="StructuresProfileFrom-popup"
18
- on="click"
19
- position="bottom right"
20
- flowing
21
- trigger={trigger}
22
- >
23
- <StructuresProfileForm />
24
- </Popup>
25
- );
26
- };
27
-
28
- export default StructuresProfileButton;
@@ -1,89 +0,0 @@
1
- import { includes, map, without } from "lodash/fp";
2
- import React, { useState } from "react";
3
- import PropTypes from "prop-types";
4
- import { useIntl } from "react-intl";
5
- import { connect } from "react-redux";
6
- import { Form, List, Header } from "semantic-ui-react";
7
- import { getSortedFields } from "../selectors";
8
- import { createProfileGroup } from "../routines";
9
-
10
- export const StructuresProfileForm = ({ fields, createProfileGroup }) => {
11
- const [selected, setSelected] = useState([]);
12
- const { formatMessage } = useIntl();
13
- const disabled = selected?.length === 0;
14
-
15
- const handleSubmit = e => {
16
- const payload = {
17
- data_structure_ids: selected
18
- };
19
- createProfileGroup(payload);
20
- };
21
-
22
- const handleCheckAll = () => setSelected(map("data_structure_id")(fields));
23
-
24
- const handleClearAll = () => setSelected([]);
25
-
26
- const handleChange = (e, { checked, value }) => {
27
- e && e.preventDefault() && e.stopPropagation();
28
- setSelected(checked ? [...selected, value] : without([value])(selected));
29
- };
30
-
31
- return fields ? (
32
- <Form onSubmit={handleSubmit} className="StructuresProfileForm">
33
- <List horizontal link>
34
- <List.Item
35
- as="a"
36
- onClick={handleCheckAll}
37
- content={formatMessage({
38
- id: "selectAll",
39
- defaultMessage: "Select All"
40
- })}
41
- />
42
- <List.Item
43
- as="a"
44
- onClick={handleClearAll}
45
- content={formatMessage({ id: "clearAll", defaultMessage: "Clear" })}
46
- />
47
- </List>
48
- <Header
49
- as="h5"
50
- content={formatMessage({
51
- id: "tabs.dd.fields",
52
- defaultMessage: "Fields"
53
- })}
54
- />
55
- <div className="scrollable">
56
- {fields.map(({ data_structure_id: id, name }) => (
57
- <Form.Checkbox
58
- key={id}
59
- name={name}
60
- label={<label>{name}</label>}
61
- value={id}
62
- checked={includes(id)(selected)}
63
- onChange={handleChange}
64
- />
65
- ))}
66
- </div>
67
- <Form.Button
68
- type="submit"
69
- content={formatMessage({ id: "structure.execute_profile" })}
70
- disabled={disabled}
71
- />
72
- </Form>
73
- ) : null;
74
- };
75
-
76
- StructuresProfileForm.propTypes = {
77
- createProfileGroup: PropTypes.func,
78
- fields: PropTypes.array
79
- };
80
-
81
- const mapStateToProps = state => ({
82
- fields: getSortedFields(state)
83
- });
84
- const mapDispatchToProps = { createProfileGroup };
85
-
86
- export default connect(
87
- mapStateToProps,
88
- mapDispatchToProps
89
- )(StructuresProfileForm);
@@ -1,56 +0,0 @@
1
- import React from "react";
2
- import { shallow } from "enzyme";
3
- import { render, waitFor } from "@testing-library/react";
4
- import userEvent from "@testing-library/user-event";
5
- import { intl } from "@truedat/test/intl-stub";
6
- import { StructuresProfileForm } from "../StructuresProfileForm";
7
-
8
- // workaround for enzyme issue with React.useContext
9
- // see https://github.com/airbnb/enzyme/issues/2176#issuecomment-532361526
10
- jest.spyOn(React, "useContext").mockImplementation(() => intl);
11
-
12
- describe("<StructuresProfileForm />", () => {
13
- const createProfileGroup = jest.fn();
14
- const fields = [{ data_structure_id: 1, name: "bar" }];
15
- const structure = {
16
- id: 1,
17
- external_id: 1,
18
- source: { external_id: 1 },
19
- };
20
- const props = {
21
- createProfileGroup,
22
- fields,
23
- structure,
24
- };
25
-
26
- it("matches the latest snapshot", () => {
27
- const wrapper = shallow(<StructuresProfileForm {...props} />);
28
- expect(wrapper).toMatchSnapshot();
29
- });
30
-
31
- it("submit button is disable when no fields are selected", async () => {
32
- const { getByText } = render(<StructuresProfileForm {...props} />);
33
-
34
- await waitFor(() => getByText("structure.execute_profile"));
35
-
36
- expect(getByText("structure.execute_profile")).toHaveAttribute("disabled");
37
- });
38
-
39
- it("calls create job when button handleSubmit click", async () => {
40
- const { getByText } = render(<StructuresProfileForm {...props} />);
41
-
42
- await waitFor(() => userEvent.click(getByText("bar")));
43
-
44
- expect(getByText("structure.execute_profile")).not.toHaveAttribute(
45
- "disabled"
46
- );
47
-
48
- await waitFor(() =>
49
- userEvent.click(getByText("structure.execute_profile"))
50
- );
51
-
52
- expect(createProfileGroup).toHaveBeenCalledWith({
53
- data_structure_ids: [structure.id],
54
- });
55
- });
56
- });
@@ -1,54 +0,0 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
- exports[`<StructuresProfileForm /> matches the latest snapshot 1`] = `
4
- <Form
5
- as="form"
6
- className="StructuresProfileForm"
7
- onSubmit={[Function]}
8
- >
9
- <List
10
- horizontal={true}
11
- link={true}
12
- >
13
- <ListItem
14
- as="a"
15
- content="Select All"
16
- onClick={[Function]}
17
- />
18
- <ListItem
19
- as="a"
20
- content="Clear"
21
- onClick={[Function]}
22
- />
23
- </List>
24
- <Header
25
- as="h5"
26
- content="tabs.dd.fields"
27
- />
28
- <div
29
- className="scrollable"
30
- >
31
- <FormCheckbox
32
- as={[Function]}
33
- checked={false}
34
- control={[Function]}
35
- key="1"
36
- label={
37
- <label>
38
- bar
39
- </label>
40
- }
41
- name="bar"
42
- onChange={[Function]}
43
- value={1}
44
- />
45
- </div>
46
- <FormButton
47
- as={[Function]}
48
- content="structure.execute_profile"
49
- control={[Function]}
50
- disabled={true}
51
- type="submit"
52
- />
53
- </Form>
54
- `;
@@ -1,39 +0,0 @@
1
- import { getSortedFields } from "..";
2
-
3
- const projects = [
4
- { name: "foo2", type: "project" },
5
- { name: "foo1", type: "project" },
6
- ];
7
- const schemas = [
8
- { name: "bar2", type: "schema", metadata: { order: 1 } },
9
- { name: "bar1", type: "schema", metadata: { order: 1 } },
10
- ];
11
- const documents = [
12
- { name: "baz1", type: "document" },
13
- { name: "baz2", type: "document" },
14
- ];
15
- const others = [
16
- { name: "bay1", type: "other", metadata: { order: "0" } },
17
- { name: "bay2", type: "other", metadata: { order: "0" } },
18
- ];
19
-
20
- describe("selectors: getSortedFields", () => {
21
- const state = {
22
- structureFields: [...documents, ...projects, ...schemas, ...others],
23
- };
24
- const foo = [
25
- ...others,
26
- ...[
27
- { name: "bar1", type: "schema", metadata: { order: 1 } },
28
- { name: "bar2", type: "schema", metadata: { order: 1 } },
29
- ],
30
- ...[
31
- { name: "foo1", type: "project" },
32
- { name: "foo2", type: "project" },
33
- ],
34
- ...documents,
35
- ];
36
- it("get fields sorted by type criteria and name", () => {
37
- expect(getSortedFields(state)).toEqual(foo);
38
- });
39
- });
@@ -1,36 +0,0 @@
1
- import { getSortedStructureChildren } from "..";
2
-
3
- const projects = [
4
- { name: "foo2", type: "project" },
5
- { name: "foo1", type: "project" }
6
- ];
7
- const schemas = [
8
- { name: "bar1", type: "schema" },
9
- { name: "bar2", type: "schema" }
10
- ];
11
- const documents = [
12
- { name: "baz1", type: "document" },
13
- { name: "baz2", type: "document" }
14
- ];
15
- const others = [
16
- { name: "bay1", type: "other" },
17
- { name: "bay2", type: "other" }
18
- ];
19
-
20
- describe("selectors: getSortedStructureChildren", () => {
21
- const state = {
22
- structureChildren: [...others, ...documents, ...schemas, ...projects]
23
- };
24
- const foo = [
25
- ...[
26
- { name: "foo1", type: "project" },
27
- { name: "foo2", type: "project" }
28
- ],
29
- ...schemas,
30
- ...documents,
31
- ...others
32
- ];
33
- it("get structure children sorted by type criteria and name", () => {
34
- expect(getSortedStructureChildren(state)).toEqual(foo);
35
- });
36
- });
@@ -1,9 +0,0 @@
1
- import _ from "lodash/fp";
2
- import { createSelector } from "reselect";
3
- import { getStructureSortingCriteria } from "./getStructureSortingCriteria";
4
-
5
- export const getSortedFields = createSelector(
6
- [_.propOr([], "structureFields"), getStructureSortingCriteria],
7
- (structureFields, structureSortingCriteria) =>
8
- _.sortBy(structureSortingCriteria)(structureFields)
9
- );
@@ -1,23 +0,0 @@
1
- import _ from "lodash/fp";
2
- import { createSelector } from "reselect";
3
- import { getStructureSortingCriteria } from "./getStructureSortingCriteria";
4
-
5
- export const getSortedStructureChildren = createSelector(
6
- [
7
- _.propOr([], "structureChildren"),
8
- _.propOr([], "structureSiblings"),
9
- _.propOr([], "systemStructures"),
10
- getStructureSortingCriteria,
11
- ],
12
- (
13
- structureChildren,
14
- structureSiblings,
15
- systemStructures,
16
- structureSortingCriteria
17
- ) =>
18
- _.size(structureChildren)
19
- ? _.sortBy(structureSortingCriteria)(structureChildren)
20
- : _.size(structureSiblings)
21
- ? _.sortBy(structureSortingCriteria)(structureSiblings)
22
- : _.sortBy(structureSortingCriteria)(systemStructures)
23
- );