Skip to content

Commit ead9ec4

Browse files
feat: Add descriptive statistics for number cells in data browser (#2529)
1 parent 09f48fc commit ead9ec4

File tree

8 files changed

+315
-4
lines changed

8 files changed

+315
-4
lines changed

src/components/BrowserCell/BrowserCell.react.js

+19-2
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,6 @@ export default class BrowserCell extends Component {
526526
hidden,
527527
width,
528528
current,
529-
onSelect,
530529
onEditChange,
531530
setCopyableValue,
532531
onPointerCmdClick,
@@ -536,6 +535,8 @@ export default class BrowserCell extends Component {
536535
onEditSelectedRow,
537536
isRequired,
538537
markRequiredFieldRow,
538+
handleCellClick,
539+
selectedCells,
539540
} = this.props;
540541

541542
const classes = [...this.state.classes];
@@ -573,6 +574,22 @@ export default class BrowserCell extends Component {
573574
);
574575
}
575576

577+
if (selectedCells?.list.has(`${row}-${col}`)) {
578+
if (selectedCells.rowStart === row) {
579+
classes.push(styles.topBorder);
580+
}
581+
if (selectedCells.rowEnd === row) {
582+
classes.push(styles.bottomBorder);
583+
}
584+
if (selectedCells.colStart === col) {
585+
classes.push(styles.leftBorder);
586+
}
587+
if (selectedCells.colEnd === col) {
588+
classes.push(styles.rightBorder);
589+
}
590+
classes.push(styles.selected);
591+
}
592+
576593
return (
577594
<span
578595
ref={this.cellRef}
@@ -582,8 +599,8 @@ export default class BrowserCell extends Component {
582599
if (e.metaKey === true && type === 'Pointer') {
583600
onPointerCmdClick(value);
584601
} else {
585-
onSelect({ row, col });
586602
setCopyableValue(hidden ? undefined : this.copyableValue);
603+
handleCellClick(e, row, col);
587604
}
588605
}}
589606
onDoubleClick={() => {

src/components/BrowserCell/BrowserCell.scss

+65-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,71 @@
3636
}
3737
}
3838

39-
.hasMore{
39+
.leftBorder {
40+
position: relative;
41+
42+
&:after {
43+
position: absolute;
44+
pointer-events: none;
45+
content: '';
46+
border-left: 2px solid #555572;
47+
top: 0;
48+
left: 0;
49+
right: 0;
50+
bottom: 0;
51+
}
52+
}
53+
54+
.rightBorder {
55+
position: relative;
56+
57+
&:after {
58+
position: absolute;
59+
pointer-events: none;
60+
content: '';
61+
border-right: 2px solid #555572;
62+
top: 0;
63+
left: 0;
64+
right: 0;
65+
bottom: 0;
66+
}
67+
}
68+
69+
.topBorder {
70+
position: relative;
71+
72+
&:after {
73+
position: absolute;
74+
pointer-events: none;
75+
content: '';
76+
border-top: 2px solid #555572;
77+
top: 0;
78+
left: 0;
79+
right: 0;
80+
bottom: 0;
81+
}
82+
}
83+
84+
.bottomBorder {
85+
position: relative;
86+
87+
&:after {
88+
position: absolute;
89+
pointer-events: none;
90+
content: '';
91+
border-bottom: 2px solid #555572;
92+
top: 0;
93+
left: 0;
94+
right: 0;
95+
bottom: 0;
96+
}
97+
}
98+
99+
.selected {
100+
background-color: #e3effd;
101+
}
102+
103+
.hasMore {
40104
height: auto;
41105
max-height: 25px;
42106
overflow-y: scroll;

src/components/BrowserRow/BrowserRow.react.js

+2
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ export default class BrowserRow extends Component {
136136
showNote={this.props.showNote}
137137
onRefresh={this.props.onRefresh}
138138
scripts={this.props.scripts}
139+
handleCellClick={this.props.handleCellClick}
140+
selectedCells={this.props.selectedCells}
139141
/>
140142
);
141143
})}

src/components/Toolbar/Toolbar.react.js

+102-1
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,110 @@
66
* the root directory of this source tree.
77
*/
88
import PropTypes from 'lib/PropTypes';
9-
import React from 'react';
9+
import React, { useEffect } from 'react';
1010
import Icon from 'components/Icon/Icon.react';
1111
import styles from 'components/Toolbar/Toolbar.scss';
12+
import Popover from 'components/Popover/Popover.react';
13+
import Position from 'lib/Position';
1214
import { useNavigate, useNavigationType, NavigationType } from 'react-router-dom';
1315

