Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: simplify module filters and api endpoints #28

Merged
merged 1 commit into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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