[Fixes #1212] Confusing sorting UI in Topics (#1389)

* Topics: Updated tests for TableHeaderCell and recreated snapshots

* Topics: Updated snapshot

* Topics: Added more tests to verify that TableHeaderCell is rendering correctly

* Update kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.tsx

Co-authored-by: Oleg Shur <workshur@gmail.com>

* Update kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.tsx

Co-authored-by: Oleg Shur <workshur@gmail.com>

* Updates snapshots

* Updates snapshot

* Updates TableHeaderCell test to use theme object instead of hardcoded values

* got rid of codesmells

* Adds more tests to raise coverage

* cleanup

* Removes orderBy, orderValue, otherOrderValue from test

* Renames sortIconTitle -> sortIconTitleValue

* Renames th -> columnheader in test

* Renames title -> testTitle, previewText -> testPreviewText, sortIconTitleValue -> sortIconTitle

* Renames titleNode -> title, previewNode -> preview

Co-authored-by: Damir Abdulganiev <ehave@Damirs-MacBook-Pro.local>
Co-authored-by: Damir Abdulganiev <dabdulganiev@provectus.com>
Co-authored-by: Oleg Shur <workshur@gmail.com>
This commit is contained in:
Damir Abdulganiev 2022-01-19 12:43:00 +03:00 committed by GitHub
parent 439d41da0b
commit cdc929add7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 250 additions and 170 deletions

View file

@ -18,22 +18,7 @@ exports[`Tasks view matches snapshot 1`] = `
background-color: #F1F2F3; background-color: #F1F2F3;
} }
.c1 { .c2 {
padding: 4px 0 4px 24px !important;
border-bottom-width: 1px !important;
vertical-align: middle !important;
}
.c1.is-clickable {
cursor: pointer !important;
pointer-events: all !important;
}
.c1.has-text-link-dark span {
color: #4F4FFF !important;
}
.c1 span {
font-family: Inter,sans-serif; font-family: Inter,sans-serif;
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@ -46,18 +31,13 @@ exports[`Tasks view matches snapshot 1`] = `
text-align: left; text-align: left;
background: #FFFFFF; background: #FFFFFF;
color: #73848C; color: #73848C;
cursor: default;
} }
.c1 span.preview { .c1 {
margin-left: 8px; padding: 4px 0 4px 24px;
font-size: 14px; border-bottom-width: 1px;
color: #4F4FFF; vertical-align: middle;
cursor: pointer;
}
.c1 span.is-clickable {
cursor: pointer !important;
pointer-events: all !important;
} }
<table <table
@ -67,40 +47,36 @@ exports[`Tasks view matches snapshot 1`] = `
<tr> <tr>
<th <th
className="c1" className="c1"
title="ID"
> >
<span <span
className="title" className="c2"
> >
ID ID
</span> </span>
</th> </th>
<th <th
className="c1" className="c1"
title="Worker"
> >
<span <span
className="title" className="c2"
> >
Worker Worker
</span> </span>
</th> </th>
<th <th
className="c1" className="c1"
title="State"
> >
<span <span
className="title" className="c2"
> >
State State
</span> </span>
</th> </th>
<th <th
className="c1" className="c1"
title="Trace"
> >
<span <span
className="title" className="c2"
> >
Trace Trace
</span> </span>
@ -109,7 +85,7 @@ exports[`Tasks view matches snapshot 1`] = `
className="c1" className="c1"
> >
<span <span
className="title" className="c2"
/> />
</th> </th>
</tr> </tr>
@ -203,22 +179,7 @@ exports[`Tasks view matches snapshot when no tasks 1`] = `
background-color: #F1F2F3; background-color: #F1F2F3;
} }
.c1 { .c2 {
padding: 4px 0 4px 24px !important;
border-bottom-width: 1px !important;
vertical-align: middle !important;
}
.c1.is-clickable {
cursor: pointer !important;
pointer-events: all !important;
}
.c1.has-text-link-dark span {
color: #4F4FFF !important;
}
.c1 span {
font-family: Inter,sans-serif; font-family: Inter,sans-serif;
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@ -231,18 +192,13 @@ exports[`Tasks view matches snapshot when no tasks 1`] = `
text-align: left; text-align: left;
background: #FFFFFF; background: #FFFFFF;
color: #73848C; color: #73848C;
cursor: default;
} }
.c1 span.preview { .c1 {
margin-left: 8px; padding: 4px 0 4px 24px;
font-size: 14px; border-bottom-width: 1px;
color: #4F4FFF; vertical-align: middle;
cursor: pointer;
}
.c1 span.is-clickable {
cursor: pointer !important;
pointer-events: all !important;
} }
<table <table
@ -252,40 +208,36 @@ exports[`Tasks view matches snapshot when no tasks 1`] = `
<tr> <tr>
<th <th
className="c1" className="c1"
title="ID"
> >
<span <span
className="title" className="c2"
> >
ID ID
</span> </span>
</th> </th>
<th <th
className="c1" className="c1"
title="Worker"
> >
<span <span
className="title" className="c2"
> >
Worker Worker
</span> </span>
</th> </th>
<th <th
className="c1" className="c1"
title="State"
> >
<span <span
className="title" className="c2"
> >
State State
</span> </span>
</th> </th>
<th <th
className="c1" className="c1"
title="Trace"
> >
<span <span
className="title" className="c2"
> >
Trace Trace
</span> </span>
@ -294,7 +246,7 @@ exports[`Tasks view matches snapshot when no tasks 1`] = `
className="c1" className="c1"
> >
<span <span
className="title" className="c2"
/> />
</th> </th>
</tr> </tr>

