Skip to content

Commit

Permalink
refactor: simplify module filters and api endpoints (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
byCedric authored Apr 5, 2024
1 parent 93a3327 commit 81f7c3a
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 264 deletions.
46 changes: 12 additions & 34 deletions webui/src/app/api/stats/[entry]/modules/graph+api.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { statsModuleFiltersFromUrlParams } from '~/components/forms/StatsModuleFilter';
import { getSource } from '~/utils/atlas';
import { globFilterModules } from '~/utils/search';
import { filterModules, moduleFiltersFromParams } from '~/utils/filters';
import { type TreemapNode, createModuleTree, finalizeModuleTree } from '~/utils/treemap';
import type { StatsEntry, StatsModule } from '~core/data/types';
import type { StatsEntry } from '~core/data/types';

export type ModuleGraphResponse = {
data: TreemapNode;
Expand All @@ -26,9 +25,15 @@ export async function GET(request: Request, params: Record<'entry', string>) {
return Response.json({ error: error.message }, { status: 406 });
}

const query = new URL(request.url).searchParams;
const allModules = Array.from(entry.modules.values());
const modules = modulesMatchingFilters(request, entry, allModules);
const tree = createModuleTree(modules);
const filteredModules = filterModules(allModules, {
projectRoot: entry.projectRoot,
filters: moduleFiltersFromParams(query),
rootPath: query.get('path') || undefined,
});

const tree = createModuleTree(filteredModules);

const response: ModuleGraphResponse = {
data: finalizeModuleTree(tree),
Expand All @@ -38,37 +43,10 @@ export async function GET(request: Request, params: Record<'entry', string>) {
moduleFiles: entry.modules.size,
},
filtered: {
moduleSize: modules.reduce((size, module) => size + module.size, 0),
moduleFiles: modules.length,
moduleSize: filteredModules.reduce((size, module) => size + module.size, 0),
moduleFiles: filteredModules.length,
},
};

return Response.json(response);
}

/**
* Get and filter the modules from the stats entry based on query parameters.
* - `modules=project,node_modules` to show only project code and/or node_modules
* - `include=<glob>` to only include specific glob patterns
* - `exclude=<glob>` to only exclude specific glob patterns
* - `path=<folder>` to only show modules in a specific folder
*/
function modulesMatchingFilters(
request: Request,
entry: StatsEntry,
modules: StatsModule[]
): StatsModule[] {
const searchParams = new URL(request.url).searchParams;

const folderRef = searchParams.get('path');
if (folderRef) {
modules = modules.filter((module) => module.path.startsWith(folderRef));
}

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

return globFilterModules(modules, entry.projectRoot, filters);
}
49 changes: 16 additions & 33 deletions webui/src/app/api/stats/[entry]/modules/index+api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { statsModuleFiltersFromUrlParams } from '~/components/forms/StatsModuleFilter';
import { getSource } from '~/utils/atlas';
import { globFilterModules } from '~/utils/search';
import { filterModules, moduleFiltersFromParams } from '~/utils/filters';
import { type StatsEntry, type StatsModule } from '~core/data/types';

/** The partial module data, when listing all available modules from a stats entry */
Expand All @@ -19,6 +18,7 @@ export type ModuleListResponse = {
};
};

/** Get all modules as simple list */
export async function GET(request: Request, params: Record<'entry', string>) {
let entry: StatsEntry;

Expand All @@ -28,54 +28,37 @@ export async function GET(request: Request, params: Record<'entry', string>) {
return Response.json({ error: error.message }, { status: 406 });
}

const query = new URL(request.url).searchParams;
const allModules = Array.from(entry.modules.values());
const modules = modulesMatchingFilters(request, entry, allModules);
const filteredModules = filterModules(allModules, {
projectRoot: entry.projectRoot,
filters: moduleFiltersFromParams(query),
rootPath: query.get('path') || undefined,
});

const response: ModuleListResponse = {
data: modules,
data: filteredModules.map((module) => ({
...module,
source: undefined,
output: undefined,
})),
entry: {
platform: entry.platform as any,
moduleSize: allModules.reduce((size, module) => size + module.size, 0),
moduleFiles: entry.modules.size,
},
filtered: {
moduleSize: modules.reduce((size, module) => size + module.size, 0),
moduleFiles: modules.length,
moduleSize: filteredModules.reduce((size, module) => size + module.size, 0),
moduleFiles: filteredModules.length,
},
};

return Response.json(response);
}

