@truedat/dq 4.37.1 → 4.37.2

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.37.2] 2022-02-02
4
+
5
+ ### Added
6
+
7
+ - [TD-4294] Rule audit tab
8
+
3
9
  ## [4.37.1] 2022-02-02
4
10
 
5
11
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/dq",
3
- "version": "4.37.1",
3
+ "version": "4.37.2",
4
4
  "description": "Truedat Web Data Quality Module",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -82,8 +82,8 @@
82
82
  },
83
83
  "dependencies": {
84
84
  "@apollo/client": "^3.4.10",
85
- "@truedat/core": "4.37.1",
86
- "@truedat/df": "4.37.1",
85
+ "@truedat/core": "4.37.2",
86
+ "@truedat/df": "4.37.2",
87
87
  "axios": "^0.19.2",
88
88
  "graphql": "^15.5.3",
89
89
  "path-to-regexp": "^1.7.0",
@@ -103,5 +103,5 @@
103
103
  "react-dom": ">= 16.8.6 < 17",
104
104
  "semantic-ui-react": ">= 0.88.2 < 2.1"
105
105
  },
106
- "gitHead": "36ffd7e86efe27f38c3ae1bb616eb9eb4d444d12"
106
+ "gitHead": "ca6ba01d31ab5ec415b9b21d6ce02b418fc6e0ff"
107
107
  }
