Skip to content

Commit

Permalink
Support search & minPriority in WebUI (#774)
Browse files Browse the repository at this point in the history
* Support search & minPriority in WebUI
* Fix build
* Santitize regex text
* Fix review issues
* Remove annonations
  • Loading branch information
atuchin-m authored Oct 19, 2023
1 parent e131b05 commit 0842a23
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 17 deletions.
29 changes: 22 additions & 7 deletions src/core/study_processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,35 @@ export class StudyFilter {
minPriority = StudyPriority.NON_INTERESTING;
includeOutdated = false;
showEmptyGroups = false;
search?: string; // search in study/exp/feature names
private _search?: string; // search in study/exp/feature names
private _searchRegexp?: RegExp;

constructor(params?: Partial<StudyFilter>) {
Object.assign(this, params);
}

get search(): string | undefined {
return this._search;
}

set search(value: string | undefined) {
this._search = value;
const sanitizedValue = value?.replaceAll(/\W/g, '');
this._searchRegexp =
value !== undefined ? new RegExp(`(${sanitizedValue})`, 'gi') : undefined;
}

get searchRegexp() {
return this._searchRegexp;
}

matches(s: ProcessedStudy): boolean {
if (this.search !== undefined) {
const regex = this.searchRegexp;
if (regex !== undefined) {
let found = false;
found ||= s.study.name.search(this.search) !== -1;
for (const e of s.study.experiment ?? [])
found ||= e.name.search(this.search) !== -1;
for (const feature of s.affectedFeatures)
found ||= feature.search(this.search) !== -1;
found ||= regex.test(s.study.name);
for (const e of s.study.experiment ?? []) found ||= regex.test(e.name);
for (const feature of s.affectedFeatures) found ||= regex.test(feature);
if (!found) return false;
}

Expand Down
110 changes: 102 additions & 8 deletions src/web/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import { useSearchParams } from 'react-router-dom';

import { type StudyModel, type StudyListModel } from './study_model';
import { type FeatureModel, type ExperimentModel } from './experiment_model';
import { type StudyFilter } from '../../core/study_processor';
import {
StudyPriority,
type StudyFilter,
priorityToText,
} from '../../core/study_processor';
import { SeedType } from '../../core/base_types';
import { loadSeedDataAsync } from './seed_loader';
import { SearchParamManager } from './search_param_manager';
Expand All @@ -21,6 +25,7 @@ function sanitizeUrl(url: string): string {

export function FeatureItem(props: {
feature: FeatureModel;
filter: StudyFilter;
className: string;
}) {
return (
Expand All @@ -30,13 +35,14 @@ export function FeatureItem(props: {
rel="noreferrer"
href={sanitizeUrl(props.feature.link)}
>
{props.feature.name}
{maybeHighlight(props.filter, props.feature.name)}
</a>
);
}

export function FeatureList(props: {
title: string;
filter: StudyFilter;
className: string;
features: FeatureModel[];
}) {
Expand All @@ -45,7 +51,11 @@ export function FeatureList(props: {
}
const features = props.features.map((f) => (
<li key={f.name}>
<FeatureItem feature={f} className={props.className} />
<FeatureItem
feature={f}
filter={props.filter}
className={props.className}
/>
</li>
));
return (
Expand All @@ -56,21 +66,42 @@ export function FeatureList(props: {
);
}

export function ExperimentItem(props: { exp: ExperimentModel }) {
function maybeHighlight(filter: StudyFilter, text: string) {
if (filter.searchRegexp === undefined) {
return <>{text}</>;
}
const parts = text.split(filter.searchRegexp);
return (
<React.Fragment>
{parts.map((part, i) => {
const matched = filter.searchRegexp?.test(part);
if (matched === true) return <mark key={i}>{part}</mark>;
return <React.Fragment key={i}>{part}</React.Fragment>;
})}
</React.Fragment>
);
}

export function ExperimentItem(props: {
exp: ExperimentModel;
filter: StudyFilter;
}) {
const paramsList = props.exp.parameters().map((p) => <li key={p}>{p}</li>);
const classes =
'list-group-item ' +
(props.exp.isMajorGroup() ? 'major-exp-item' : 'exp-item');
return (
<li className={classes}>
{props.exp.name()} ({props.exp.weight()}%)
{maybeHighlight(props.filter, props.exp.name())} ({props.exp.weight()}%)
<FeatureList
title="Enabled features"
className="enabled-feature"
filter={props.filter}
features={props.exp.enabledFeatures()}
/>
<FeatureList
title="Disabled features"
filter={props.filter}
className="disabled-feature"
features={props.exp.disabledFeatures()}
/>
Expand Down Expand Up @@ -141,13 +172,13 @@ export function StudyItem(props: { study: StudyModel; filter: StudyFilter }) {
href={sanitizeUrl(props.study.getConfigUrl())}
rel="noreferrer"
>
{props.study.name()}
{maybeHighlight(props.filter, props.study.name())}
</a>
</div>
<div className="card-body">
<ul className="list-group list-group-flush">
{experiments.map((e) => (
<ExperimentItem key={e.name()} exp={e} />
<ExperimentItem key={e.name()} exp={e} filter={props.filter} />
))}
</ul>
</div>
Expand Down Expand Up @@ -243,6 +274,7 @@ export function FilterCheckbox(props: {
<label>
<input
type="checkbox"
className="filter-checkbox"
checked={props.checked}
onChange={props.toggle}
/>
Expand All @@ -252,6 +284,58 @@ export function FilterCheckbox(props: {
);
}

export function PriorityFilter(props: {
priority: number;
setPriority: (newPos: number) => void;
}) {
const optionsList = Object.values(StudyPriority)
.filter((v): v is StudyPriority => {
return typeof v !== 'string';
})
.map((v) => (
<option value={v} key={v}>
{priorityToText(v)}
</option>
));

return (
<div className="filter">
<label>
Min priority
<select
value={props.priority}
onInput={(e) => {
const target = e.target as HTMLSelectElement;
props.setPriority(parseInt(target.value));
}}
>
${optionsList}
</select>
</label>
</div>
);
}

export function FilterSearch(props: {
search: string | undefined;
setSearch: (search: string) => void;
}) {
return (
<div className="filter filter-left">
<label>
Search
<input
value={props.search}
onInput={(e) => {
const target = e.target as HTMLInputElement;
props.setSearch(target.value);
}}
/>
</label>
</div>
);
}

export function CurrentStudyList(props: {
studies: Map<SeedType, StudyListModel>;
searchParamManager: SearchParamManager;
Expand Down Expand Up @@ -286,6 +370,14 @@ export function CurrentStudyList(props: {
checked={paramManager.filter.includeOutdated}
toggle={paramManager.toggleIncludeOutdated.bind(paramManager)}
/>
<PriorityFilter
priority={paramManager.filter.minPriority}
setPriority={paramManager.setMinPriority.bind(paramManager)}
/>
<FilterSearch
search={paramManager.filter.search}
setSearch={paramManager.setSearch.bind(paramManager)}
/>
</div>
</div>
{studyList}
Expand Down Expand Up @@ -322,7 +414,9 @@ export function App() {
return (
<div className="container" id="app">
<section className="navbar navbar-light bg-light">
<h1>Brave Variations</h1>
<a className="heading" href=".">
<h1>Brave Variations</h1>
</a>
<nav className="nav nav-pills">
<NavItem
type={SeedType.PRODUCTION}
Expand Down
16 changes: 14 additions & 2 deletions src/web/app/search_param_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,25 @@ export class SearchParamManager {

toggleShowEmptyGroups() {
this.setParams({
showEmptyGroups: this.filter.showEmptyGroups ? 'false' : 'true',
showEmptyGroups: this.filter.showEmptyGroups ? null : 'true',
});
}

toggleIncludeOutdated() {
this.setParams({
includeOutdated: this.filter.includeOutdated ? 'false' : 'true',
includeOutdated: this.filter.includeOutdated ? null : 'true',
});
}

setMinPriority(minPriority: number) {
this.setParams({
minPriority: minPriority.toString(),
});
}

setSearch(value: string) {
this.setParams({
search: value === '' ? null : value,
});
}

Expand Down
24 changes: 24 additions & 0 deletions src/web/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
margin: 0;
}

.heading {
color: inherit;
text-decoration: inherit;
}

section.navbar {
margin: 15px 0 5px 0;
padding: 0;
Expand Down Expand Up @@ -63,9 +68,28 @@ section.navbar {
}

.filter {
display: flex;
justify-content: center;
align-items: center;
font-size: 0.8em;
margin-right: 15px;
}

.filter-left {
margin-left: auto;
margin-right: 0px;
}

.filter-checkbox {
vertical-align: middle;
position: relative;
bottom: 1px;
}

mark {
padding: 0 0 0 0;
}

.enabled-feature {
color: rgb(51, 88, 43);
padding: 0;
Expand Down

0 comments on commit 0842a23

Please sign in to comment.