16+
const POPOVER_CONTENT_ID = 'toolbarStatsPopover';
17+
18+
const Stats = ({ data }) => {
19+
const [selected, setSelected] = React.useState(null);
20+
const [open, setOpen] = React.useState(false);
21+
const buttonRef = React.useRef();
22+
23+
const statsOptions = [
24+
{
25+
type: 'sum',
26+
label: 'Sum',
27+
getValue: data => data.reduce((sum, value) => sum + value, 0),
28+
},
29+
{
30+
type: 'mean',
31+
label: 'Mean',
32+
getValue: data => data.reduce((sum, value) => sum + value, 0) / data.length,
33+
},
34+
{
35+
type: 'count',
36+
label: 'Count',
37+
getValue: data => data.length,
38+
},
39+
{
40+
type: 'p99',
41+
label: 'P99',
42+
getValue: data => {
43+
const sorted = data.sort((a, b) => a - b);
44+
return sorted[Math.floor(sorted.length * 0.99)];
45+
},
46+
},
47+
];
48+
49+
const toggle = () => {
50+
setOpen(!open);
51+
};
52+
53+
const renderPopover = () => {
54+
const node = buttonRef.current;
55+
const position = Position.inDocument(node);
56+
return (
57+
<Popover
58+
fixed={true}
59+
position={position}
60+
onExternalClick={toggle}
61+
contentId={POPOVER_CONTENT_ID}
62+
>
63+
<div id={POPOVER_CONTENT_ID}>
64+
<div
65+
onClick={toggle}
66+
style={{
67+
cursor: 'pointer',
68+
width: node.clientWidth,
69+
height: node.clientHeight,
70+
}}
71+
></div>
72+
<div className={styles.stats_popover_container}>
73+
{statsOptions.map(item => {
74+
const itemStyle = [styles.stats_popover_item];
75+
if (item.type === selected?.type) {
76+
itemStyle.push(styles.active);
77+
}
78+
return (
79+
<div
80+
key={item.type}
81+
className={itemStyle.join(' ')}
82+
onClick={() => {
83+
setSelected(item);
84+
toggle();
85+
}}
86+
>
87+
<span>{item.label}</span>
88+
</div>
89+
);
90+
})}
91+
</div>
92+
</div>
93+
</Popover>
94+
);
95+
};
96+
97+
useEffect(() => {
98+
setSelected(statsOptions[0]);
99+
}, []);
100+
101+
return (
102+
<>
103+
{selected ? (
104+
<button ref={buttonRef} className={styles.stats} onClick={toggle}>
105+
{`${selected.label}: ${selected.getValue(data)}`}
106+
</button>
107+
) : null}
108+
{open ? renderPopover() : null}
109+
</>
110+
);
111+
};
112+
14113
const Toolbar = props => {
15114
const action = useNavigationType();
16115
const navigate = useNavigate();
@@ -34,6 +133,7 @@ const Toolbar = props => {
34133
</div>
35134
</div>
36135
</div>
136+
{props.selectedData.length ? <Stats data={props.selectedData} /> : null}
37137
<div className={styles.actions}>{props.children}</div>
38138
</div>
39139
);
@@ -44,6 +144,7 @@ Toolbar.propTypes = {
44144
subsection: PropTypes.string,
45145
details: PropTypes.string,
46146
relation: PropTypes.object,
147+
selectedData: PropTypes.array,
47148
};
48149

49150
export default Toolbar;

src/components/Toolbar/Toolbar.scss

+40
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,43 @@ body:global(.expanded) {
8585
right: 14px;
8686
top: 4px;
8787
}
88+
89+
.stats {
90+
position: absolute;
91+
right: 20px;
92+
bottom: 10px;
93+
background: $blue;
94+
border-radius: 3px;
95+
padding: 2px 6px;
96+
font-size: 14px;
97+
color: white;
98+
box-shadow: none;
99+
border: none;
100+
}
101+
102+
.stats_popover_container {
103+
display: flex;
104+
flex-direction: column;
105+
background: $blue;
106+
border-radius: 3px;
107+
margin-top: 1px;
108+
gap: 2px;
109+
padding: 2px 0px;
110+
font-size: 14px;
111+
color: white;
112+
}
113+
114+
.stats_popover_item {
115+
cursor: pointer;
116+
padding: 0px 6px;
117+
118+
&:hover {
119+
color: $blue;
120+
background-color: white;
121+
}
122+
}
123+
124+
.active {
125+
color: $blue;
126+
background-color: white;
127+
}

src/dashboard/Data/Browser/BrowserTable.react.js

+6
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ export default class BrowserTable extends React.Component {
168168
showNote={this.props.showNote}
169169
onRefresh={this.props.onRefresh}
170170
scripts={this.context.scripts}
171+
selectedCells={this.props.selectedCells}
172+
handleCellClick={this.props.handleCellClick}
171173
/>
172174
<Button
173175
value="Clone"
@@ -236,6 +238,8 @@ export default class BrowserTable extends React.Component {
236238
showNote={this.props.showNote}
237239
onRefresh={this.props.onRefresh}
238240
scripts={this.context.scripts}
241+
selectedCells={this.props.selectedCells}
242+
handleCellClick={this.props.handleCellClick}
239243
/>
240244
<Button
241245
value="Add"
@@ -312,6 +316,8 @@ export default class BrowserTable extends React.Component {
312316
showNote={this.props.showNote}
313317
onRefresh={this.props.onRefresh}
314318
scripts={this.context.scripts}
319+
selectedCells={this.props.selectedCells}
320+
handleCellClick={this.props.handleCellClick}
315321
/>
316322
);
317323
}

src/dashboard/Data/Browser/BrowserToolbar.react.js

+3
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ const BrowserToolbar = ({
7272
login,
7373
logout,
7474
toggleMasterKeyUsage,
75+
76+
selectedData,
7577
}) => {
7678
const selectionLength = Object.keys(selection).length;
7779
const isPendingEditCloneRows = editCloneRows && editCloneRows.length > 0;
@@ -238,6 +240,7 @@ const BrowserToolbar = ({
238240
section={relation ? `Relation <${relation.targetClassName}>` : 'Class'}
239241
subsection={subsection}
240242
details={details.join(' \u2022 ')}
243+
selectedData={selectedData}
241244
>
242245
{onAddRow && (
243246
<a className={classes.join(' ')} onClick={onClick}>

0 commit comments

Comments
 (0)