@truedat/dq 4.56.2 → 4.56.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.56.4] 2022-11-26
4
+
5
+ ### Fixed
6
+
7
+ - [TD-5247] Keep table name if structure field dropdown item changes
8
+
9
+ ## [4.56.3] 2022-11-24
10
+
11
+ ### Added
12
+
13
+ - [TD-5286] Implementation results pagination
14
+
3
15
  ## [4.56.2] 2022-11-24
4
16
 
5
17
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/dq",
3
- "version": "4.56.2",
3
+ "version": "4.56.4",
4
4
  "description": "Truedat Web Data Quality Module",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -34,7 +34,7 @@
34
34
  "@testing-library/jest-dom": "^5.16.4",
35
35
  "@testing-library/react": "^12.0.0",
36
36
  "@testing-library/user-event": "^13.2.1",
37
- "@truedat/test": "4.56.0",
37
+ "@truedat/test": "4.56.4",
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",
@@ -93,8 +93,8 @@
93
93
  },
94
94
  "dependencies": {
95
95
  "@apollo/client": "^3.7.0",
96
- "@truedat/core": "4.56.2",
97
- "@truedat/df": "4.56.2",
96
+ "@truedat/core": "4.56.4",
97
+ "@truedat/df": "4.56.4",
98
98
  "graphql": "^15.5.3",
99
99
  "path-to-regexp": "^1.7.0",
100
100
  "prop-types": "^15.8.1",
@@ -114,5 +114,5 @@
114
114
  "react-dom": ">= 16.8.6 < 17",
115
115
  "semantic-ui-react": ">= 0.88.2 < 2.1"
116
116
  },
117
- "gitHead": "d76f2da5ed4892e557e4aa159d4e9ff009e0e853"
117
+ "gitHead": "f327860aa80db650f562d53dc22218307f43e0ac"
118
118
  }
@@ -15,16 +15,17 @@ export const IMPLEMENTATION_VERSIONS_QUERY = gql`
15
15
  }
16
16
  `;
17
17
 
18
- export const IMPLEMENTATION_RESULTS_QUERY = gql`
19
- query Implementation($id: ID!) {
18
+ export const IMPLEMENTATION_RESULTS_CONNECTION_QUERY = gql`
19
+ query Implementation(
20
+ $id: ID!
21
+ $after: Cursor
22
+ $before: Cursor
23
+ $last: Int
24
+ $first: Int
25
+ ) {
20
26
  implementation(id: $id) {
21
27
  id
22
- implementationKey
23
- lastQualityEvent {
24
- insertedAt
25
- message
26
- type
27
- }
28
+ version
28
29
  versions {
29
30
  id
30
31
  implementationKey
@@ -32,7 +33,20 @@ export const IMPLEMENTATION_RESULTS_QUERY = gql`
32
33
  status
33
34
  minimum
34
35
  goal
35
- results {
36
+ }
37
+ lastQualityEvent {
38
+ insertedAt
39
+ message
40
+ type
41
+ }
42
+ resultsConnection(
43
+ first: $first
44
+ last: $last
45
+ before: $before
46
+ after: $after
47
+ ) {
48
+ totalCount
49
+ page {
36
50
  id
37
51
  date
38
52
  details
@@ -43,8 +57,22 @@ export const IMPLEMENTATION_RESULTS_QUERY = gql`
43
57
  records
44
58
  result
45
59
  resultType
60
+ implementation {
61
+ id
62
+ version
63
+ }
64
+ __typename
65
+ }
66
+ pageInfo {
67
+ startCursor
68
+ endCursor
69
+ hasNextPage
70
+ hasPreviousPage
71
+ __typename
46
72
  }
73
+ __typename
47
74
  }
75
+ __typename
48
76
  }
49
77
  }
50
78
  `;
@@ -9,26 +9,31 @@ import { linkTo } from "@truedat/core/routes";
9
9
 
10
10
  const concatValues = (values, link) => _.join(link)(values);
11
11
 
12
- const LinkToStructure = ({ value }) =>
12
+ const LinkToStructure = ({ value, aliasArray }) =>
13
13
  value?.id ? (
14
14
  <Link to={linkTo.STRUCTURE({ id: value.id })}>
15
- <span className="highlighted">{`"${
16
- !_.isEmpty(value.path) ? path(value) : value.name
17
- }"`}</span>{" "}
15
+ <div className="highlighted">
16
+ {qualifySqlIdentifier({ aliasArray, value })}
17
+ </div>{" "}
18
18
  </Link>
