@truedat/profile 4.45.0

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/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # Truedat Web Profile
2
+
3
+ Data structure profile component
4
+
5
+ ## How To...
6
+
7
+ ### Install depencencies
8
+
9
+ `yarn`
10
+
11
+ ### Run the tests
12
+
13
+ `yarn test`
package/package.json ADDED
@@ -0,0 +1,115 @@
1
+ {
2
+ "name": "@truedat/profile",
3
+ "version": "4.45.0",
4
+ "description": "Truedat Web Custom",
5
+ "sideEffects": false,
6
+ "jsnext:main": "src/index.js",
7
+ "module": "src/index.js",
8
+ "files": [
9
+ "src"
10
+ ],
11
+ "author": "Bluetab Solutions",
12
+ "license": "GPL-3.0",
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "scripts": {
17
+ "clean": "rimraf yarn-error.log",
18
+ "debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
19
+ "test": "TZ=UTC jest --coverage",
20
+ "test:watch": "TZ=UTC jest --watch",
21
+ "eslint": "eslint src/**",
22
+ "eslint:fix": "eslint --fix src/**"
23
+ },
24
+ "devDependencies": {
25
+ "@babel/cli": "^7.17.10",
26
+ "@babel/core": "^7.18.0",
27
+ "@babel/plugin-proposal-class-properties": "^7.17.12",
28
+ "@babel/plugin-proposal-object-rest-spread": "^7.18.0",
29
+ "@babel/plugin-proposal-optional-chaining": "^7.17.12",
30
+ "@babel/plugin-syntax-dynamic-import": "^7.8.3",
31
+ "@babel/plugin-transform-modules-commonjs": "^7.18.0",
32
+ "@babel/preset-env": "^7.18.0",
33
+ "@babel/preset-react": "^7.17.12",
34
+ "@testing-library/jest-dom": "^5.16.4",
35
+ "@testing-library/react": "^12.0.0",
36
+ "@testing-library/user-event": "^13.2.1",
37
+ "@truedat/test": "4.45.0",
38
+ "babel-jest": "^28.1.0",
39
+ "babel-plugin-dynamic-import-node": "^2.3.3",
40
+ "babel-plugin-lodash": "^3.3.4",
41
+ "babel-plugin-react-intl": "^5.1.18",
42
+ "babel-plugin-transform-semantic-ui-react-imports": "^1.4.1",
43
+ "enzyme": "^3.11.0",
44
+ "enzyme-adapter-react-16": "^1.15.6",
45
+ "enzyme-to-json": "^3.6.2",
46
+ "identity-obj-proxy": "^3.0.0",
47
+ "jest": "^28.1.0",
48
+ "jest-environment-jsdom": "^28.1.0",
49
+ "react": "^16.14.0",
50
+ "react-dom": "^16.14.0",
51
+ "redux-saga-test-plan": "^4.0.4",
52
+ "rimraf": "^3.0.2",
53
+ "semantic-ui-react": "^2.0.3"
54
+ },
55
+ "jest": {
56
+ "maxWorkers": "50%",
57
+ "testTimeout": 10000,
58
+ "moduleDirectories": [
59
+ "<rootDir>/src",
60
+ "../../node_modules"
61
+ ],
62
+ "setupFilesAfterEnv": [
63
+ "@truedat/test/setup"
64
+ ],
65
+ "moduleNameMapper": {
66
+ "\\.(css|less|png)$": "identity-obj-proxy",
67
+ "^@truedat/([^/]+)$": "<rootDir>/../$1/src/index",
68
+ "^@truedat/([^/]+)/(.*)$": "<rootDir>/../$1/src/$2"
69
+ },
70
+ "snapshotSerializers": [
71
+ "enzyme-to-json/serializer"
72
+ ],
73
+ "testEnvironment": "jsdom",
74
+ "testPathIgnorePatterns": [
75
+ "<rootDir>/node_modules/"
76
+ ],
77
+ "transform": {
78
+ "\\.js$": [
79
+ "babel-jest",
80
+ {
81
+ "rootMode": "upward"
82
+ }
83
+ ]
84
+ },
85
+ "transformIgnorePatterns": [
86
+ "/node_modules/(?!@truedat).*"
87
+ ]
88
+ },
89
+ "dependencies": {
90
+ "@apollo/client": "^3.6.4",
91
+ "@truedat/auth": "4.45.0",
92
+ "@truedat/core": "4.45.0",
93
+ "@truedat/df": "4.45.0",
94
+ "lodash": "^4.17.21",
95
+ "path-to-regexp": "^1.7.0",
96
+ "prop-types": "^15.8.1",
97
+ "react-hook-form": "^7.30.0",
98
+ "react-intl": "^5.20.10",
99
+ "react-moment": "^1.1.2",
100
+ "react-redux": "^7.2.4",
101
+ "react-router-dom": "^5.2.0",
102
+ "react-vis": "^1.11.7",
103
+ "redux": "^4.1.1",
104
+ "redux-saga": "^1.1.3",
105
+ "redux-saga-routines": "^3.2.3",
106
+ "reselect": "^4.0.0",
107
+ "svg-pan-zoom": "^3.6.1"
108
+ },
109
+ "peerDependencies": {
110
+ "react": ">= 16.8.6 < 17",
111
+ "react-dom": ">= 16.8.6 < 17",
112
+ "semantic-ui-react": ">= 0.88.2 < 2.1"
113
+ },
114
+ "gitHead": "b8a49c6bae0bbde203dfb4763a5657fc398e3b7a"
115
+ }
@@ -0,0 +1,89 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useState } from "react";
3
+ import PropTypes from "prop-types";
4
+ import { connect } from "react-redux";
5
+ import { Button, Icon, Grid } from "semantic-ui-react";
6
+ import { FormattedMessage } from "react-intl";
7
+ import ProfilingGraph from "./ProfilingGraph";
8
+ import ProfilingPatterns from "./ProfilingPatterns";
9
+
10
+ export const ProfilingElements = ({ profile }) => {
11
+ const { most_frequent, patterns } = profile || {};
12
+ const [elementActive, setElementActive] = useState("graph");
13
+ const titleElement =
14
+ elementActive === "graph"
15
+ ? "structure.field.graph.title"
16
+ : "structure.field.patterns.title";
17
+ return (
18
+ <>
19
+ {!_.isEmpty(most_frequent) && !_.isEmpty(patterns) && (
20
+ <>
21
+ <Grid>
22
+ <Grid.Column width={8}>
23
+ <p>
24
+ <FormattedMessage id={titleElement} />:
25
+ </p>
26
+ </Grid.Column>
27
+ <Grid.Column width={8} textAlign="right">
28
+ <Button.Group floated="right">
29
+ <Button
30
+ active={elementActive === "graph"}
31
+ icon
32
+ onClick={() => setElementActive("graph")}
33
+ >
34
+ <Icon name="chart line" />
35
+ </Button>
36
+ <Button
37
+ active={elementActive === "patterns"}
38
+ icon
39
+ onClick={() => setElementActive("patterns")}
40
+ >
41
+ <Icon name="list ul" />
42
+ </Button>
43
+ </Button.Group>
44
+ </Grid.Column>
45
+ </Grid>
46
+
47
+ {elementActive === "graph" && <ProfilingGraph />}
48
+ {elementActive === "patterns" && <ProfilingPatterns />}
49
+ </>
50
+ )}
51
+
52
+ {!_.isEmpty(most_frequent) && _.isEmpty(patterns) && (
53
+ <>
54
+ <Grid>
55
+ <Grid.Column width={16}>
56
+ <p>
57
+ <FormattedMessage id="structure.field.graph.title" />:
58
+ </p>
59
+ </Grid.Column>
60
+ </Grid>
61
+
62
+ <ProfilingGraph />
63
+ </>
64
+ )}
65
+
66
+ {_.isEmpty(most_frequent) && !_.isEmpty(patterns) && (
67
+ <>
68
+ <Grid>
69
+ <Grid.Column width={16}>
70
+ <p>
71
+ <FormattedMessage id="structure.field.patterns.title" />
72
+ </p>
73
+ </Grid.Column>
74
+ </Grid>
75
+
76
+ <ProfilingPatterns />
77
+ </>
78
+ )}
79
+ </>
80
+ );
81
+ };
82
+
83
+ ProfilingElements.propTypes = {
84
+ profile: PropTypes.object,
85
+ };
86
+
87
+ const mapStateToProps = ({ structureProfile: profile }) => ({ profile });
88
+
89
+ export default connect(mapStateToProps)(ProfilingElements);
@@ -0,0 +1,89 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useState } from "react";
3
+ import PropTypes from "prop-types";
4
+ import { connect } from "react-redux";
5
+ import { Label } from "semantic-ui-react";
6
+ import { FormattedMessage } from "react-intl";
7
+ import { XYPlot, XAxis, YAxis, HorizontalBarSeries, Hint } from "react-vis";
8
+
9
+ const getDistribution = _.flow(
10
+ _.pathOr("[]", "most_frequent"),
11
+ _.map(({ k, v }) => ({ x: v, y: k })),
12
+ _.orderBy("x", "desc"),
13
+ _.take(30),
14
+ _.reverse
15
+ );
16
+
17
+ const getMinMax = _.flow(
18
+ _.map("x"),
19
+ (d) => [_.min(d) - 1, _.max(d)],
20
+ _.defaultTo([-Infinity, Infinity])
21
+ );
22
+
23
+ const formatNumber = (num) =>
24
+ num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,");
25
+
26
+ export const ProfilingGraph = ({ count, distribution }) => {
27
+ const [hintValue, setHintValue] = useState(null);
28
+ const data = _.map((d) => ({ color: d.y == hintValue?.y ? 0 : 1, ...d }))(
29
+ distribution
30
+ );
31
+ return (
32
+ <XYPlot
33
+ yType="ordinal"
34
+ xType="linear"
35
+ width={800}
36
+ height={400}
37
+ margin={{ left: 180 }}
38
+ xDomain={getMinMax(data)}
39
+ colorRange={["#ed5c17", "#002057"]}
40
+ >
41
+ <YAxis
42
+ tickFormat={_.truncate({ length: 30 })}
43
+ style={{ fontSize: "12px", width: "100px" }}
44
+ />
45
+ <XAxis style={{ fontSize: "12px" }} />
46
+ <HorizontalBarSeries
47
+ onValueMouseOver={setHintValue}
48
+ onValueMouseOut={() => setHintValue(null)}
49
+ data={data}
50
+ />
51
+ {hintValue ? (
52
+ <Hint
53
+ style={{ width: "500px", bottom: "190px" }}
54
+ align={{ horizontal: "right", vertical: "top" }}
55
+ value={hintValue}
56
+ >
57
+ <Label pointing="left" className="graph">
58
+ <div>
59
+ <div>
60
+ <FormattedMessage id="structure.field.graph.value" />
61
+ {hintValue.y}
62
+ </div>
63
+ <div>
64
+ <FormattedMessage id="structure.field.graph.count" />
65
+ {formatNumber(hintValue.x)}
66
+ </div>
67
+ <div>
68
+ <FormattedMessage id="structure.field.graph.percentage" />
69
+ {Math.floor((hintValue.x / count) * 100) + "%"}
70
+ </div>
71
+ </div>
72
+ </Label>
73
+ </Hint>
74
+ ) : null}
75
+ </XYPlot>
76
+ );
77
+ };
78
+
79
+ ProfilingGraph.propTypes = {
80
+ count: PropTypes.number,
81
+ distribution: PropTypes.arrayOf(PropTypes.object),
82
+ };
83
+
84
+ const mapStateToProps = ({ structureProfile }) => ({
85
+ count: structureProfile?.total_count,
86
+ distribution: getDistribution(structureProfile),
87
+ });
88
+
89
+ export default connect(mapStateToProps)(ProfilingGraph);
@@ -0,0 +1,57 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import PropTypes from "prop-types";
4
+ import { connect } from "react-redux";
5
+ import { Table } from "semantic-ui-react";
6
+ import { useIntl } from "react-intl";
7
+
8
+ export const ProfilingPatterns = ({ patterns, totalCount }) => {
9
+ const { formatNumber, formatMessage } = useIntl();
10
+ const dataPatterns = _.flow(
11
+ _.orderBy("v", "desc"),
12
+ _.map(({ k, v }) => ({
13
+ frecuency: k,
14
+ count: formatNumber(v),
15
+ percentage: parseFloat(((v / totalCount) * 100).toFixed(2)),
16
+ }))
17
+ )(patterns);
18
+
19
+ return !_.isEmpty(dataPatterns) ? (
20
+ <Table>
21
+ <Table.Header>
22
+ <Table.Row>
23
+ <Table.HeaderCell>
24
+ {formatMessage({ id: "structure.field.patterns.frecuency" })}
25
+ </Table.HeaderCell>
26
+ <Table.HeaderCell>
27
+ {formatMessage({ id: "structure.field.patterns.count" })}
28
+ </Table.HeaderCell>
29
+ <Table.HeaderCell>
30
+ {formatMessage({ id: "structure.field.patterns.percentage" })}
31
+ </Table.HeaderCell>
32
+ </Table.Row>
33
+ </Table.Header>
34
+ <Table.Body>
35
+ {dataPatterns.map((pattern, i) => (
36
+ <Table.Row key={i}>
37
+ <Table.Cell>{pattern.frecuency}</Table.Cell>
38
+ <Table.Cell>{pattern.count}</Table.Cell>
39
+ <Table.Cell>{`${pattern.percentage} %`}</Table.Cell>
40
+ </Table.Row>
41
+ ))}
42
+ </Table.Body>
43
+ </Table>
44
+ ) : null;
45
+ };
46
+
47
+ ProfilingPatterns.propTypes = {
48
+ patterns: PropTypes.array,
49
+ totalCount: PropTypes.number,
50
+ };
51
+
52
+ const mapStateToProps = ({ structureProfile }) => ({
53
+ patterns: structureProfile?.patterns,
54
+ totalCount: structureProfile?.total_count,
55
+ });
56
+
57
+ export default connect(mapStateToProps)(ProfilingPatterns);
@@ -0,0 +1,124 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import PropTypes from "prop-types";
4
+ import { connect } from "react-redux";
5
+ import { Table } from "semantic-ui-react";
6
+ import { useIntl } from "react-intl";
7
+ import { Link } from "react-router-dom";
8
+ import { linkTo } from "@truedat/core/routes";
9
+ import { getSortedFields } from "@truedat/dd/selectors";
10
+ import ProfilingElements from "./ProfilingElements";
11
+
12
+ const calculatePercent = (value_count, total_count) =>
13
+ ((parseFloat(value_count) / parseFloat(total_count)) * 100).toFixed(2) + "%";
14
+
15
+ const calculateUniquePercent = ({ unique_count, total_count }) =>
16
+ calculatePercent(unique_count, total_count);
17
+ const calculateNullPercent = ({ null_count, total_count }) =>
18
+ calculatePercent(null_count, total_count);
19
+
20
+ const calculateMode = (distribution) =>
21
+ _.isEmpty(distribution)
22
+ ? "-"
23
+ : _.flow(_.orderBy("v", "desc"), _.head, _.prop("k"))(distribution);
24
+
25
+ export const FieldProfileRow = ({ profile, data_structure_id, name }) => {
26
+ const { unique_count, null_count, total_count, min, max, most_frequent } =
27
+ profile;
28
+ const uniquePercent =
29
+ !_.isNil(unique_count) && !_.isNil(total_count) && total_count > 0
30
+ ? calculateUniquePercent(profile)
31
+ : "-";
32
+ const nullPercent =
33
+ !_.isNil(null_count) && !_.isNil(total_count)
34
+ ? calculateNullPercent(profile)
35
+ : "-";
36
+
37
+ const maxLength = 50;
38
+ const truncFn = _.truncate({ length: maxLength });
39
+ const minValue = _.isNil(min) ? "-" : truncFn(min);
40
+ const maxValue = _.isNil(max) ? "-" : truncFn(max);
41
+ const minTitle = !_.isNil(min) && min.length > maxLength ? min : "";
42
+ const maxTitle = !_.isNil(max) && max.length > maxLength ? max : "";
43
+ const modeValue = _.isNil(most_frequent) ? "-" : calculateMode(most_frequent);
44
+
45
+ return (
46
+ <Table.Row>
47
+ <Table.Cell>
48
+ <Link
49
+ to={
50
+ _.isNil(most_frequent)
51
+ ? linkTo.STRUCTURE({ id: data_structure_id })
52
+ : linkTo.STRUCTURE_PROFILE({ id: data_structure_id })
53
+ }
54
+ >
55
+ {name}
56
+ </Link>
57
+ </Table.Cell>
58
+ <Table.Cell className="text-right" content={uniquePercent} />
59
+ <Table.Cell className="text-right" content={nullPercent} />
60
+ <Table.Cell>
61
+ <span title={minTitle}> {minValue} </span>
62
+ </Table.Cell>
63
+ <Table.Cell>
64
+ <span title={maxTitle}> {maxValue} </span>
65
+ </Table.Cell>
66
+ <Table.Cell content={modeValue} />
67
+ </Table.Row>
68
+ );
69
+ };
70
+
71
+ FieldProfileRow.propTypes = {
72
+ data_structure_id: PropTypes.number,
73
+ name: PropTypes.string,
74
+ profile: PropTypes.object,
75
+ };
76
+
77
+ export const StructureProfiling = ({ fields }) => {
78
+ const { formatMessage } = useIntl();
79
+ return _.isEmpty(fields) ? (
80
+ <ProfilingElements />
81
+ ) : (
82
+ <Table>
83
+ <Table.Header>
84
+ <Table.Row>
85
+ <Table.HeaderCell
86
+ content={formatMessage({ id: "profiling.field.name" })}
87
+ />
88
+ <Table.HeaderCell
89
+ className="text-right"
90
+ content={formatMessage({ id: "profiling.field.unique" })}
91
+ />
92
+ <Table.HeaderCell
93
+ className="text-right"
94
+ content={formatMessage({ id: "profiling.field.null" })}
95
+ />
96
+ <Table.HeaderCell
97
+ content={formatMessage({ id: "profiling.field.min" })}
98
+ />
99
+ <Table.HeaderCell
100
+ content={formatMessage({ id: "profiling.field.max" })}
101
+ />
102
+ <Table.HeaderCell
103
+ content={formatMessage({ id: "profiling.field.mode" })}
104
+ />
105
+ </Table.Row>
106
+ </Table.Header>
107
+ <Table.Body>
108
+ {fields.map((field, i) => (
109
+ <FieldProfileRow key={i} {...field} />
110
+ ))}
111
+ </Table.Body>
112
+ </Table>
113
+ );
114
+ };
115
+
116
+ StructureProfiling.propTypes = {
117
+ fields: PropTypes.array,
118
+ };
119
+
120
+ const mapStateToProps = (state) => ({
121
+ fields: _.filter(_.has("profile"))(getSortedFields(state)),
122
+ });
123
+
124
+ export default connect(mapStateToProps)(StructureProfiling);
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import { render } from "@truedat/test/render";
3
+ import StructureProfiling from "../StructureProfiling";
4
+
5
+ const profile = {
6
+ unique_count: 5,
7
+ null_count: 2,
8
+ total_count: 10,
9
+ min: "33",
10
+ max: "40",
11
+ most_frequent: [
12
+ { k: "foo", v: 10 },
13
+ { k: "bar", v: 42 },
14
+ ],
15
+ };
16
+
17
+ describe("<StructureProfiling />", () => {
18
+ it("matches the latest snapshot (fields)", () => {
19
+ const structureFields = [
20
+ { data_structure_id: 123, name: "field1", profile },
21
+ { data_structure_id: 122, name: "field2", profile },
22
+ ];
23
+ const state = { structureFields };
24
+ const { container } = render(<StructureProfiling />, { state });
25
+ expect(container).toMatchSnapshot();
26
+ });
27
+
28
+ it("matches the latest snapshot (profile)", () => {
29
+ const state = { structureProfile: profile };
30
+ const { container } = render(<StructureProfiling />, { state });
31
+ expect(container).toMatchSnapshot();
32
+ });
33
+ });