/**
* Get and filter the modules from the stats entry based on query parameters.
* - `modules=project,node_modules` to show only project code and/or node_modules
* - `include=<glob>` to only include specific glob patterns
* - `exclude=<glob>` to only exclude specific glob patterns
* - `path=<folder>` to only show modules in a specific folder
*/
function modulesMatchingFilters(
request: Request,
entry: StatsEntry,
modules: StatsModule[]
): StatsModule[] {
const searchParams = new URL(request.url).searchParams;

const folderRef = searchParams.get('path');
if (folderRef) {
modules = modules.filter((module) => module.path.startsWith(folderRef));
}

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

return globFilterModules(modules, entry.projectRoot, filters);
}

/**
* Get the full module information through a post request.
* This requires a `path` property in the request body.
*/
export async function POST(request: Request, params: Record<'entry', string>) {
const moduleRef: string | undefined = (await request.json()).path;
Expand Down
12 changes: 4 additions & 8 deletions webui/src/app/stats/[entry]/folders/[path].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,18 @@ import { useLocalSearchParams } from 'expo-router';
import type { ModuleGraphResponse } from '~/app/api/stats/[entry]/modules/graph+api';
import { BundleGraph } from '~/components/BundleGraph';
import { Page, PageHeader, PageTitle } from '~/components/Page';
import {
ModuleFilters,
StatsModuleFilter,
statsModuleFiltersToUrlParams,
useStatsModuleFilters,
} from '~/components/forms/StatsModuleFilter';
import { StatsModuleFilter } from '~/components/forms/StatsModuleFilter';
import { useStatsEntry } from '~/providers/stats';
import { Tag } from '~/ui/Tag';
import { fetchApi } from '~/utils/api';
import { type ModuleFilters, useModuleFilters, moduleFiltersToParams } from '~/utils/filters';
import { formatFileSize } from '~/utils/formatString';
import { relativeEntryPath } from '~/utils/stats';

export default function FolderPage() {
const { path: absolutePath } = useLocalSearchParams<{ path: string }>();
const { entry } = useStatsEntry();
const { filters, filtersEnabled } = useStatsModuleFilters();
const { filters, filtersEnabled } = useModuleFilters();
const modules = useModuleGraphDataInFolder(entry.id, absolutePath!, filters);
const treeHasData = !!modules.data?.data?.children?.length;

Expand Down Expand Up @@ -91,7 +87,7 @@ function useModuleGraphDataInFolder(entryId: string, path: string, filters: Modu
ModuleFilters | undefined,
];
const url = filters
? `/api/stats/${entry}/modules/graph?path=${encodeURIComponent(path)}&${statsModuleFiltersToUrlParams(filters)}`
? `/api/stats/${entry}/modules/graph?path=${encodeURIComponent(path)}&${moduleFiltersToParams(filters)}`
: `/api/stats/${entry}/modules/graph?path=${encodeURIComponent(path)}`;

return fetchApi(url)
Expand Down
12 changes: 4 additions & 8 deletions webui/src/app/stats/[entry]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,17 @@ import { keepPreviousData, useQuery } from '@tanstack/react-query';
import type { ModuleGraphResponse } from '~/app/api/stats/[entry]/modules/graph+api';
import { BundleGraph } from '~/components/BundleGraph';
import { Page, PageHeader, PageTitle } from '~/components/Page';
import {
type ModuleFilters,
StatsModuleFilter,
statsModuleFiltersToUrlParams,
useStatsModuleFilters,
} from '~/components/forms/StatsModuleFilter';
import { StatsModuleFilter } from '~/components/forms/StatsModuleFilter';
import { useStatsEntry } from '~/providers/stats';
import { Spinner } from '~/ui/Spinner';
import { Tag } from '~/ui/Tag';
import { fetchApi } from '~/utils/api';
import { type ModuleFilters, moduleFiltersToParams, useModuleFilters } from '~/utils/filters';
import { formatFileSize } from '~/utils/formatString';

export default function StatsPage() {
const { entry } = useStatsEntry();
const { filters, filtersEnabled } = useStatsModuleFilters();
const { filters, filtersEnabled } = useModuleFilters();
const modules = useModuleGraphData(entry.id, filters);
const treeHasData = !!modules.data?.data?.children?.length;

Expand Down Expand Up @@ -77,7 +73,7 @@ function useModuleGraphData(entryId: string, filters: ModuleFilters) {
queryFn: ({ queryKey }) => {
const [_key, entry, filters] = queryKey as [string, string, ModuleFilters | undefined];
const url = filters
? `/api/stats/${entry}/modules/graph?${statsModuleFiltersToUrlParams(filters)}`
? `/api/stats/${entry}/modules/graph?${moduleFiltersToParams(filters)}`
: `/api/stats/${entry}/modules/graph`;

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

import { Button } from '~/ui/Button';
Expand All @@ -14,34 +14,15 @@ import {
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(): { filters: ModuleFilters; filtersEnabled: boolean } {
const filters = useGlobalSearchParams<Partial<ModuleFilters>>();
return {
filtersEnabled: !!filters.modules || !!filters.include || !!filters.exclude,
filters: {
modules: filters.modules || DEFAULT_FILTERS.modules,
include: filters.include || DEFAULT_FILTERS.include,
exclude: filters.exclude || DEFAULT_FILTERS.exclude,
},
};
}
import { useModuleFilters } from '~/utils/filters';

type StatsModuleFilterProps = {
disableNodeModules?: boolean;
};

export function StatsModuleFilter(props: StatsModuleFilterProps) {
const router = useRouter();
const { filters, filtersEnabled } = useStatsModuleFilters();
const { filters, filtersEnabled } = useModuleFilters();

// NOTE(cedric): we want to programmatically close the dialog when the form is submitted, so make it controlled
const [dialogOpen, setDialogOpen] = useState(false);
Expand All @@ -59,9 +40,9 @@ export function StatsModuleFilter(props: StatsModuleFilterProps) {
}
}

const onModuleChange = useCallback((includeNodeModules: boolean) => {
const onModuleChange = useCallback((withNodeModules: boolean) => {
router.setParams({
modules: includeNodeModules ? undefined : 'project',
scope: withNodeModules ? undefined : 'project',
});
}, []);

Expand All @@ -82,7 +63,7 @@ export function StatsModuleFilter(props: StatsModuleFilterProps) {
const onClearFilters = useCallback(() => {
setDialogOpen(false);
router.setParams({
modules: undefined,
scope: undefined,
include: undefined,
exclude: undefined,
});
Expand All @@ -108,7 +89,7 @@ export function StatsModuleFilter(props: StatsModuleFilterProps) {
</Label>
<Checkbox
id="filter-node_modules"
defaultChecked={filters.modules.includes('node_modules')}
defaultChecked={filters.scope !== 'project'}
name="filterNodeModules"
onCheckedChange={onModuleChange}
disabled={props.disableNodeModules}
Expand Down Expand Up @@ -166,20 +147,3 @@ export function StatsModuleFilter(props: StatsModuleFilterProps) {
</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,
};
}
Loading

0 comments on commit 81f7c3a

Please sign in to comment.