@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/CHANGELOG.md +7 -0
- package/LICENSE +685 -0
- package/README.md +13 -0
- package/package.json +115 -0
- package/src/components/ProfilingElements.js +89 -0
- package/src/components/ProfilingGraph.js +89 -0
- package/src/components/ProfilingPatterns.js +57 -0
- package/src/components/StructureProfiling.js +124 -0
- package/src/components/__tests__/StructureProfiling.spec.js +33 -0
- package/src/components/__tests__/__snapshots__/StructureProfiling.spec.js.snap +444 -0
- package/src/index.js +3 -0
package/README.md
ADDED
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
|
+
});
|