19
19
  ) : (
20
20
  <span>{value.name}</span>
21
21
  );
22
22
 
23
- const FormattedLink = ({ value, operator = {} }) => {
23
+ LinkToStructure.propTypes = {
24
+ value: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
25
+ aliasArray: PropTypes.array,
26
+ };
27
+
28
+ const FormattedLink = ({ value, aliasArray, operator = {} }) => {
24
29
  switch (operator?.value_type) {
25
30
  case "field":
26
- return <LinkToStructure value={value} />;
31
+ return <LinkToStructure value={value} aliasArray={aliasArray} />;
27
32
  case "field_list":
28
33
  return (
29
34
  <>
30
35
  {_.map.convert({ cap: false })((v, i) => (
31
- <LinkToStructure key={i} value={v} />
36
+ <LinkToStructure key={i} value={v} aliasArray={aliasArray} />
32
37
  ))(value)}
33
38
  </>
34
39
  );
@@ -49,6 +54,7 @@ const FormattedLink = ({ value, operator = {} }) => {
49
54
  FormattedLink.propTypes = {
50
55
  value: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
51
56
  operator: PropTypes.object,
57
+ aliasArray: PropTypes.array,
52
58
  };
53
59
 
54
60
  const nilOrEmpty = (v) => _.isNil(v) || v === "";
@@ -101,7 +107,7 @@ export const getValues = ({ value = [], operator = {}, value_modifier }) => {
101
107
  ? valuesFromKeys(
102
108
  defaultOrValue,
103
109
  ["name"],
104
- ["path", "id", "type", "parent_index"],
110
+ ["path", "metadata", "id", "type", "parent_index"],
105
111
  value_modifier
106
112
  )
107
113
  : valuesFromKeys(defaultOrValue, ["raw"], [], value_modifier);
@@ -124,12 +130,15 @@ OperatorMessage.propTypes = {
124
130
  operator: PropTypes.object,
125
131
  };
126
132
 
133
+ const qualifySqlIdentifier = ({ aliasArray, value }) =>
134
+ _.flow(
135
+ _.find({ index: value?.parent_index }),
136
+ _.propOr(null, "text"),
137
+ _.defaultTo(value?.metadata?.table || _.last(value?.path)),
138
+ (qualifier) => (qualifier ? `(${qualifier}).${value?.name}` : value?.name)
139
+ )(aliasArray);
140
+
127
141
  const ConditionCell = ({ row, alias, validationPopulationCondition }) => {
128
- const alias_text =
129
- _.flow(
130
- _.find({ index: row.structure?.parent_index }),
131
- _.propOr(null, "text")
132
- )(alias) || (row.structure ? _.last(row.structure.path) : null);
133
142
  const values = getValues(row);
134
143
  const operator = _.prop("operator")(row);
135
144
  const { formatMessage } = useIntl();
@@ -154,8 +163,10 @@ const ConditionCell = ({ row, alias, validationPopulationCondition }) => {
154
163
  })}
155
164
  >
156
165
  <span className="highlighted">
157
- {alias_text && `(${alias_text}).`}{" "}
158
- {`"${_.pathOr("", "structure.name")(row)}"`}
166
+ {qualifySqlIdentifier({
167
+ aliasArray: alias,
168
+ value: row.structure,
169
+ })}
159
170
  </span>
160
171
  </Link>
161
172
  </>
@@ -190,8 +201,10 @@ const ConditionCell = ({ row, alias, validationPopulationCondition }) => {
190
201
  })}
191
202
  >
192
203
  <span className="highlighted">
193
- {alias_text && `(${alias_text}).`}{" "}
194
- {`"${_.pathOr("", "name")(row)}"`}
204
+ {qualifySqlIdentifier({
205
+ aliasArray: alias,
206
+ value: row,
207
+ })}
195
208
  </span>
196
209
  </Link>
197
210
  ) : (
@@ -214,7 +227,11 @@ const ConditionCell = ({ row, alias, validationPopulationCondition }) => {
214
227
  <Table.Cell width={5}>
215
228
  {_.map.convert({ cap: false })((value, i) => (
216
229
  <Fragment key={i}>
217
- <FormattedLink value={value} operator={operator} />
230
+ <FormattedLink
231
+ value={value}
232
+ operator={operator}
233
+ aliasArray={alias}
234
+ />
218
235
  {i + 1 < _.size(values) && (
219
236
  <span>
220
237
  {" "}
@@ -58,33 +58,36 @@ ExecutionRow.propTypes = {
58
58
  implementation: PropTypes.object,
59
59
  };
60
60
 
61
- export const ImplementationExecutions = ({ page }) => (
62
- <Table basic="very">
63
- <Table.Header>
64
- <Table.Row>
65
- <Table.HeaderCell width="1" textAlign="right">
66
- <FormattedMessage id="ruleImplementations.props.version" />
67
- </Table.HeaderCell>
68
- <Table.HeaderCell width="2">
69
- <FormattedMessage id="execution" />
70
- </Table.HeaderCell>
71
- <Table.HeaderCell width="2">
72
- <FormattedMessage id="execution.latestEvent.insertedAt" />
73
- </Table.HeaderCell>
74
- <Table.HeaderCell width="2">
75
- <FormattedMessage id="execution.latestEvent.type" />
76
- </Table.HeaderCell>
77
- <Table.HeaderCell>
78
- <FormattedMessage id="execution.latestEvent.message" />
79
- </Table.HeaderCell>
80
- </Table.Row>
81
- </Table.Header>
82
- <Table.Body>
83
- {page.map((props, key) => (
84
- <ExecutionRow key={key} {...props} />
85
- ))}
86
- </Table.Body>
87
- </Table>
61
+ export const ImplementationExecutions = (connection) => (
62
+ <>
63
+ <Table basic="very">
64
+ <Table.Header>
65
+ <Table.Row>
66
+ <Table.HeaderCell width="1" textAlign="right">
67
+ <FormattedMessage id="ruleImplementations.props.version" />
68
+ </Table.HeaderCell>
69
+ <Table.HeaderCell width="2">
70
+ <FormattedMessage id="execution" />
71
+ </Table.HeaderCell>
72
+ <Table.HeaderCell width="2">
73
+ <FormattedMessage id="execution.latestEvent.insertedAt" />
74
+ </Table.HeaderCell>
75
+ <Table.HeaderCell width="2">
76
+ <FormattedMessage id="execution.latestEvent.type" />
77
+ </Table.HeaderCell>
78
+ <Table.HeaderCell>
79
+ <FormattedMessage id="execution.latestEvent.message" />
80
+ </Table.HeaderCell>
81
+ </Table.Row>
82
+ </Table.Header>
83
+ <Table.Body>
84
+ {connection.page.map((props, key) => (
85
+ <ExecutionRow key={key} {...props} />
86
+ ))}
87
+ </Table.Body>
88
+ </Table>
89
+ <CursorPagination {...connection} />
90
+ </>
88
91
  );
89
92
 
90
93
  ImplementationExecutions.propTypes = {
@@ -111,9 +114,6 @@ export const ImplementationExecutionsLoader = () => {
111
114
  <Grid.Column>
112
115
  <ImplementationExecutionFilters />
113
116
  </Grid.Column>
114
- <Grid.Column textAlign="right">
115
- <CursorPagination {...connection} />
116
- </Grid.Column>
117
117
  </Grid.Row>
118
118
  </Grid>
119
119
  {loading ? <Loading /> : null}
@@ -4,29 +4,28 @@ import { useParams } from "react-router-dom";
4
4
  import { useQuery } from "@apollo/client";
5
5
  import PropTypes from "prop-types";
6
6
  import { useIntl } from "react-intl";
7
- import { Message, Divider } from "semantic-ui-react";
7
+ import { Segment, Message, Divider } from "semantic-ui-react";
8
8
  import { columnDecorator } from "@truedat/core/services";
9
9
  import { DateTime, Loading } from "@truedat/core/components";
10
- import { IMPLEMENTATION_RESULTS_QUERY } from "../api/queries";
10
+ import { CursorPagination } from "@truedat/core/components";
11
+ import { useLocation } from "react-router-dom";
12
+ import queryString from "query-string";
13
+ import { IMPLEMENTATION_RESULTS_CONNECTION_QUERY } from "../api/queries";
11
14
  import RuleResultsTable from "./RuleResultsTable";
12
15
 
16
+ export const PAGE_SIZE = 20;
17
+
13
18
  export const getCustomColumnsWithData = (ruleResults, columns) =>
14
19
  _.filter((column) =>
15
20
  _.any(_.flow(columnDecorator(column), _.negate(_.isEmpty)))(ruleResults)
16
21
  )(columns);
17
22
 
18
- export const RuleImplementationResults = ({ ruleImplementation }) => {
23
+ export const RuleImplementationResults = ({ implementation, results }) => {
19
24
  const { formatMessage, locale } = useIntl();
20
25
 
21
- if (_.isEmpty(ruleImplementation)) return null;
22
-
23
- const implementationVersions = _.propOr([], "versions")(ruleImplementation);
24
- const currentImplementationVersion = _.flow(
25
- _.filter((version) => version.id === ruleImplementation.id),
26
- _.first
27
- )(implementationVersions);
26
+ if (_.isEmpty(implementation)) return null;
28
27
 
29
- const { lastQualityEvent } = ruleImplementation;
28
+ const { lastQualityEvent } = implementation;
30
29
 
31
30
  return (
32
31
  <>
@@ -46,31 +45,45 @@ export const RuleImplementationResults = ({ ruleImplementation }) => {
46
45
  <p style={{ whiteSpace: "pre-wrap" }}>{lastQualityEvent.message}</p>
47
46
  </Message>
48
47
  ) : null}
49
- <RuleResultsTable
50
- currentImplementationVersion={currentImplementationVersion}
51
- implementationVersions={implementationVersions}
52
- />
48
+ <RuleResultsTable implementation={implementation} results={results} />
53
49
  </>
54
50
  );
55
51
  };
56
52
 
57
53
  RuleImplementationResults.propTypes = {
58
- ruleImplementation: PropTypes.object,
54
+ results: PropTypes.array,
55
+ implementation: PropTypes.object,
59
56
  };
60
57
 
61
58
  export const RuleImplementationResultsLoader = (props) => {
62
59
  const { implementation_id: id } = useParams();
63
- const { loading, error, data } = useQuery(IMPLEMENTATION_RESULTS_QUERY, {
64
- variables: { id },
65
- });
60
+
61
+ const { search } = useLocation();
62
+ const { before, after } = queryString.parse(search);
63
+ const variables = after
64
+ ? { id, after, before, first: PAGE_SIZE }
65
+ : { id, after, before, last: PAGE_SIZE };
66
+
67
+ const { loading, error, data } = useQuery(
68
+ IMPLEMENTATION_RESULTS_CONNECTION_QUERY,
69
+ {
70
+ variables,
71
+ }
72
+ );
66
73
  if (error) return null;
67
74
  if (loading) return <Loading />;
68
- const ruleImplementation = data?.implementation;
75
+ const implementation = data?.implementation;
76
+ const connection = data?.implementation?.resultsConnection;
77
+ const results = implementation?.resultsConnection?.page;
69
78
  return (
70
- <RuleImplementationResults
71
- ruleImplementation={ruleImplementation}
72
- {...props}
73
- />
79
+ <Segment attached="bottom">
80
+ <RuleImplementationResults
81
+ implementation={implementation}
82
+ results={results}
83
+ {...props}
84
+ />
85
+ <CursorPagination {...connection} />
86
+ </Segment>
74
87
  );
75
88
  };
76
89
 
@@ -79,7 +79,10 @@ export const RuleResultRow = ({
79
79
  <>
80
80
  <Icon
81
81
  name="circle"
82
- color={selectColor({ ...ruleImplementation, ...ruleResult })}
82
+ color={selectColor({
83
+ ...ruleImplementation,
84
+ ...ruleResult,
85
+ })}
83
86
  />
84
87
  {`${parseFloat(ruleResult.result)} %`}
85
88
  </>
@@ -21,22 +21,14 @@ export const getCustomColumnsWithData = (ruleResults, columns) =>
21
21
  _.any(_.flow(columnDecorator(column), _.negate(_.isEmpty)))(ruleResults)
22
22
  )(columns);
23
23
 
24
- export const RuleResultsTable = ({
25
- columns,
26
- implementationVersions,
27
- currentImplementationVersion,
28
- }) => {
24
+ export const RuleResultsTable = ({ columns, implementation, results }) => {
29
25
  const isAdmin = useAuthorized();
30
26
  const { formatMessage } = useIntl();
31
27
 
32
- const ruleResults = _.flow(
33
- _.map((iv) => _.pathOr([], "results")(iv)),
34
- _.flatten
35
- )(implementationVersions);
36
- const optionalColumns = getOptionalColumnsWithData(ruleResults);
37
- const customColumns = getCustomColumnsWithData(ruleResults, columns);
28
+ const optionalColumns = getOptionalColumnsWithData(results);
29
+ const customColumns = getCustomColumnsWithData(results, columns);
38
30
 
39
- return _.isEmpty(ruleResults) ? (
31
+ return _.isEmpty(results) ? (
40
32
  <Message
41
33
  style={{ marginTop: "14px" }}
42
34
  header={formatMessage({
@@ -77,28 +69,43 @@ export const RuleResultsTable = ({
77
69
  </Table.Row>
78
70
  </Table.Header>
79
71
  <Table.Body>
80
- {implementationVersions.map((version) =>
81
- _.pathOr(
82
- [],
83
- "results"
84
- )(version).map((result, key) => (
72
+ {_.flow(
73
+ _.groupBy("implementation.version"),
74
+ _.toPairs, // first element: version number; second element: version results.
75
+ // order by descending version number
76
+ _.orderBy(
77
+ (versionAndResultsTuple) => versionAndResultsTuple[0],
78
+ "desc"
79
+ ),
80
+ _.mapValues((versionAndResultsTuple) =>
81
+ versionAndResultsTuple[1].map(
82
+ (result, indexWithinVersionGroup) => ({
83
+ indexWithinVersionGroup,
84
+ ...result,
85
+ })
86
+ )
87
+ ),
88
+ _.reduce((acc, versionResults) => [...acc, ...versionResults], []),
89
+ _.map((result) => (
85
90
  <RuleResultRow
86
- key={key}
91
+ key={result.id}
87
92
  optionalColumns={optionalColumns}
88
93
  ruleResult={result}
89
94
  customColumns={customColumns}
90
- ruleImplementation={version}
91
- active={currentImplementationVersion.id === version.id}
95
+ ruleImplementation={_.find(
96
+ (version) => version?.id === result?.implementation?.id
97
+ )(implementation?.versions)}
98
+ active={implementation.version === result.implementation.version}
92
99
  tagLabel={
93
- key === 0
100
+ result.indexWithinVersionGroup === 0
94
101
  ? `${formatMessage({
95
102
  id: "ruleImplementations.props.version",
96
- })}: ${version.version}`
103
+ })}: ${result.implementation.version}`
97
104
  : null
98
105
  }
99
106
  />
100
107
  ))
101
- )}
108
+ )(results)}
102
109
  </Table.Body>
103
110
  </Table>
104
111
  );
@@ -106,8 +113,8 @@ export const RuleResultsTable = ({
106
113
 
107
114
  RuleResultsTable.propTypes = {
108
115
  columns: PropTypes.array,
109
- implementationVersions: PropTypes.array,
110
- currentImplementationVersion: PropTypes.object,
116
+ implementation: PropTypes.object,
117
+ results: PropTypes.array,
111
118
  };
112
119
 
113
120
  const mapStateToProps = (state) => ({
@@ -266,6 +266,18 @@ describe("<NewRuleImplementation> NewRuleImplementation doSubmit", () => {
266
266
  fields: [
267
267
  {
268
268
  id: 11127109,
269
+ metadata: {
270
+ data_type_class: "string",
271
+ database: "xe",
272
+ default: "None",
273
+ host: "localhost",
274
+ nullable: false,
275
+ order: "4",
276
+ precision: "25",
277
+ schema: "HR",
278
+ table: "EMPLOYEES",
279
+ type: "VARCHAR2",
280
+ },
269
281
  name: "EMAIL",
270
282
  parent_index: 4,
271
283
  path: undefined,
@@ -273,6 +285,18 @@ describe("<NewRuleImplementation> NewRuleImplementation doSubmit", () => {
273
285
  },
274
286
  {
275
287
  id: 11127116,
288
+ metadata: {
289
+ data_type_class: "string",
290
+ database: "xe",
291
+ default: "None",
292
+ host: "localhost",
293
+ nullable: true,
294
+ order: "5",
295
+ precision: "20",
296
+ schema: "HR",
297
+ table: "EMPLOYEES",
298
+ type: "VARCHAR2",
299
+ },
276
300
  name: "PHONE_NUMBER",
277
301
  parent_index: 4,
278
302
  path: undefined,
@@ -1,78 +1,31 @@
1
1
  import _ from "lodash/fp";
2
2
  import React from "react";
3
3
  import { render } from "@truedat/test/render";
4
- import {
5
- RuleImplementationResults,
4
+ import RuleImplementationResults, {
6
5
  getCustomColumnsWithData,
6
+ PAGE_SIZE,
7
7
  } from "../RuleImplementationResults";
8
8
 
9
+ import { implementationResultsMock } from "./__fixtures__/implementationResultsMocks";
10
+
11
+ jest.mock("react-router-dom", () => ({
12
+ ...jest.requireActual("react-router-dom"),
13
+ useParams: () => ({ implementation_id: "2" }),
14
+ }));
15
+
9
16
  describe("<RuleImplementationResults />", () => {
10
- const ruleImplementation = {
11
- id: 1,
12
- implementation_key: "default_key",
13
- versions: [
14
- {
15
- implementation_key: "default_key",
16
- id: 1,
17
- goal: "20.0",
18
- minimum: "10.0",
19
- results: [
20
- {
21
- errors: 2,
22
- hasRemediation: false,
23
- hasSegments: true,
24
- id: "66577",
25
- records: 31,
26
- result: "93.54",
27
- resultType: "percentage",
28
- },
29
- {
30
- errors: 2,
31
- hasRemediation: false,
32
- hasSegments: true,
33
- id: "66578",
34
- records: 31,
35
- result: "93.54",
36
- resultType: "percentage",
37
- },
38
- ],
39
- },
40
- {
41
- implementation_key: "key_2",
42
- id: 2,
43
- goal: "20.0",
44
- minimum: "10.0",
45
- results: [
46
- {
47
- errors: 2,
48
- hasRemediation: false,
49
- hasSegments: true,
50
- id: "66579",
51
- records: 31,
52
- result: "93.54",
53
- resultType: "percentage",
54
- },
55
- {
56
- errors: 2,
57
- hasRemediation: false,
58
- hasSegments: true,
59
- id: "66580",
60
- records: 31,
61
- result: "93.54",
62
- resultType: "percentage",
63
- },
64
- ],
65
- },
66
- ],
17
+ const renderOpts = {
18
+ mocks: [implementationResultsMock({ id: "2", last: PAGE_SIZE })],
67
19
  };
20
+
68
21
  const ruleResultsColumns = [{ foo: "bar", name: "foo" }];
69
- const props = {
70
- ruleImplementation,
71
- ruleResultsColumns,
72
- };
73
22
 
74
- it("matches the latest snapshot", () => {
75
- const { container } = render(<RuleImplementationResults {...props} />);
23
+ it("matches the latest snapshot", async () => {
24
+ const { container, findByRole } = render(
25
+ <RuleImplementationResults />,
26
+ renderOpts
27
+ );
28
+ expect(await findByRole("table")).toBeInTheDocument();
76
29
  expect(container).toMatchSnapshot();
77
30
  });
78
31
  });