Skip to content

Commit

Permalink
refactor: move filters from context to url (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
byCedric authored Mar 31, 2024
1 parent f61620d commit 9e4847e
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 139 deletions.
File renamed without changes.
2 changes: 1 addition & 1 deletion webui/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ config.cacheStores = [
// Initialize the Expo Atlas global data source in development
if (process.env.NODE_ENV === 'development') {
const { StatsFileSource } = require('../build/src/data/StatsFileSource');
const statsFile = path.resolve(__dirname, './fixture/stats-tabs-50.jsonl');
const statsFile = path.resolve(__dirname, './fixture/atlas-tabs-50.jsonl');

global.EXPO_ATLAS_SOURCE = new StatsFileSource(statsFile);
}
Expand Down
5 changes: 1 addition & 4 deletions webui/src/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Slot } from 'expo-router';

import { ModuleFilterProvider } from '~/providers/modules';
import { QueryProvider } from '~/providers/query';
import { StatsEntryProvider } from '~/providers/stats';
import { ThemeProvider } from '~/providers/theme';
Expand Down Expand Up @@ -34,9 +33,7 @@ export default function RootLayout() {
<QueryProvider>
<ThemeProvider>
<StatsEntryProvider>
<ModuleFilterProvider>
<Slot />
</ModuleFilterProvider>
<Slot />
</StatsEntryProvider>
</ThemeProvider>
</QueryProvider>
Expand Down
8 changes: 4 additions & 4 deletions webui/src/app/api/stats/[entry]/modules/index+api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { filtersFromUrlParams } from '~/providers/modules';
import { statsModuleFiltersFromUrlParams } from '~/components/forms/StatsModuleFilter';
import { getSource } from '~/utils/atlas';
import { type StatsEntry, type StatsModule } from '~core/data/types';
import { fuzzyFilterModules } from '~core/utils/search';
Expand Down Expand Up @@ -55,14 +55,14 @@ export async function GET(request: Request, params: Record<'entry', string>) {
* - `exclude=<glob>` to only exclude specific glob patterns
*/
function filterModules(request: Request, stats: StatsEntry): ModuleMetadata[] {
const { types, ...patterns } = filtersFromUrlParams(new URL(request.url).searchParams);
const filters = statsModuleFiltersFromUrlParams(new URL(request.url).searchParams);
let modules = Array.from(stats.modules.values());

if (!types.includes('node_modules')) {
if (!filters.modules.includes('node_modules')) {
modules = modules.filter((module) => !module.package);
}

return fuzzyFilterModules(modules, patterns).map((module) => ({
return fuzzyFilterModules(modules, filters).map((module) => ({
...module,
source: undefined,
output: undefined,
Expand Down
16 changes: 8 additions & 8 deletions webui/src/app/stats/[entry]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ import { useQuery } from '@tanstack/react-query';

import type { EntryGraphData } from '~/app/api/stats/[entry]/modules/index+api';
import { Page, PageHeader, PageTitle } from '~/components/Page';
import { StatsModuleFilter } from '~/components/forms/StatsModuleFilter';
import { TreemapGraph } from '~/components/graphs/TreemapGraph';
import {
type ModuleFilters,
useModuleFilterContext,
filtersToUrlParams,
} from '~/providers/modules';
StatsModuleFilter,
statsModuleFiltersToUrlParams,
useStatsModuleFilters,
} from '~/components/forms/StatsModuleFilter';
import { TreemapGraph } from '~/components/graphs/TreemapGraph';
import { useStatsEntry } from '~/providers/stats';
import { Tag } from '~/ui/Tag';
import { fetchApi } from '~/utils/api';
import { formatFileSize } from '~/utils/formatString';

export default function StatsScreen() {
const { entry } = useStatsEntry();
const { filters } = useModuleFilterContext();
const filters = useStatsModuleFilters();

const graph = useBundleGraphData(entry.id, filters);

Expand Down Expand Up @@ -58,13 +58,13 @@ function BundleSummary({ data }: { data: EntryGraphData }) {
}

/** Load the bundle graph data from API, with default or custom filters */
function useBundleGraphData(entryId: string, filters?: ModuleFilters) {
function useBundleGraphData(entryId: string, filters: ModuleFilters) {
return useQuery<EntryGraphData>({
queryKey: [`bundle-graph`, entryId, filters],
queryFn: ({ queryKey }) => {
const [_key, entry, filters] = queryKey as [string, string, ModuleFilters | undefined];
const url = filters
? `/api/stats/${entry}/modules?${filtersToUrlParams(filters)}`
? `/api/stats/${entry}/modules?${statsModuleFiltersToUrlParams(filters)}`
: `/api/stats/${entry}/modules`;

return fetchApi(url)
Expand Down
113 changes: 70 additions & 43 deletions webui/src/components/forms/StatsModuleFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type FormEvent, type KeyboardEvent, useState } from 'react';
import { useGlobalSearchParams, useRouter } from 'expo-router';
import { type FormEvent, type KeyboardEvent, useState, useCallback } from 'react';

import { useModuleFilterContext, useModuleFilterReducer } from '~/providers/modules';
import { Button } from '~/ui/Button';
import { Checkbox } from '~/ui/Checkbox';
import { Input } from '~/ui/Input';
Expand All @@ -13,40 +13,67 @@ import {
SheetTitle,
SheetTrigger,
} from '~/ui/Sheet';
import { debounce } from '~/utils/debounce';

export type ModuleFilters = typeof DEFAULT_FILTERS;

const DEFAULT_FILTERS = {
modules: 'project,node_modules',
include: '',
exclude: '',
};

export function useStatsModuleFilters(): ModuleFilters {
const filters = useGlobalSearchParams<Partial<ModuleFilters>>();
return {
modules: filters.modules || DEFAULT_FILTERS.modules,
include: filters.include || DEFAULT_FILTERS.include,
exclude: filters.exclude || DEFAULT_FILTERS.exclude,
};
}

export function StatsModuleFilter() {
const { filters, setFilters } = useModuleFilterContext();
// NOTE(cedric): keep a duplicate data store to avoid spamming the API with every change
const [formData, setFormData] = useModuleFilterReducer(filters);
const router = useRouter();
const filters = useStatsModuleFilters();

// NOTE(cedric): we want to programmatically close the dialog when the form is submitted, so make it controlled
const [dialogOpen, setDialogOpen] = useState(false);

function onFormSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
event.stopPropagation();

setDialogOpen(false);
setFilters(formData);
}

function onInputEnter(
event: KeyboardEvent<HTMLInputElement>,
data: (data: typeof formData) => typeof formData
) {
function onInputEnter(event: KeyboardEvent<HTMLInputElement>) {
if (event.key === 'Enter') {
event.preventDefault();
setDialogOpen(false);
setFilters(data(formData));
}
}

function onDialogChange(isOpen: boolean) {
setDialogOpen(isOpen);
if (!isOpen) setFormData(filters);
}
const onModuleChange = useCallback((includeNodeModules: boolean) => {
router.setParams({
modules: includeNodeModules ? undefined : 'project',
});
}, []);

const onIncludeChange = useCallback(
debounce((value: string) => {
router.setParams({ include: value || undefined });
}, 300),
[]
);

const onExcludeChange = useCallback(
debounce((value: string) => {
router.setParams({ include: value || undefined });
}, 300),
[]
);

return (
<Sheet open={dialogOpen} onOpenChange={onDialogChange}>
<Sheet open={dialogOpen} onOpenChange={setDialogOpen}>
<SheetTrigger asChild>
<Button variant="secondary">Filter</Button>
</SheetTrigger>
Expand All @@ -65,13 +92,9 @@ export function StatsModuleFilter() {
</Label>
<Checkbox
id="filter-node_modules"
checked={formData.types.includes('node_modules')}
name=""
onCheckedChange={(isChecked) => {
setFormData(
isChecked ? { types: ['project', 'node_modules'] } : { types: ['project'] }
);
}}
defaultChecked={filters.modules.includes('node_modules')}
name="filterNodeModules"
onCheckedChange={onModuleChange}
/>
</fieldset>

Expand All @@ -85,11 +108,9 @@ export function StatsModuleFilter() {
type="text"
className="mt-2"
placeholder="e.g. app/**/*.{ts}"
value={formData.include}
onChange={(event) => setFormData({ include: event.currentTarget.value })}
onKeyDown={(event) =>
onInputEnter(event, (data) => ({ ...data, include: event.currentTarget.value }))
}
defaultValue={filters.include}
onChange={(event) => onIncludeChange(event.currentTarget.value)}
onKeyDown={onInputEnter}
/>
</fieldset>

Expand All @@ -103,24 +124,30 @@ export function StatsModuleFilter() {
type="text"
className="mt-2"
placeholder="e.g. react-native/**"
value={formData.exclude}
onChange={(event) => setFormData({ exclude: event.currentTarget.value })}
onKeyDown={(event) =>
onInputEnter(event, (data) => ({ ...data, exclude: event.currentTarget.value }))
}
defaultValue={filters.exclude}
onChange={(event) => onExcludeChange(event.currentTarget.value)}
onKeyDown={onInputEnter}
/>
</fieldset>

<div className="mt-[25px] flex justify-between">
<Button variant="quaternary" onClick={() => onDialogChange(false)}>
Cancel
</Button>
<Button variant="primary" type="submit">
Apply filters
</Button>
</div>
</form>
</SheetContent>
</Sheet>
);
}

export function statsModuleFiltersToUrlParams(filters: ModuleFilters) {
const params = new URLSearchParams({ modules: filters.modules });

if (filters.include) params.set('include', filters.include);
if (filters.exclude) params.set('exclude', filters.exclude);

return params.toString();
}

export function statsModuleFiltersFromUrlParams(params: URLSearchParams): ModuleFilters {
return {
modules: params.get('modules') || DEFAULT_FILTERS.modules,
include: params.get('include') || DEFAULT_FILTERS.include,
exclude: params.get('exclude') || DEFAULT_FILTERS.exclude,
};
}
79 changes: 0 additions & 79 deletions webui/src/providers/modules.tsx

This file was deleted.

31 changes: 31 additions & 0 deletions webui/src/utils/debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
type Debounced<T extends (...args: any) => any> = {
/** The debounced function, without any return value */
(...args: Parameters<T>): void;
/** Cancel the debounced timer and prevent the function being called */
cancel(): void;
};

/**
* Create a function that which is called after some delay.
* The delay timer is reset every time the function is called.
* It's possible to fully cancel the function by using `debounce(...).cancel()`.
*/
export function debounce<T extends (...args: any) => any>(action: T, delay = 500): Debounced<T> {
let timerId: ReturnType<typeof setTimeout> | null = null;

function cancel() {
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
}

const debounced = (...args: any[]) => {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => action(...args), delay);
};

debounced.cancel = cancel;

return debounced;
}

0 comments on commit 9e4847e

Please sign in to comment.