Skip to content

Commit

Permalink
Improve feature search UX, add dark mode, and update deps (#3422)
Browse files Browse the repository at this point in the history
  • Loading branch information
riverar authored Jan 7, 2025
1 parent eed7453 commit 9911fee
Show file tree
Hide file tree
Showing 6 changed files with 688 additions and 481 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: '23.1' # Temporary: https://github.com/nodejs/node/issues/55826
node-version: 'latest'

- name: Build sites
shell: pwsh
Expand Down
829 changes: 456 additions & 373 deletions web/features/package-lock.json

Large diffs are not rendered by default.

284 changes: 186 additions & 98 deletions web/features/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
import React from 'react';
import { SearchResult } from './worker/search';
import {
makeStyles,
mergeClasses,
shorthands,
Input,
DataGridHeaderCell,
Button,
createTableColumn,
DataGrid,
DataGridBody,
DataGridCell,
DataGridHeader,
DataGridHeaderCell,
DataGridRow,
DataGrid,
TableColumnDefinition,
TableCellLayout,
InputOnChangeData,
createTableColumn,
InfoLabel,
Dropdown,
InfoLabel,
Input,
InputOnChangeData,
makeStyles,
mergeClasses,
Option,
OptionOnSelectData,
SelectionEvents
SelectionEvents,
shorthands,
TableCellLayout,
TableColumnDefinition
} from '@fluentui/react-components';
import Path from './components/path';
import FeatureList from './components/featurelist';
import { IWorkerMessage, IInitializeMessage, IInitializeResultMessage, ISearchMessage, ISearchResultMessage } from './worker/worker';
import { WeatherMoonRegular, WeatherSunnyRegular } from '@fluentui/react-icons';
import React, { useCallback, useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import FeatureList from './components/featurelist';
import Path from './components/path';
import { SearchResult } from './worker/search';
import { IInitializeMessage, IInitializeResultMessage, ISearchMessage, ISearchResultMessage, IWorkerMessage } from './worker/worker';

const useStyles = makeStyles({
root: {
Expand All @@ -41,6 +43,20 @@ const useStyles = makeStyles({
grid: {
width: '100%'
},
desktopOnly: {
'@media (max-width: 768px)': {
display: 'none',
},
},
mobileGrid: {
'@media (min-width: 769px)': {
display: 'none',
},
display: 'flex',
flexDirection: 'column',
width: '100%',
...shorthands.gap('1em'),
},
featureList: {
textAlign: 'right',
},
Expand All @@ -66,7 +82,27 @@ const useStyles = makeStyles({
flexGrow: 5
},
searchBranch: {
flexGrow: 1
flexGrow: 1,
minWidth: '200px'
},
mobileRow: {
display: 'grid',
gap: '20px',
paddingBottom: '16px',
flexDirection: 'column',
borderBottomWidth: '1px',
borderBottomStyle: 'solid',
borderBottomColor: 'var(--colorNeutralStroke1)',
...shorthands.gap('0.5em'),
},
labelValuePair: {
display: 'grid',
gridTemplateRows: 'auto auto',
gap: '4px',
},
label: {
fontWeight: 'bold',
fontSize: '.75em'
}
});

Expand Down Expand Up @@ -117,55 +153,62 @@ const columns: TableColumnDefinition<Renderable<SearchResult>>[] = [
type Renderable<TItem> = TItem & { classes: ReturnType<typeof useStyles> };

function App() {
const [searchResults, setSearchResults] = React.useState<SearchResult[]>([]);
const [resultsTruncated, setResultsTruncated] = React.useState(false);
const [searchTimeoutId, setSearchTimeoutId] = React.useState(0);
const [searchReady, setSearchReady] = React.useState(false);
const [worker, setWorker] = React.useState<Worker | null>(null);
const [query, setQuery] = React.useState('');
// State

const params = useParams();
const styles = useStyles();
const navigate = useNavigate();

const [worker, setWorker] = useState<Worker | null>(null);
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
const [resultsTruncated, setResultsTruncated] = useState(false);
const [searchTimeoutId, setSearchTimeoutId] = useState(0);
const [searchReady, setSearchReady] = useState(false);
const [query, setQuery] = useState(params.query! ?? '');
const [savedTheme, setSavedTheme] = useState(localStorage.getItem('theme') ?? 'light');

const branches = process.env.REACT_APP_BRANCHES!.split(',');
const defaultBranch = branches.includes(params.branch!) ? params.branch : branches[0];
const [branch, setBranch] = React.useState(defaultBranch);
const [branch, setBranch] = useState(defaultBranch);

const styles = useStyles();
const onMessage = function ({ data }: MessageEvent<IWorkerMessage>) {
switch (data.name) {
case 'initializeResult':
{
const msg = data as IInitializeResultMessage;
setSearchReady(msg.result);
break;
}
case 'searchResult':
{
const msg = data as ISearchResultMessage;
setSearchResults(msg.results);
setResultsTruncated(msg.truncated);
break;
}
// Effects and Hooks

const onMessage = useCallback(({ data }: MessageEvent<IWorkerMessage>) => {
if (data.name === 'initializeResult') {
const msg = data as IInitializeResultMessage;
setSearchReady(msg.result);
return;
}
};

React.useEffect(() => {
const worker = new Worker(
new URL('./worker/worker.ts', import.meta.url)
);
worker.addEventListener('message', onMessage);
worker.postMessage({
if (data.name === 'searchResult') {
const msg = data as ISearchResultMessage;
setSearchResults(msg.results);
setResultsTruncated(msg.truncated);
return;
}
}, []);

useEffect(() => {
const newWorker = new Worker(new URL('./worker/worker.ts', import.meta.url));
newWorker.addEventListener('message', onMessage);
newWorker.postMessage({
name: 'initialize',
branch
} as IInitializeMessage);
setWorker(worker);

setWorker(newWorker);

return () => {
worker.removeEventListener('message', onMessage);
worker.terminate();
newWorker.removeEventListener('message', onMessage);
newWorker.terminate();
setWorker(null);
setSearchReady(false);
};
}, [branch]);

React.useEffect(() => {
useEffect(() => {
if (!searchReady) return;

if (query.length > 0) {
if (searchTimeoutId) {
clearTimeout(searchTimeoutId);
Expand All @@ -182,25 +225,43 @@ function App() {
} else {
setSearchResults([]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query, worker]);
}, [query, worker, searchReady]);

// Event handlers

const onChangeQuery = (
ev: React.ChangeEvent<HTMLInputElement>,
data: InputOnChangeData
) => {
setQuery(data.value);
setResultsTruncated(false);

if (data.value.length > 0) {
navigate(`/${branch}/search/${data.value}`, { replace: true });
} else {
navigate(`/${branch}`, { replace: true });
}
};

const onThemeChange = () => {
const newTheme = savedTheme === 'light' ? 'dark' : 'light';
localStorage.setItem('theme', newTheme);
setSavedTheme(newTheme);
window.location.reload();
};

const navigate = useNavigate();
const onBranchSelected = (
ev: SelectionEvents,
data: OptionOnSelectData
) => {
const branch = data.optionValue ?? defaultBranch;
setBranch(branch);
navigate(`/${branch}`, { replace: true });

if (query.length > 0) {
navigate(`/${branch}/search/${query}`, { replace: true });
} else {
navigate(`/${branch}`, { replace: true });
}
};

return (
Expand All @@ -210,6 +271,7 @@ function App() {
<Input
className={styles.searchInput}
size='large'
value={query}
onChange={onChangeQuery}
spellCheck='false'
autoComplete='off'
Expand Down Expand Up @@ -249,53 +311,79 @@ function App() {
))
}
</Dropdown>
<Button
icon={savedTheme === 'dark' ? <WeatherSunnyRegular /> : <WeatherMoonRegular />}
onClick={onThemeChange}
appearance="transparent"
/>
</div>

<DataGrid
items={searchResults}
columns={columns}
focusMode='none'
size='medium'
className={styles.grid}
>
<DataGridHeader>
<h2>
Results ({searchResults.length}
{resultsTruncated ? '+' : ''})
</h2>
<DataGridRow>
{(col) => {
<div className={mergeClasses(styles.grid, styles.desktopOnly)}>
<DataGrid
items={searchResults}
columns={columns}
focusMode='none'
size='medium'
className={styles.grid}
>
<DataGridHeader>
<h2>
Results ({searchResults.length}
{resultsTruncated ? '+' : ''})
</h2>
<DataGridRow>
{(col) => {
return (
<DataGridHeaderCell key={col.columnId}>
{col.renderHeaderCell(styles)}
</DataGridHeaderCell>
);
}}
</DataGridRow>
</DataGridHeader>
<DataGridBody<SearchResult>>
{({ item, rowId }) => {
return (
<DataGridHeaderCell key={col.columnId}>
{col.renderHeaderCell(styles)}
</DataGridHeaderCell>
<DataGridRow
key={rowId}
style={{
paddingTop: '1em',
paddingBottom: '1em',
}}
>
{(col) => (
<DataGridCell>
{col.renderCell({
...item,
classes: styles,
})}
</DataGridCell>
)}
</DataGridRow>
);
}}
</DataGridRow>
</DataGridHeader>
<DataGridBody<SearchResult>>
{({ item, rowId }) => {
return (
<DataGridRow
key={rowId}
style={{
paddingTop: '1em',
paddingBottom: '1em',
}}
>
{(col) => (
<DataGridCell>
{col.renderCell({
...item,
classes: styles,
})}
</DataGridCell>
)}
</DataGridRow>
);
}}
</DataGridBody>
</DataGrid>
</DataGridBody>
</DataGrid>
</div>

<div className={styles.mobileGrid}>
<h2>
Results ({searchResults.length}
{resultsTruncated ? '+' : ''})
</h2>
{searchResults.map((item, i) => (
<div key={i} className={styles.mobileRow}>
<div className={styles.labelValuePair}>
<span className={styles.label}>name</span>
<Path namespace={item.namespace} name={item.name} />
</div>
<div className={styles.labelValuePair}>
<span className={styles.label}>features</span>
<FeatureList features={item.features} />
</div>
</div>
))}
</div>

<p
className={
Expand Down
Loading

0 comments on commit 9911fee

Please sign in to comment.