View file

@ -260,6 +260,8 @@ exports[`Connectors ListItem matches snapshot 1`] = `
"normal": "#FFFFFF", "normal": "#FFFFFF",
}, },
"color": Object { "color": Object {
"active": "#4F4FFF",
"hover": "#4F4FFF",
"normal": "#73848C", "normal": "#73848C",
}, },
"previewColor": Object { "previewColor": Object {

View file

@ -271,6 +271,8 @@ exports[`Details when it has readonly flag does not render the Action button a T
"normal": "#FFFFFF", "normal": "#FFFFFF",
}, },
"color": Object { "color": Object {
"active": "#4F4FFF",
"hover": "#4F4FFF",
"normal": "#73848C", "normal": "#73848C",
}, },
"previewColor": Object { "previewColor": Object {

View file

@ -1,43 +1,52 @@
import styled from 'styled-components'; import styled, { css } from 'styled-components';
import { Colors } from 'theme/theme';
import { TableHeaderCellProps } from './TableHeaderCell'; interface TitleProps {
isOrderable?: boolean;
isOrdered?: boolean;
}
export const TableHeaderCell = styled.th<TableHeaderCellProps>` const isOrderableStyles = css`
padding: 4px 0 4px 24px !important; cursor: pointer;
border-bottom-width: 1px !important;
vertical-align: middle !important;
&.is-clickable { &:hover {
cursor: pointer !important; color: ${(props) => props.theme.thStyles.color.hover};
pointer-events: all !important;
}
&.has-text-link-dark span {
color: ${Colors.brand[50]} !important;
}
span {
font-family: Inter, sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: 0em;
text-align: left;
background: ${(props) => props.theme.thStyles.backgroundColor.normal};
color: ${(props) => props.theme.thStyles.color.normal};
&.preview {
margin-left: 8px;
font-size: 14px;
color: ${(props) => props.theme.thStyles.previewColor.normal};
cursor: pointer;
}
&.is-clickable {
cursor: pointer !important;
pointer-events: all !important;
}
} }
`; `;
export const Title = styled.span<TitleProps>`
font-family: Inter, sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: 0em;
text-align: left;
background: ${(props) => props.theme.thStyles.backgroundColor.normal};
color: ${(props) =>
props.isOrdered
? props.theme.thStyles.color.active
: props.theme.thStyles.color.normal};
cursor: default;
${(props) => props.isOrderable && isOrderableStyles}
`;
export const Preview = styled.span`
margin-left: 8px;
font-family: Inter, sans-serif;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: 0em;
text-align: left;
background: ${(props) => props.theme.thStyles.backgroundColor.normal};
font-size: 14px;
color: ${(props) => props.theme.thStyles.previewColor.normal};
cursor: pointer;
`;
export const TableHeaderCell = styled.th`
padding: 4px 0 4px 24px;
border-bottom-width: 1px;
vertical-align: middle;
`;

View file

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { TopicColumnsToSort } from 'generated-sources'; import { TopicColumnsToSort } from 'generated-sources';
import * as S from 'components/common/table/TableHeaderCell/TableHeaderCell.styled'; import * as S from 'components/common/table/TableHeaderCell/TableHeaderCell.styled';
import cx from 'classnames';
export interface TableHeaderCellProps { export interface TableHeaderCellProps {
title?: string; title?: string;
@ -13,38 +12,57 @@ export interface TableHeaderCellProps {
} }
const TableHeaderCell: React.FC<TableHeaderCellProps> = (props) => { const TableHeaderCell: React.FC<TableHeaderCellProps> = (props) => {
const { title, previewText, onPreview, orderBy, orderValue, handleOrderBy } = const {
props; title,
previewText,
onPreview,
orderBy,
orderValue,
handleOrderBy,
...restProps
} = props;
const isOrdered = !!orderValue && orderValue === orderBy;
const isOrderable = !!(orderValue && handleOrderBy);
const handleOnClick = () => {
return orderValue && handleOrderBy && handleOrderBy(orderValue);
};
const handleOnKeyDown = (event: React.KeyboardEvent) => {
return (
event.code === 'Space' &&
orderValue &&
handleOrderBy &&
handleOrderBy(orderValue)
);
};
const orderableProps = isOrderable && {
isOrderable,
onClick: handleOnClick,
onKeyDown: handleOnKeyDown,
role: 'button',
tabIndex: 0,
};
return ( return (
<S.TableHeaderCell <S.TableHeaderCell {...restProps}>
className={cx(orderBy && orderBy === orderValue && 'has-text-link-dark')} <S.Title isOrdered={isOrdered} {...orderableProps}>
{...props} {title}
> {isOrderable && (
<span className="title">{title}</span> <span title="Sort icon" className="icon is-small">
<i className="fas fa-sort" />
</span>
)}
</S.Title>
{previewText && ( {previewText && (
<span <S.Preview
className="preview"
onClick={onPreview} onClick={onPreview}
onKeyDown={onPreview} onKeyDown={onPreview}
role="button" role="button"
tabIndex={0} tabIndex={0}
> >
{previewText} {previewText}
</span> </S.Preview>
)}
{orderValue && (
<span
className="icon is-small is-clickable"
onClick={() =>
orderValue && handleOrderBy && handleOrderBy(orderValue)
}
onKeyDown={() => handleOrderBy}
role="button"
tabIndex={0}
>
<i className="fas fa-sort" />
</span>
)} )}
</S.TableHeaderCell> </S.TableHeaderCell>
); );

View file

@ -1,17 +1,25 @@
import React from 'react'; import React from 'react';
import { StaticRouter } from 'react-router'; import { screen, within } from '@testing-library/react';
import { render } from 'lib/testHelpers';
import TableHeaderCell, { import TableHeaderCell, {
TableHeaderCellProps, TableHeaderCellProps,
} from 'components/common/table/TableHeaderCell/TableHeaderCell'; } from 'components/common/table/TableHeaderCell/TableHeaderCell';
import { mountWithTheme } from 'lib/testHelpers';
import { TopicColumnsToSort } from 'generated-sources'; import { TopicColumnsToSort } from 'generated-sources';
import theme from 'theme/theme';
import userEvent from '@testing-library/user-event';
const STUB_TITLE = 'stub test title'; const SPACE_KEY = ' ';
const STUB_PREVIEW_TEXT = 'stub preview text';
const testTitle = 'test title';
const testPreviewText = 'test preview text';
const handleOrderBy = jest.fn();
const onPreview = jest.fn();
const sortIconTitle = 'Sort icon';
describe('TableHeaderCell', () => { describe('TableHeaderCell', () => {
const setupComponent = (props: TableHeaderCellProps) => ( const setupComponent = (props: Partial<TableHeaderCellProps> = {}) =>
<StaticRouter> render(
<table> <table>
<thead> <thead>
<tr> <tr>
@ -19,49 +27,135 @@ describe('TableHeaderCell', () => {
</tr> </tr>
</thead> </thead>
</table> </table>
</StaticRouter> );
);
it('renders without props', () => { it('renders without props', () => {
const wrapper = mountWithTheme(setupComponent({})); setupComponent();
expect(wrapper.contains(<TableHeaderCell />)).toBeTruthy(); expect(screen.getByRole('columnheader')).toBeInTheDocument();
}); });
it('renders with title & preview text', () => { it('renders with title & preview text', () => {
const wrapper = mountWithTheme( setupComponent({
setupComponent({ title: testTitle,
title: STUB_TITLE, previewText: testPreviewText,
previewText: STUB_PREVIEW_TEXT, });
})
);
expect(wrapper.find('span.title').text()).toEqual(STUB_TITLE); const columnheader = screen.getByRole('columnheader');
expect(wrapper.find('span.preview').text()).toEqual(STUB_PREVIEW_TEXT); expect(within(columnheader).getByText(testTitle)).toBeInTheDocument();
expect(within(columnheader).getByText(testPreviewText)).toBeInTheDocument();
}); });
it('renders with orderBy props', () => { it('renders with orderable props', () => {
const wrapper = mountWithTheme( setupComponent({
setupComponent({ title: testTitle,
title: STUB_TITLE, orderBy: TopicColumnsToSort.NAME,
orderBy: TopicColumnsToSort.NAME, orderValue: TopicColumnsToSort.NAME,
orderValue: TopicColumnsToSort.NAME, handleOrderBy,
}) });
); const columnheader = screen.getByRole('columnheader');
const title = within(columnheader).getByRole('button');
expect(title).toBeInTheDocument();
expect(title).toHaveTextContent(testTitle);
expect(within(title).getByTitle(sortIconTitle)).toBeInTheDocument();
expect(title).toHaveStyle(`color: ${theme.thStyles.color.active};`);
expect(title).toHaveStyle('cursor: pointer;');
});
expect(wrapper.find('span.title').text()).toEqual(STUB_TITLE); it('renders click on title triggers handler', () => {
expect(wrapper.exists('span.icon.is-small.is-clickable')).toBeTruthy(); setupComponent({
expect(wrapper.exists('i.fas.fa-sort')).toBeTruthy(); title: testTitle,
orderBy: TopicColumnsToSort.NAME,
orderValue: TopicColumnsToSort.NAME,
handleOrderBy,
});
const columnheader = screen.getByRole('columnheader');
const title = within(columnheader).getByRole('button');
userEvent.click(title);
expect(handleOrderBy.mock.calls.length).toBe(1);
});
it('renders space on title triggers handler', () => {
setupComponent({
title: testTitle,
orderBy: TopicColumnsToSort.NAME,
orderValue: TopicColumnsToSort.NAME,
handleOrderBy,
});
const columnheader = screen.getByRole('columnheader');
const title = within(columnheader).getByRole('button');
userEvent.type(title, SPACE_KEY);
// userEvent.type clicks and only then presses space
expect(handleOrderBy.mock.calls.length).toBe(2);
});
it('click on preview triggers handler', () => {
setupComponent({
title: testTitle,
previewText: testPreviewText,
onPreview,
});
const columnheader = screen.getByRole('columnheader');
const preview = within(columnheader).getByRole('button');
userEvent.click(preview);
expect(onPreview.mock.calls.length).toBe(1);
});
it('click on preview triggers handler', () => {
setupComponent({
title: testTitle,
previewText: testPreviewText,
onPreview,
});
const columnheader = screen.getByRole('columnheader');
const preview = within(columnheader).getByRole('button');
userEvent.type(preview, SPACE_KEY);
// userEvent.type clicks and only then presses space
expect(onPreview.mock.calls.length).toBe(2);
});
it('renders without sort indication', () => {
setupComponent({
title: testTitle,
orderBy: TopicColumnsToSort.NAME,
});
const columnheader = screen.getByRole('columnheader');
const title = within(columnheader).getByText(testTitle);
expect(within(title).queryByTitle(sortIconTitle)).not.toBeInTheDocument();
expect(title).toHaveStyle('cursor: default;');
});
it('renders with hightlighted title when orderBy and orderValue are equal', () => {
setupComponent({
title: testTitle,
orderBy: TopicColumnsToSort.NAME,
orderValue: TopicColumnsToSort.NAME,
});
const columnheader = screen.getByRole('columnheader');
const title = within(columnheader).getByText(testTitle);
expect(title).toHaveStyle(`color: ${theme.thStyles.color.active};`);
});
it('renders without hightlighted title when orderBy and orderValue are not equal', () => {
setupComponent({
title: testTitle,
orderBy: TopicColumnsToSort.NAME,
orderValue: TopicColumnsToSort.OUT_OF_SYNC_REPLICAS,
});
const columnheader = screen.getByRole('columnheader');
const title = within(columnheader).getByText(testTitle);
expect(title).toHaveStyle(`color: ${theme.thStyles.color.normal}`);
}); });
it('renders with default (primary) theme', () => { it('renders with default (primary) theme', () => {
const wrapper = mountWithTheme( setupComponent({
setupComponent({ title: testTitle,
title: STUB_TITLE, });
})
);
const domNode = wrapper.find('span').at(0).getDOMNode(); const columnheader = screen.getByRole('columnheader');
const background = getComputedStyle(domNode).getPropertyValue('background'); const title = within(columnheader).getByText(testTitle);
expect(background).toBe('rgb(255, 255, 255)'); expect(title).toHaveStyle(
`background: ${theme.thStyles.backgroundColor.normal};`
);
}); });
}); });

View file

@ -2,13 +2,14 @@ import { Action, TopicsState } from 'redux/interfaces';
import { getType } from 'typesafe-actions'; import { getType } from 'typesafe-actions';
import * as actions from 'redux/actions'; import * as actions from 'redux/actions';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { TopicColumnsToSort } from 'generated-sources';
export const initialState: TopicsState = { export const initialState: TopicsState = {
byName: {}, byName: {},
allNames: [], allNames: [],
totalPages: 1, totalPages: 1,
search: '', search: '',
orderBy: null, orderBy: TopicColumnsToSort.NAME,
consumerGroups: [], consumerGroups: [],
}; };

View file

@ -133,6 +133,8 @@ const theme = {
}, },
color: { color: {
normal: Colors.neutral[50], normal: Colors.neutral[50],
hover: Colors.brand[50],
active: Colors.brand[50],
}, },
previewColor: { previewColor: {
normal: Colors.brand[50], normal: Colors.brand[50],