Skip to content

Commit

Permalink
feat: implement filtering by browser in new ui (#593)
Browse files Browse the repository at this point in the history
* feat: implement filtering by browser in new ui
  • Loading branch information
shadowusr authored Aug 23, 2024
1 parent 549a42f commit e526ec3
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {isString} from 'lodash';
import React, {ReactNode} from 'react';

const valueToIcon = {
google: 'chrome',
chrome: 'chrome',
firefox: 'firefox',
safari: 'safari',
edge: 'edge',
yandex: 'yandex',
yabro: 'yandex',
ie: 'internet-explorer',
explorer: 'internet-explorer',
opera: 'opera',
phone: 'mobile',
mobile: 'mobile',
tablet: 'tablet',
ipad: 'tablet'
} as const;

export function BrowserIcon({name: browser}: {name: string}): ReactNode {
const getIcon = (iconName: string): ReactNode => <i className={`fa fa-${iconName}`} aria-hidden="true" />;

if (!isString(browser)) {
return getIcon('browser');
}

const lowerValue = browser.toLowerCase();

for (const pattern in valueToIcon) {
if (lowerValue.includes(pattern)) {
return getIcon(valueToIcon[pattern as keyof typeof valueToIcon]);
}
}

return getIcon('browser');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
:global(.g-popup) {
z-index: 9999;
}

.browserlist__filter {
padding: 4px;
}

.action-button {
width: 60px;
margin-left: auto;
}

.browserlist__popup {
--g-color-text-info: var(--g-color-private-color-500-solid);
}

.browserlist__popup :global(.g-select-list__option) {
gap: 8px;
flex-direction: row-reverse;
}
180 changes: 180 additions & 0 deletions lib/static/new-ui/features/suites/components/BrowsersSelect/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import {Globe} from '@gravity-ui/icons';
import {Button, Flex, Select, SelectRenderControlProps, SelectRenderOption} from '@gravity-ui/uikit';
import React, {useState, useEffect, ReactNode} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';

import * as actions from '@/static/modules/actions';
import {BrowserIcon} from '@/static/new-ui/features/suites/components/BrowsersSelect/BrowserIcon';
import {State} from '@/static/new-ui/types/store';
import {BrowserItem} from '@/types';
import styles from './index.module.css';

// In the onUpdate callback we only have access to array of selected strings. That's why we need to serialize
// id/version in string. Encoding to avoid errors if id/version contains delimiter.
// The other approach would be to use mapping, but in practice it makes things even more complex.
const DELIMITER = '/';

const serializeBrowserData = (id: string, version: string): string =>
`${encodeURIComponent(id)}${DELIMITER}${encodeURIComponent(version)}`;

const deserializeBrowserData = (data: string): {id: string; version: string} => {
const [idEncoded, versionEncoded] = data.split(DELIMITER);
return {id: decodeURIComponent(idEncoded), version: decodeURIComponent(versionEncoded)};
};

interface BrowsersSelectProps {
browsers: BrowserItem[];
filteredBrowsers: BrowserItem[];
actions: typeof actions;
}

function BrowsersSelectInternal({browsers, filteredBrowsers, actions}: BrowsersSelectProps): ReactNode {
const [selectedBrowsers, setSelectedBrowsers] = useState<BrowserItem[]>([]);

useEffect(() => {
setSelectedBrowsers(filteredBrowsers);
}, [browsers, filteredBrowsers]);

const renderFilter = (): React.JSX.Element => {
return (
<div className={styles['browserlist__filter']}>
<Button onClick={(): void => setSelectedBrowsers(browsers)} width='max'>
Select All
</Button>
</div>
);
};

const onUpdate = (values: string[]): void => {
const selectedItems: BrowserItem[] = [];

values.forEach(encodedBrowserData => {
const {id, version} = deserializeBrowserData(encodedBrowserData);
const existingBrowser = selectedItems.find(browser => browser.id === id);

if (existingBrowser) {
if (!existingBrowser.versions.includes(version)) {
existingBrowser.versions.push(version);
}
} else {
selectedItems.push({id, versions: [version]});
}
});

setSelectedBrowsers(selectedItems);
};

const renderOptions = (): React.JSX.Element | React.JSX.Element[] => {
const browsersWithMultipleVersions = browsers.filter(browser => browser.versions.length > 1);
const browsersWithSingleVersion = browsers.filter(browser => browser.versions.length === 1);

const getOptionProps = (browser: BrowserItem, version: string): {value: string; content: string; data: Record<string, unknown>} => ({
value: serializeBrowserData(browser.id, version),
content: browser.id,
data: {id: browser.id, version}
});

if (browsersWithMultipleVersions.length === 0) {
// If there are no browsers with multiple versions, we want to render a simple plain list
return browsers.map(browser => <Select.Option
key={browser.id}
{...getOptionProps(browser, browser.versions[0])}
/>);
} else {
// Otherwise render browser version groups and place all browsers with single version into "Other" group
return (
<>
{browsersWithMultipleVersions.map(browser => (
<Select.OptionGroup key={browser.id} label={browser.id}>
{browser.versions.map(version => (
<Select.Option
key={version}
{...getOptionProps(browser, version)}
/>
))}
</Select.OptionGroup>
))}
<Select.OptionGroup label="Other">
{browsersWithSingleVersion.map(browser => (
<Select.Option
key={browser.id}
{...getOptionProps(browser, browser.versions[0])}
/>
))}
</Select.OptionGroup>
</>
);
}
};

const renderControl = ({onClick, onKeyDown, ref}: SelectRenderControlProps): React.JSX.Element => {
return <Button ref={ref} onClick={onClick} extraProps={{onKeyDown}} view={'outlined'} style={{width: 28}}>
<Globe/>
</Button>;
};

const selected = selectedBrowsers.flatMap(browser => browser.versions.map(version => serializeBrowserData(browser.id, version)));

const onClose = (): void => {
actions.selectBrowsers(selectedBrowsers.filter(browser => browser.versions.length > 0));
};

const onFocus = (): void => {
if (selected.length === 0) {
setSelectedBrowsers(browsers);
}
};

const renderOption: SelectRenderOption<{id: string; version: string}> = (option) => {
const isTheOnlySelected = selected.includes(option.value) && selected.length === 1;
const selectOnly = (e: React.MouseEvent<HTMLElement>): void => {
e.preventDefault();
e.stopPropagation();
setSelectedBrowsers([{id: option.data?.id as string, versions: [option.data?.version as string]}]);
};
const selectExcept = (e: React.MouseEvent<HTMLElement>): void => {
e.preventDefault();
e.stopPropagation();
setSelectedBrowsers(browsers.map(browser => ({
id: browser.id,
versions: browser.versions.filter(version => browser.id !== option.data?.id || version !== option.data?.version)
})));
};
return (
<Flex alignItems="center" width={'100%'} gap={2}>
<Flex height={16} width={16} alignItems={'center'} justifyContent={'center'}><BrowserIcon name={option.data?.id as string} /></Flex>
<div className="browser-name">{option.content}</div>
<Button size="s" onClick={isTheOnlySelected ? selectExcept : selectOnly} className={styles.actionButton}>{isTheOnlySelected ? 'Except' : 'Only'}</Button>
</Flex>
);
};

return (
<Select
disablePortal
value={selected}
multiple={true}
hasCounter
filterable
renderFilter={renderFilter}
renderOption={renderOption}
renderControl={renderControl}
popupClassName={styles['browserlist__popup']}
className='browserlist'
onUpdate={onUpdate}
onFocus={onFocus}
onClose={onClose}
>
{renderOptions()}
</Select>
);
}

export const BrowsersSelect = connect(
(state: State) => ({
filteredBrowsers: state.view.filteredBrowsers,
browsers: state.browsers
}),
(dispatch) => ({actions: bindActionCreators(actions, dispatch)})
)(BrowsersSelectInternal);
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import {SplitViewLayout} from '@/static/new-ui/components/SplitViewLayout';
import {TestNameFilter} from '@/static/new-ui/features/suites/components/TestNameFilter';
import {SuitesTreeView} from '@/static/new-ui/features/suites/components/SuitesTreeView';
import {TestStatusFilter} from '@/static/new-ui/features/suites/components/TestStatusFilter';
import {BrowsersSelect} from '@/static/new-ui/features/suites/components/BrowsersSelect';

function SuitesPageInternal(): ReactNode {
return <SplitViewLayout>
<div>
<Flex direction={'column'} spacing={{p: '2'}} style={{height: '100vh'}}>
<h2 className="text-display-1">Suites</h2>
<Flex>
<Flex gap={2}>
<TestNameFilter/>
<BrowsersSelect/>
</Flex>
<Flex spacing={{mt: 2}}>
<TestStatusFilter/>
Expand Down
10 changes: 6 additions & 4 deletions lib/static/new-ui/types/store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {TestStatus, ViewMode} from '@/constants';
import {ImageFile} from '@/types';
import {BrowserItem, ImageFile} from '@/types';

export interface SuiteEntityNode {
name: string;
Expand Down Expand Up @@ -65,7 +65,8 @@ export interface State {
app: {
isInitialized: boolean;
currentSuiteId: string | null;
}
};
browsers: BrowserItem[];
tree: {
browsers: {
allIds: string[];
Expand All @@ -83,9 +84,10 @@ export interface State {
byId: Record<string, SuiteEntity>;
stateById: Record<string, SuiteState>;
};
}
};
view: {
testNameFilter: string;
viewMode: ViewMode;
}
filteredBrowsers: BrowserItem[];
};
}
7 changes: 1 addition & 6 deletions lib/tests-tree-builder/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {BaseTestsTreeBuilder, BaseTestsTreeBuilderOptions, Tree} from './base';
import {BrowserVersions, DB_COLUMN_INDEXES, TestStatus} from '../constants';
import {ReporterTestResult} from '../adapters/test-result';
import {SqliteTestResultAdapter} from '../adapters/test-result/sqlite';
import {RawSuitesRow} from '../types';
import {BrowserItem, RawSuitesRow} from '../types';

interface Stats {
total: number;
Expand All @@ -27,11 +27,6 @@ export interface SkipItem {
comment?: string;
}

interface BrowserItem {
id: string;
versions: string[];
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface StaticTestsTreeBuilderOptions extends BaseTestsTreeBuilderOptions {}

Expand Down
5 changes: 5 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,3 +275,8 @@ export type RawSuitesRow = [
export type LabeledSuitesRow = {
[K in (typeof SUITES_TABLE_COLUMNS)[number]['name']]: string;
};

export interface BrowserItem {
id: string;
versions: string[];
}

0 comments on commit e526ec3

Please sign in to comment.