Commit af044f0f authored by Andrii Vorobiov's avatar Andrii Vorobiov

ui: CSS modules for SortableTable and StatementsTable

This change refactors components to use CSS modules and
incorporate all required styles without any external
dependencies and prevent styles altering from outside.
It affects several components which tightly
coupled with StatementsTable and couldn't be changed
separately.

Following component are changed:
- HighlightedText
- Drawer
- StatementsTable
- SortableTable

Note, that `StatementsTable#makeCommonColumns` function
is refactored to provide custom styles from parent to
child components via props instead of overriding styles.

Storybook is extended to show some components as independent
units or in context of `StatementTable` component (if it is
only the way components work).

Release note: None
parent 5fff70be
// Copyright 2020 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
._text-bold
color #87beff
font-family RobotoMono-Bold
......@@ -9,6 +9,10 @@
// licenses/APL.txt.
import React from "react";
import classNames from "classnames/bind";
import styles from "./highlightedText.module.styl";
const cx = classNames.bind(styles);
export default function getHighlightedText(text: string, highlight: string, isOriginalText?: boolean) {
if (!highlight || highlight.length === 0) {
......@@ -25,7 +29,7 @@ export default function getHighlightedText(text: string, highlight: string, isOr
return parts.map((part, i) => {
if (search.includes(part.toLowerCase())) {
return (
<span key={i} className="_text-bold">
<span key={i} className={cx("_text-bold")}>
{`${part}`}
</span>
);
......
......@@ -18,6 +18,7 @@ import { Bytes } from "src/util/format";
import Loading from "src/views/shared/components/loading";
import { CachedDataReducerState } from "src/redux/cachedDataReducer";
import { NonTableStatsResponseMessage } from "src/util/api";
import "src/views/shared/components/sortabletable/sortabletable.styl";
interface TimeSeriesSummaryProps {
nonTableStats: protos.cockroach.server.serverpb.NonTableStatsResponse;
......
// Copyright 2020 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
.__actions
button, a
width auto
color #f6f6f6
font-size 12px
font-family Lato-Bold
line-height 2
letter-spacing 0.1px
&:hover
color #5f6c87
.drawer--preset-black
display flex
align-items center
/* width */
::-webkit-scrollbar {
width 7px
}
/* Track */
::-webkit-scrollbar-track {
background transparent
border-radius 10px
}
/* Handle */
::-webkit-scrollbar-thumb {
background #5f6c87
border-radius 10px
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background #5f6c87ad
}
:global(.ant-divider)
width 1px
background #5f6c87
margin: 0 15px
height 14px
.__actions
button, a
width auto
color #f6f6f6
font-size 12px
font-family Lato-Bold
line-height 2
letter-spacing 0.1px
&:hover
color #5f6c87
:global(.ant-drawer-content)
overflow auto
:global(.ant-drawer-mask)
background-color transparent
:global(.ant-drawer-content-wrapper)
padding-left 80px
box-shadow none !important
:global(.ant-drawer-content)
background #242a35
:global(.ant-drawer-header)
padding 15px 0
margin 0 24px
background transparent
border-bottom: 1px solid #5f6c87;
:global(.ant-drawer-body)
padding 15px 20px
margin 0 4px
height 190px
overflow-x hidde
overflow-y auto
......@@ -11,6 +11,10 @@
import React from "react";
import { Drawer, Button, Divider } from "antd";
import { Link } from "react-router-dom";
import classNames from "classnames/bind";
import styles from "./drawer.module.styl";
const cx = classNames.bind(styles);
interface IDrawerProps {
visible: boolean;
......@@ -32,7 +36,7 @@ const openDetails = (data: any) => {
export const DrawerComponent = ({ visible, onClose, children, data, details, ...props }: IDrawerProps) => (
<Drawer
title={
<div className="__actions">
<div className={cx("__actions")}>
<Button type="default" ghost block onClick={onClose}>
Close
</Button>
......@@ -48,7 +52,7 @@ export const DrawerComponent = ({ visible, onClose, children, data, details, ...
closable={false}
onClose={onClose}
visible={visible}
className="drawer--preset-black"
className={cx("drawer--preset-black")}
// getContainer={false}
{...props}
>
......
......@@ -9,7 +9,7 @@
// licenses/APL.txt.
import React from "react";
import classNames from "classnames";
import classNames from "classnames/bind";
import map from "lodash/map";
import isUndefined from "lodash/isUndefined";
import times from "lodash/times";
......@@ -18,11 +18,12 @@ import getHighlightedText from "src/util/highlightedText";
import { DrawerComponent } from "../drawer";
import { trackTableSort } from "src/util/analytics";
import "./sortabletable.styl";
import styles from "./sortabletable.module.styl";
import { Spin, Icon } from "antd";
import SpinIcon from "src/components/icon/spin";
import { Empty, IEmptyProps } from "src/components/empty";
const cx = classNames.bind(styles);
/**
* SortableColumn describes the contents a single column of a
* sortable table.
......@@ -151,7 +152,7 @@ export class SortableTable extends React.Component<TableProps> {
expansionControl(expanded: boolean) {
const content = expanded ? "" : "";
return (
<td className="sort-table__cell sort-table__cell__expansion-control">
<td className={cx("sort-table__cell", "sort-table__cell__expansion-control")}>
<div>
{content}
</div>
......@@ -161,7 +162,7 @@ export class SortableTable extends React.Component<TableProps> {
renderRow = (rowIndex: number) => {
const { columns, expandableConfig, drawer, firstCellBordered } = this.props;
const classes = classNames(
const classes = cx(
"sort-table__row",
"sort-table__row--body",
this.state.activeIndex === rowIndex ? "drawer-active" : "",
......@@ -187,7 +188,15 @@ export class SortableTable extends React.Component<TableProps> {
{expandableConfig ? this.expansionControl(expanded) : null}
{map(columns, (c: SortableColumn, colIndex: number) => {
return (
<td className={classNames("sort-table__cell", { "sort-table__cell--header": firstCellBordered && colIndex === 0 }, c.className)} key={colIndex}>
<td
className={cx(
"sort-table__cell",
{
"sort-table__cell--header": firstCellBordered && colIndex === 0,
},
c.className,
)}
key={colIndex}>
{c.cell(rowIndex)}
</td>
);
......@@ -196,7 +205,7 @@ export class SortableTable extends React.Component<TableProps> {
</tr>,
];
if (expandableConfig && expandableConfig.rowIsExpanded(rowIndex)) {
const expandedAreaClasses = classNames(
const expandedAreaClasses = cx(
"sort-table__row",
"sort-table__row--body",
"sort-table__row--expanded-area",
......@@ -208,7 +217,7 @@ export class SortableTable extends React.Component<TableProps> {
<tr className={expandedAreaClasses} key={output.length + 2} >
<td />
<td
className="sort-table__cell"
className={cx("sort-table__cell")}
colSpan={columns.length}
>
{expandableConfig.expandedContent(rowIndex)}
......@@ -253,39 +262,39 @@ export class SortableTable extends React.Component<TableProps> {
return <Empty {...emptyProps}/>;
}
return (
<div className="cl-table-wrapper">
<table className={classNames("sort-table", className)}>
<div className={cx("cl-table-wrapper")}>
<table className={cx("sort-table", className)}>
<thead>
<tr className="sort-table__row sort-table__row--header">
{expandableConfig ? <th className="sort-table__cell" /> : null}
<tr className={cx("sort-table__row", "sort-table__row--header")}>
{expandableConfig ? <th className={cx("sort-table__cell")} /> : null}
{map(columns, (c: SortableColumn, colIndex: number) => {
const classes = ["sort-table__cell"];
const classes = [cx("sort-table__cell")];
const style = {
textAlign: c.titleAlign,
};
let onClick: (e: any) => void = undefined;
if (!isUndefined(c.sortKey)) {
classes.push("sort-table__cell--sortable");
classes.push(cx("sort-table__cell--sortable"));
onClick = () => {
trackTableSort(className, c, sortSetting);
this.clickSort(c.sortKey);
};
if (c.sortKey === sortSetting.sortKey) {
if (sortSetting.ascending) {
classes.push(" sort-table__cell--ascending");
classes.push(cx("sort-table__cell--ascending"));
} else {
classes.push("sort-table__cell--descending");
classes.push(cx("sort-table__cell--descending"));
}
}
}
if (firstCellBordered && colIndex === 0) {
classes.push("sort-table__cell--header");
classes.push(cx("sort-table__cell--header"));
}
return (
<th className={classNames(classes)} key={colIndex} onClick={onClick} style={style}>
{c.title}
{!isUndefined(c.sortKey) && <span className="sortable__actions" />}
{!isUndefined(c.sortKey) && <span className={cx("sortable__actions")} />}
</th>
);
})}
......@@ -296,18 +305,18 @@ export class SortableTable extends React.Component<TableProps> {
</tbody>
</table>
{loading && (
<div className="table__loading">
<Spin className="table__loading--spin" indicator={<Icon component={SpinIcon} spin />} />
{loadingLabel && <span className="table__loading--label">{loadingLabel}</span>}
<div className={cx("table__loading")}>
<Spin className={cx("table__loading--spin")} indicator={<Icon component={SpinIcon} spin />} />
{loadingLabel && <span className={cx("table__loading--label")}>{loadingLabel}</span>}
</div>
)}
{drawer && (
<DrawerComponent visible={visible} onClose={this.onClose} data={drawerData} details>
<span className="drawer__content">{getHighlightedText(drawerData.statement, drawerData.search, true)}</span>
<span className={cx("drawer__content")}>{getHighlightedText(drawerData.statement, drawerData.search, true)}</span>
</DrawerComponent>
)}
{count === 0 && (
<div className="table__no-results">
<div className={cx("table__no-results")}>
{renderNoResult}
</div>
)}
......
// Copyright 2018 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
@import "sortabletable.styl"
......@@ -13,10 +13,14 @@ import _ from "lodash";
import { assert } from "chai";
import { shallow } from "enzyme";
import * as sinon from "sinon";
import classNames from "classnames/bind";
import styles from "./sortabletable.module.styl";
import "src/enzymeInit";
import { SortableTable, SortableColumn, SortSetting } from "src/views/shared/components/sortabletable";
const cx = classNames.bind(styles);
const columns: SortableColumn[] = [
{
title: "first",
......@@ -48,7 +52,7 @@ describe("<SortableTable>", () => {
const wrapper = makeTable(1);
assert.lengthOf(wrapper.find("table"), 1, "one table");
assert.lengthOf(wrapper.find("thead").find("tr"), 1, "one header row");
assert.lengthOf(wrapper.find("tr.sort-table__row--header"), 1, "column header row");
assert.lengthOf(wrapper.find(`tr.${cx("sort-table__row--header")}`), 1, "column header row");
assert.lengthOf(wrapper.find("tbody"), 1, "tbody element");
});
......@@ -58,10 +62,10 @@ describe("<SortableTable>", () => {
// Verify header structure.
assert.equal(wrapper.find("tbody").find("tr").length, rowCount, "correct number of rows");
const headers = wrapper.find("tr.sort-table__row--header");
const headers = wrapper.find(`tr.${cx("sort-table__row--header")}`);
_.each(columns, (c, index) => {
const header = headers.childAt(index);
assert.isTrue(header.is(".sort-table__cell"), "header is correct class.");
assert.isTrue(header.is(`.${cx("sort-table__cell")}`), "header is correct class.");
assert.equal(header.text(), c.title, "header has correct title.");
});
......@@ -76,27 +80,27 @@ describe("<SortableTable>", () => {
});
// Nothing is sorted.
assert.lengthOf(wrapper.find("th.sort-table__cell--ascending"), 0, "expected zero sorted columns.");
assert.lengthOf(wrapper.find("th.sort-table__cell--descending"), 0, "expected zero sorted columns.");
assert.lengthOf(wrapper.find(`th.${cx("sort-table__cell--ascending")}`), 0, "expected zero sorted columns.");
assert.lengthOf(wrapper.find(`th.${cx("sort-table__cell--descending")}`), 0, "expected zero sorted columns.");
});
it("renders sorted column correctly.", () => {
// ascending = false.
let wrapper = makeTable(1, { sortKey: 1, ascending: false });
let sortHeader = wrapper.find("th.sort-table__cell--descending");
let sortHeader = wrapper.find(`th.${cx("sort-table__cell--descending")}`);
assert.lengthOf(sortHeader, 1, "only a single column is sorted descending.");
assert.equal(sortHeader.text(), columns[0].title, "first column should be sorted.");
sortHeader = wrapper.find("th.sort-table__cell--ascending");
sortHeader = wrapper.find(`th.${cx("sort-table__cell--ascending")}`);
assert.lengthOf(sortHeader, 0, "no columns are sorted ascending.");
// ascending = true
wrapper = makeTable(1, { sortKey: 2, ascending: true });
sortHeader = wrapper.find("th.sort-table__cell--ascending");
sortHeader = wrapper.find(`th.${cx("sort-table__cell--ascending")}`);
assert.lengthOf(sortHeader, 1, "only a single column is sorted ascending.");
assert.equal(sortHeader.text(), columns[1].title, "second column should be sorted.");
sortHeader = wrapper.find("th.sort-table__cell--descending");
sortHeader = wrapper.find(`th.${cx("sort-table__cell--descending")}`);
assert.lengthOf(sortHeader, 0, "no columns are sorted descending.");
});
});
......@@ -105,7 +109,7 @@ describe("<SortableTable>", () => {
it("sorts descending on initial click.", () => {
const spy = sinon.spy();
const wrapper = makeTable(1, undefined, spy);
wrapper.find("th.sort-table__cell--sortable").first().simulate("click");
wrapper.find(`th.${cx("sort-table__cell")}`).first().simulate("click");
assert.isTrue(spy.calledOnce);
assert.isTrue(spy.calledWith({
sortKey: 1,
......@@ -118,7 +122,7 @@ describe("<SortableTable>", () => {
const spy = sinon.spy();
const wrapper = makeTable(1, {sortKey: 2, ascending: true}, spy);
wrapper.find("th.sort-table__cell--sortable").first().simulate("click");
wrapper.find(`th.${cx("sort-table__cell")}`).first().simulate("click");
assert.isTrue(spy.calledOnce);
assert.isTrue(spy.calledWith({
sortKey: 1,
......@@ -130,7 +134,7 @@ describe("<SortableTable>", () => {
const spy = sinon.spy();
const wrapper = makeTable(1, {sortKey: 1, ascending: false}, spy);
wrapper.find("th.sort-table__cell--sortable").first().simulate("click");
wrapper.find(`th.${cx("sort-table__cell")}`).first().simulate("click");
assert.isTrue(spy.calledOnce);
assert.isTrue( spy.calledWith({
sortKey: 1,
......@@ -142,7 +146,7 @@ describe("<SortableTable>", () => {
const spy = sinon.spy();
const wrapper = makeTable(1, {sortKey: 1, ascending: true}, spy);
wrapper.find("th.sort-table__cell--sortable").first().simulate("click");
wrapper.find(`th.${cx("sort-table__cell")}`).first().simulate("click");
assert.isTrue(spy.calledOnce);
assert.isTrue( spy.calledWith({
sortKey: null,
......@@ -155,7 +159,7 @@ describe("<SortableTable>", () => {
const spy = sinon.spy();
const wrapper = makeTable(1, {sortKey: 1, ascending: true}, spy);
wrapper.find("thead th.sort-table__cell").last().simulate("click");
wrapper.find(`thead th.${cx("sort-table__cell")}`).last().simulate("click");
assert.isTrue(spy.notCalled);
});
});
......
......@@ -135,4 +135,10 @@
font-size $font-size--medium
line-height 22px
letter-spacing $letter-spacing--compact
color $colors--neutral-6
\ No newline at end of file
color $colors--neutral-6
.drawer__content
color #f6f6f6
font-family RobotoMono-Regular
font-size 12px
line-height 24px
......@@ -13,10 +13,14 @@ import _ from "lodash";
import { assert } from "chai";
import { mount } from "enzyme";
import * as sinon from "sinon";
import classNames from "classnames/bind";
import "src/enzymeInit";
import { SortedTable, ColumnDescriptor, ISortedTablePagination } from "src/views/shared/components/sortedtable";
import { SortSetting } from "src/views/shared/components/sortabletable";
import styles from "../sortabletable/sortabletable.module.styl";
const cx = classNames.bind(styles);
class TestRow {
constructor(public name: string, public value: number) { }
......@@ -67,14 +71,14 @@ describe("<SortedTable>", function() {
const wrapper = makeTable([new TestRow("test", 1)]);
assert.lengthOf(wrapper.find("table"), 1, "one table");
assert.lengthOf(wrapper.find("thead").find("tr"), 1, "one header row");
assert.lengthOf(wrapper.find("tr.sort-table__row--header"), 1, "column header row");
assert.lengthOf(wrapper.find(`tr.${cx("sort-table__row--header")}`), 1, "column header row");
assert.lengthOf(wrapper.find("tbody"), 1, "tbody element");
});
it("correctly uses onChangeSortSetting", function() {
const spy = sinon.spy();
const wrapper = makeTable([new TestRow("test", 1)], undefined, spy);
wrapper.find("th.sort-table__cell--sortable").first().simulate("click");
wrapper.find(`th.${cx("sort-table__cell")}`).first().simulate("click");
assert.isTrue(spy.calledOnce);
assert.deepEqual(spy.getCall(0).args[0], {
sortKey: 0,
......@@ -110,30 +114,30 @@ describe("<SortedTable>", function() {
const wrapper = makeExpandableTable([new TestRow("test", 1)], undefined);
assert.lengthOf(wrapper.find("table"), 1, "one table");
assert.lengthOf(wrapper.find("thead").find("tr"), 1, "one header row");
assert.lengthOf(wrapper.find("tr.sort-table__row--header"), 1, "column header row");
assert.lengthOf(wrapper.find(`tr.${cx("sort-table__row--header")}`), 1, "column header row");
assert.lengthOf(wrapper.find("tbody"), 1, "tbody element");
assert.lengthOf(wrapper.find("tbody tr"), 1, "one body row");
assert.lengthOf(wrapper.find("tbody td"), 3, "two body cells plus one expansion control cell");
assert.lengthOf(wrapper.find("td.sort-table__cell__expansion-control"), 1, "one expansion control cell");
assert.lengthOf(wrapper.find(`td.${cx("sort-table__cell__expansion-control")}`), 1, "one expansion control cell");
});
it("expands and collapses the clicked row", function() {
const wrapper = makeExpandableTable([new TestRow("test", 1)], undefined);
assert.lengthOf(wrapper.find(".sort-table__row--expanded-area"), 0, "nothing expanded yet");
wrapper.find(".sort-table__cell__expansion-control").simulate("click");
assert.lengthOf(wrapper.find(`.${cx("sort-table__row--expanded-area")}`), 0, "nothing expanded yet");
wrapper.find(`.${cx("sort-table__cell__expansion-control")}`).simulate("click");
const expandedArea = wrapper.find(".sort-table__row--expanded-area");
assert.lengthOf(expandedArea, 1, "row is expanded");
assert.lengthOf(expandedArea.children(), 2, "expanded row contains placeholder and content area");
assert.isTrue(expandedArea.contains(<td />));
assert.isTrue(expandedArea.contains(
<td className="sort-table__cell" colSpan={2}>
<td className={cx("sort-table__cell")} colSpan={2}>
<div>
test=1
</div>
</td>,
));
wrapper.find(".sort-table__cell__expansion-control").simulate("click");
assert.lengthOf(wrapper.find(".sort-table__row--expanded-area"), 0, "row collapsed again");
wrapper.find(`.${cx("sort-table__cell__expansion-control")}`).simulate("click");
assert.lengthOf(wrapper.find(`.${cx("sort-table__row--expanded-area")}`), 0, "row collapsed again");
});
});
......
......@@ -406,8 +406,6 @@ $plan-node-warning-background-color = rgba(209, 135, 55, 0.06) // light orange
&:hover
color $colors--primary-blue-3
text-decoration underline
._text-bold
font-family RobotoMono-Bold
.cl-table-link__statement-tooltip--fixed-width
max-width max-content
......
// Copyright 2020 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
._pg-jump
width 32px
height 24px
position relative
justify-content center
align-items center
._jump-dots
position absolute
top 50%
left 50%
transform: translate(-50%, -50%)
color $colors--neutral-5
font-size 14px
letter-spacing 1.5px
.anticon
position absolute
top 50%
left 50%
transform: translate(-50%, -50%)
display none
font-size 10px
color #3a7ce1
&:hover
._jump-dots
display none
.anticon
display block
.cl-table-statistic
display flex
justify-content space-between
align-items center
margin-bottom 7px
.cl-count-title, .last-cleared-title
font-family SourceSansPro-Regular
font-size 14px
padding 0px
margin 0px
color $placeholder
line-height 1.57
letter-spacing 0.1px
.label
font-family $font-family--bold
color $colors--neutral-7
.section
flex 0 0 auto
padding 12px 24px
max-width $max-window-width
clearfix()
&--heading
padding-top 0
padding-bottom 0
&--container
padding 0 24px 0 0
.base-heading
composes base-heading from '~styl/base/typography.styl'
......@@ -17,6 +17,7 @@ import Helmet from "react-helmet";
import { connect } from "react-redux";
import { createSelector } from "reselect";
import { RouteComponentProps, withRouter } from "react-router-dom";
import classNames from "classnames/bind";
import { paginationPageCount } from "src/components/pagination/pagination";
import * as protos from "src/js/protos";
import { refreshStatementDiagnosticsRequests, refreshStatements } from "src/redux/apiReducers";
......@@ -35,17 +36,19 @@ import { SortSetting } from "src/views/shared/components/sortabletable";
import { Search } from "../app/components/Search";
import { AggregateStatistics, makeStatementsColumns, StatementsSortedTable } from "./statementsTable";
import ActivateDiagnosticsModal, { ActivateDiagnosticsModalRef } from "src/views/statements/diagnostics/activateDiagnosticsModal";
import "./statements.styl";
import {
selectLastDiagnosticsReportPerStatement,
} from "src/redux/statements/statementsSelectors";
import { createStatementDiagnosticsAlertLocalSetting } from "src/redux/alerts";
import { getMatchParamByName } from "src/util/query";
import { trackPaginate, trackSearch } from "src/util/analytics";
import "./statements.styl";
import { ISortedTablePagination } from "../shared/components/sortedtable";
import { statementsTable } from "src/util/docs";
import styles from "./statementsPage.module.styl";
import sortableTableStyles from "src/views/shared/components/sortabletable/sortabletable.module.styl";
const cx = classNames.bind(styles);
const sortableTableCx = classNames.bind(sortableTableStyles);
type ICollectedStatementStatistics = protos.cockroach.server.serverpb.StatementsResponse.ICollectedStatementStatistics;
......@@ -199,16 +202,16 @@ export class StatementsPage extends React.Component<StatementsPageProps, Stateme
switch (type) {
case "jump-prev":
return (
<div className="_pg-jump">
<div className={cx("_pg-jump")}>
<Icon type="left" />
<span className="_jump-dots">•••</span>