@@ -0,0 +1,47 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import PropTypes from "prop-types";
4
+ import { Feed } from "semantic-ui-react";
5
+ import { useIntl } from "react-intl";
6
+ import Moment from "react-moment";
7
+
8
+ export const RuleEventRow = ({ user, user_name, ts, payload }) => {
9
+ const { formatMessage, locale } = useIntl();
10
+ const userName = _.propOr(user_name, "user_name")(user);
11
+
12
+ return _.isEmpty(payload) ? null : (
13
+ <Feed.Event>
14
+ <Feed.Content>
15
+ <Feed.Summary>
16
+ <Feed.User>{userName}</Feed.User>{" "}
17
+ <Feed.Date>
18
+ <Moment locale={locale} date={ts} format="YYYY-MM-DD HH:mm" />
19
+ </Feed.Date>
20
+ </Feed.Summary>
21
+ {payload.map(({ field, action, value }, i) => (
22
+ <Feed.Extra
23
+ key={i}
24
+ text
25
+ content={formatMessage(
26
+ {
27
+ id: `rules.events.action_${action}${
28
+ typeof value === "undefined" || _.isObject(value) ? "" : "_to"
29
+ }`,
30
+ },
31
+ [field, JSON.stringify(value)]
32
+ )}
33
+ />
34
+ ))}
35
+ </Feed.Content>
36
+ </Feed.Event>
37
+ );
38
+ };
39
+
40
+ RuleEventRow.propTypes = {
41
+ user: PropTypes.object,
42
+ user_name: PropTypes.string,
43
+ ts: PropTypes.string,
44
+ payload: PropTypes.array,
45
+ };
46
+
47
+ export default RuleEventRow;
@@ -0,0 +1,32 @@
1
+
2
+ import React from "react";
3
+ import PropTypes from "prop-types";
4
+ import { Feed, Segment } from "semantic-ui-react";
5
+ import { connect } from "react-redux";
6
+ import { getParsedEvents } from "../selectors";
7
+ import { RuleEventRow } from "./RuleEventRow";
8
+
9
+ export const RuleEvents = ({ events, eventsLoading }) => {
10
+ return eventsLoading ? null : (
11
+ <Segment attached="bottom">
12
+ <Feed size="small">
13
+ {events.map((e, i) => (
14
+ <RuleEventRow key={i} {...e} />
15
+ ))}
16
+ </Feed>
17
+ </Segment>
18
+ );
19
+ };
20
+ RuleEvents.propTypes = {
21
+ events: PropTypes.array.isRequired,
22
+ eventsLoading: PropTypes.bool,
23
+ };
24
+
25
+ const mapStateToProps = (state) => {
26
+ return {
27
+ events: getParsedEvents(state),
28
+ eventsLoading: state.eventsLoading,
29
+ };
30
+ };
31
+
32
+ export default connect(mapStateToProps)(RuleEvents);
@@ -9,6 +9,7 @@ import { useAuthorized } from "@truedat/core/hooks";
9
9
  import {
10
10
  RULE,
11
11
  RULE_EDIT,
12
+ RULE_EVENTS,
12
13
  RULE_IMPLEMENTATION_RESULT_DETAILS,
13
14
  RULE_IMPLEMENTATION_RESULTS_DETAILS,
14
15
  RULE_IMPLEMENTATION,
@@ -32,6 +33,7 @@ import NewRule from "./NewRule";
32
33
  import NewRuleImplementation from "./NewRuleImplementation";
33
34
  import Rule from "./Rule";
34
35
  import RuleCrumbs from "./RuleCrumbs";
36
+ import RuleEvents from "./RuleEvents";
35
37
  import RuleImplementation from "./RuleImplementation";
36
38
  import RuleImplementationEvents from "./RuleImplementationEvents";
37
39
  import RuleImplementationLoader from "./RuleImplementationLoader";
@@ -61,6 +63,12 @@ const ImplementationStructuresLoader = React.lazy(() =>
61
63
  const QualityTemplatesLoader = () => <TemplatesLoader scope="dq" />;
62
64
  const ImplementationTemplatesLoader = () => <TemplatesLoader scope="ri" />;
63
65
 
66
+ const RuleEventsLoader = () => {
67
+ const match = useRouteMatch();
68
+ const id = _.path("params.id")(match);
69
+ return <EventsLoader resource_id={id} resource_type="rule" />;
70
+ };
71
+
64
72
  const ImplementationEventsLoader = () => {
65
73
  const match = useRouteMatch();
66
74
  const id = _.path("params.implementation_id")(match);
@@ -135,6 +143,22 @@ const RuleRoutes = ({
135
143
  </>
136
144
  )}
137
145
  />
146
+
147
+ <Route
148
+ exact
149
+ path={RULE_EVENTS}
150
+ render={() => (
151
+ <>
152
+ <RuleCrumbs />
153
+ <Segment>
154
+ <RuleEventsLoader />
155
+ {ruleLoaded && <Rule />}
156
+ {ruleLoaded && <RuleEvents />}
157
+ </Segment>
158
+ </>
159
+ )}
160
+ />
161
+
138
162
  <Route
139
163
  exact
140
164
  path={RULE_IMPLEMENTATIONS}
@@ -6,7 +6,12 @@ import { Link } from "react-router-dom";
6
6
  import { connect } from "react-redux";
7
7
  import { FormattedMessage } from "react-intl";
8
8
  import { usePath } from "@truedat/core/hooks";
9
- import { RULE, RULE_IMPLEMENTATIONS, linkTo } from "@truedat/core/routes";
9
+ import {
10
+ RULE,
11
+ RULE_IMPLEMENTATIONS,
12
+ RULE_EVENTS,
13
+ linkTo,
14
+ } from "@truedat/core/routes";
10
15
 
11
16
  const RuleTabs = ({ rule }) => {
12
17
  const path = usePath();
@@ -22,6 +27,15 @@ const RuleTabs = ({ rule }) => {
22
27
  >
23
28
  <FormattedMessage id="tabs.dq.ruleImplementations" />
24
29
  </Menu.Item>
30
+ <Menu.Item
31
+ active={path === RULE_EVENTS}
32
+ as={Link}
33
+ to={linkTo.RULE_EVENTS({
34
+ id: rule.id,
35
+ })}
36
+ >
37
+ <FormattedMessage id="tabs.dq.rule.audit" />
38
+ </Menu.Item>
25
39
  </Menu>
26
40
  );
27
41
  };
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import { render } from "@truedat/test/render";
3
+ import { RuleEventRow } from "../RuleEventRow";
4
+
5
+ describe("<RuleEventsRow />", () => {
6
+ const props = {
7
+ payload: [
8
+ {
9
+ field: "field1",
10
+ action: "changed",
11
+ value: "new value",
12
+ },
13
+ ],
14
+ user_name: "some.user@domain.tld",
15
+ user: {
16
+ email: "some.user@domain.tld",
17
+ full_name: "Some User",
18
+ id: 142,
19
+ user_name: "some.user@domain.tld",
20
+ },
21
+ ts: "2021-09-16T07:36:32.848603Z",
22
+ };
23
+ const renderOpts = {
24
+ messages: {
25
+ en: { "rules.events.action_changed_to": "Field {0} changed to: {1}" },
26
+ },
27
+ };
28
+
29
+ it("matches the latest snapshot", () => {
30
+ const { container } = render(<RuleEventRow {...props} />, renderOpts);
31
+ expect(container).toMatchSnapshot();
32
+ });
33
+ });
@@ -0,0 +1,64 @@
1
+ import React from "react";
2
+ import { render } from "@truedat/test/render";
3
+ import { RuleEvents } from "../RuleEvents";
4
+ import { getParsedEvents } from "../../selectors/getParsedEvents";
5
+
6
+ describe("<RuleEvents />", () => {
7
+ const events = [
8
+ {
9
+ id: 100217,
10
+ service: "td_dd",
11
+ resource_id: 777,
12
+ resource_type: "rule",
13
+ event: "rule_updated",
14
+ payload: {
15
+ domain_id: 140,
16
+ },
17
+ user_id: 142,
18
+ user_name: null,
19
+ user: {
20
+ email: "some.user@domain.tld",
21
+ full_name: "Some User",
22
+ id: 142,
23
+ user_name: "some.user@domain.tld"
24
+ },
25
+ ts: "2021-09-16T07:36:32.848603Z"
26
+ },
27
+ {
28
+ id: 100436,
29
+ service: "td_dd",
30
+ resource_id: 777,
31
+ resource_type: "rule",
32
+ event: "rule_updated",
33
+ payload: {
34
+ domain_id: 168
35
+ },
36
+ user_id: 171,
37
+ user_name: null,
38
+ user: {
39
+ email: "some.user@domain.tld",
40
+ full_name: "Some User",
41
+ id: 171,
42
+ user_name: "some.user"
43
+ },
44
+ ts: "2021-09-16T09:31:03.061864Z"
45
+ },
46
+ ];
47
+ const renderOpts = {
48
+ messages: {
49
+ en: {
50
+ "rules.events.action_created": "Rule created",
51
+ "rules.events.action_changed": "Field {0} changed: {1}",
52
+ "rules.events.action_changed_to": "Field {0} changed to: {1}",
53
+ "rules.events.action_updated": "Rule updated",
54
+ },
55
+ },
56
+ };
57
+
58
+ const props = { events: getParsedEvents({ events }), eventsLoading: false };
59
+
60
+ it("matches the latest snapshot", () => {
61
+ const { container } = render(<RuleEvents {...props} />, renderOpts);
62
+ expect(container).toMatchSnapshot();
63
+ });
64
+ });
@@ -0,0 +1,38 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<RuleEventsRow /> matches the latest snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ class="event"
7
+ >
8
+ <div
9
+ class="content"
10
+ >
11
+ <div
12
+ class="summary"
13
+ >
14
+ <a
15
+ class="user"
16
+ >
17
+ some.user@domain.tld
18
+ </a>
19
+
20
+ <div
21
+ class="date"
22
+ >
23
+ <time
24
+ datetime="1631777792848"
25
+ >
26
+ 2021-09-16 07:36
27
+ </time>
28
+ </div>
29
+ </div>
30
+ <div
31
+ class="text extra"
32
+ >
33
+ Field field1 changed to: "new value"
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ `;
@@ -0,0 +1,78 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<RuleEvents /> matches the latest snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ class="ui bottom attached segment"
7
+ >
8
+ <div
9
+ class="ui small feed"
10
+ >
11
+ <div
12
+ class="event"
13
+ >
14
+ <div
15
+ class="content"
16
+ >
17
+ <div
18
+ class="summary"
19
+ >
20
+ <a
21
+ class="user"
22
+ >
23
+ some.user@domain.tld
24
+ </a>
25
+
26
+ <div
27
+ class="date"
28
+ >
29
+ <time
30
+ datetime="1631777792848"
31
+ >
32
+ 2021-09-16 07:36
33
+ </time>
34
+ </div>
35
+ </div>
36
+ <div
37
+ class="text extra"
38
+ >
39
+ Field domain_id changed to: 140
40
+ </div>
41
+ </div>
42
+ </div>
43
+ <div
44
+ class="event"
45
+ >
46
+ <div
47
+ class="content"
48
+ >
49
+ <div
50
+ class="summary"
51
+ >
52
+ <a
53
+ class="user"
54
+ >
55
+ some.user
56
+ </a>
57
+
58
+ <div
59
+ class="date"
60
+ >
61
+ <time
62
+ datetime="1631784663061"
63
+ >
64
+ 2021-09-16 09:31
65
+ </time>
66
+ </div>
67
+ </div>
68
+ <div
69
+ class="text extra"
70
+ >
71
+ Field domain_id changed to: 168
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ `;
@@ -132,6 +132,7 @@ export default {
132
132
  "All rule implementations should be removed",
133
133
  "rule.error.rule_name_bc_id.unique_constraint":
134
134
  "Duplicated rule name. Edit name or associated business concept.",
135
+
135
136
  "rule.form.accordion.select": "Select",
136
137
  "rule.form.concept.label": "Concept",
137
138
  "ruleImplementation.form.tooltip.deviation.goal":
@@ -541,6 +542,10 @@ export default {
541
542
  "rules.actions.upload.confirmation.header": "Confirm bulk upload",
542
543
  "rules.actions.upload.tooltip": "Upload Rules",
543
544
  "rules.crumbs.top": "Rules",
545
+ "rules.events.action_created": "Rule created",
546
+ "rules.events.action_changed": "Field {0} changed: {1}",
547
+ "rules.events.action_changed_to": "Field {0} changed to: {1}",
548
+ "rules.events.action_updated": "Rule updated",
544
549
  "rules.retrieved.results": "{count} rules found",
545
550
  "rules.search.placeholder": "Search rules...",
546
551
  "rules.searching": "Searching...",
@@ -557,6 +562,7 @@ export default {
557
562
  "structureFields.dropdown.placeholder": "Fields",
558
563
  "summary.link.and": "and",
559
564
  "tabs.dq.rule": "Rule",
565
+ "tabs.dq.rule.audit": "Audit",
560
566
  "tabs.dq.ruleImplementation": "Implementation",
561
567
  "tabs.dq.ruleImplementation.audit": "Audit",
562
568
  "tabs.dq.ruleImplementation.details": "Details",
@@ -556,6 +556,14 @@ export default {
556
556
  "rules.actions.upload.confirmation.header": "Subir ficheros de reglas",
557
557
  "rules.actions.upload.tooltip": "Subir reglas",
558
558
  "rules.crumbs.top": "Reglas",
559
+ "rules.events.action_created": "Regla creada",
560
+ "rules.events.action_changed": "Campo {0} actualizado: {1}",
561
+ "rules.events.action_changed_to": "Campo {0} actualizado a {1}",
562
+ "rules.events.action_updated": "Regla actualizada",
563
+ "ruleImplementations.events.action_created": "Implementación creada en regla: {0}",
564
+ "ruleImplementations.events.action_changed": "Campo {0} actualizado a {1}",
565
+ "ruleImplementations.events.action_deprecated": "Implementación archivada",
566
+ "ruleImplementations.events.action_moved": "Implementación movida a la regla: {0}",
559
567
  "rules.retrieved.results": "{count} reglas encontradas",
560
568
  "rules.search.placeholder": "Buscar reglas...",
561
569
  "rules.searching": "Buscando...",
@@ -573,6 +581,7 @@ export default {
573
581
  "structureFields.dropdown.placeholder": "Campos",
574
582
  "summary.link.and": "y",
575
583
  "tabs.dq.rule": "Regla",
584
+ "tabs.dq.rule.audit": "Auditoría",
576
585
  "tabs.dq.ruleImplementation.audit": "Auditoría",
577
586
  "tabs.dq.ruleImplementation.details": "Detalles",
578
587
  "tabs.dq.ruleImplementation.results": "Resultados",
@@ -0,0 +1,173 @@
1
+ import { getParsedEvents } from "..";
2
+
3
+ describe("selectors: getParsedEvents", () => {
4
+ const events = [
5
+ {
6
+ id: 52351,
7
+ service: "td_dd",
8
+ resource_id: 777,
9
+ resource_type: "rule",
10
+ event: "rule_created",
11
+ payload: {
12
+ active: true,
13
+ content: {
14
+ added: {
15
+ data_owner: null,
16
+ proyecto: ["P1"],
17
+ },
18
+ },
19
+ description: {},
20
+ df_name: "dq_default_template",
21
+ domain_id: 2,
22
+ goal: 20,
23
+ minimum: 80,
24
+ name: "des01",
25
+ result_type: "deviation",
26
+ updated_by: 398,
27
+ },
28
+ user_id: 398,
29
+ user_name: null,
30
+ user: {
31
+ email: "some.user@domain.tld",
32
+ full_name: "Some User",
33
+ id: 398,
34
+ user_name: "some.user@domain.tld",
35
+ },
36
+ ts: "2021-08-12T12:11:26.908147Z",
37
+ },
38
+
39
+ {
40
+ id: 100217,
41
+ service: "td_dd",
42
+ resource_id: 777,
43
+ resource_type: "rule",
44
+ event: "rule_updated",
45
+ payload: {
46
+ domain_id: 140,
47
+ },
48
+ user_id: 142,
49
+ user_name: null,
50
+ user: {
51
+ email: "some.user@domain.tld",
52
+ full_name: "Some User",
53
+ id: 142,
54
+ user_name: "some.user@domain.tld",
55
+ },
56
+ ts: "2021-09-16T07:36:32.848603Z",
57
+ },
58
+ {
59
+ id: 100435,
60
+ service: "td_dd",
61
+ resource_id: 777,
62
+ resource_type: "rule",
63
+ event: "rule_updated",
64
+ payload: {
65
+ description: {
66
+ document: {
67
+ data: {},
68
+ nodes: [
69
+ {
70
+ data: {},
71
+ nodes: [
72
+ {
73
+ marks: [],
74
+ object: "text",
75
+ text: "Añadimos descripción a la regla",
76
+ },
77
+ ],
78
+ object: "block",
79
+ type: "paragraph",
80
+ },
81
+ ],
82
+ object: "document",
83
+ },
84
+ object: "value",
85
+ },
86
+ updated_by: 171,
87
+ },
88
+ user_id: 171,
89
+ user_name: null,
90
+ user: {
91
+ email: "some.user@domain.tld",
92
+ full_name: "Some User",
93
+ id: 171,
94
+ user_name: "some.user",
95
+ },
96
+ ts: "2021-09-16T09:30:52.317357Z",
97
+ },
98
+ {
99
+ id: 177692,
100
+ service: "td_dd",
101
+ resource_id: 777,
102
+ resource_type: "rule",
103
+ event: "rule_updated",
104
+ payload: {
105
+ content: {
106
+ changed: {
107
+ data_stew: null,
108
+ principle: "Validez",
109
+ scheduler: "",
110
+ tags: "et",
111
+ },
112
+ removed: {
113
+ data_owner: null,
114
+ proyecto: ["P1"],
115
+ },
116
+ },
117
+ df_name: "quality_template",
118
+ updated_by: 426,
119
+ },
120
+ user_id: 426,
121
+ user_name: null,
122
+ user: {
123
+ email: "some.user@domain.tld",
124
+ full_name: "Some User",
125
+ id: 426,
126
+ user_name: "some.user",
127
+ },
128
+ ts: "2021-10-20T07:56:45.891087Z",
129
+ },
130
+ ];
131
+
132
+ const parsed = [
133
+ {
134
+ ...events[0],
135
+ payload: [{ field: "des01", action: "created" }],
136
+ },
137
+ {
138
+ ...events[1],
139
+ payload: [{ field: "domain_id", action: "changed", value: 140 }],
140
+ },
141
+ {
142
+ ...events[2],
143
+ payload: [
144
+ {
145
+ field: "description",
146
+ action: "changed",
147
+ value: "Añadimos descripción a la regla",
148
+ },
149
+ ],
150
+ },
151
+ {
152
+ ...events[3],
153
+ payload: [
154
+ { field: "df_name", action: "changed", value: "quality_template" },
155
+ { action: "changed", field: "data_stew", value: null },
156
+ { action: "changed", field: "principle", value: "Validez" },
157
+ { action: "changed", field: "scheduler", value: "" },
158
+ { action: "changed", field: "tags", value: "et" },
159
+ ],
160
+ },
161
+ ];
162
+
163
+ it("should return all the events with the new payload", () => {
164
+ const res = getParsedEvents({ events });
165
+ expect(res).toEqual(parsed);
166
+ });
167
+
168
+ it("should return empty array when we have no elements", () => {
169
+ const res = getParsedEvents({ events: [] });
170
+ expect(res).toHaveLength(0);
171
+ expect(res).toEqual(expect.arrayContaining([]));
172
+ });
173
+ });
@@ -8,6 +8,24 @@ const getEvents = ({ events }) => {
8
8
  const transformFieldFormat = (field) =>
9
9
  _.isBoolean(field) ? _.toString(field) : field;
10
10
 
11
+ const findDeep = (obj, key) =>
12
+ _.has(key, obj)
13
+ ? obj[key]
14
+ : _.flatten(
15
+ _.map((v) => (typeof v == "object" ? findDeep(v, key) : []), obj)
16
+ );
17
+
18
+ const transformRulePayloadFormat = (payload, field) => {
19
+ switch (field) {
20
+ case "content":
21
+ return payload.content;
22
+ case "description":
23
+ return _.first(findDeep(payload.description, "text"));
24
+ default:
25
+ return payload[field];
26
+ }
27
+ };
28
+
11
29
  const getParsedEvents = createSelector([getEvents], (events) => {
12
30
  return _.map((d) => {
13
31
  switch (d.event) {
@@ -68,6 +86,43 @@ const getParsedEvents = createSelector([getEvents], (events) => {
68
86
  }))
69
87
  )(d),
70
88
  };
89
+ case "rule_created":
90
+ return {
91
+ ...d,
92
+ payload: [
93
+ {
94
+ field: _.path("payload.name")(d),
95
+ action: "created",
96
+ },
97
+ ],
98
+ };
99
+
100
+ case "rule_updated":
101
+ const content = _.flow(
102
+ _.getOr({}, "payload.content.changed"),
103
+ _.toPairs,
104
+ _.map(([field, value]) => ({
105
+ field,
106
+ value,
107
+ action: "changed",
108
+ }))
109
+ )(d);
110
+ return {
111
+ ...d,
112
+ payload: [
113
+ ..._.flow(
114
+ _.path("payload"),
115
+ _.omit(["updated_by", "content"]),
116
+ _.keys,
117
+ _.map((field) => ({
118
+ field: field,
119
+ action: "changed",
120
+ value: transformRulePayloadFormat(d.payload, field),
121
+ }))
122
+ )(d),
123
+ ...content,
124
+ ],
125
+ };
71
126
 
72
127
  default:
73
128
  return {