diff --git a/webui/src/app/index.tsx b/webui/src/app/index.tsx index b3ab52c..2050ff8 100644 --- a/webui/src/app/index.tsx +++ b/webui/src/app/index.tsx @@ -1,75 +1,9 @@ -import { useQuery } from '@tanstack/react-query'; +import { Redirect } from 'expo-router'; -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'; -import { useStatsEntryContext } from '~/providers/stats'; -import { Tag } from '~/ui/Tag'; -import { fetchApi } from '~/utils/api'; -import { formatFileSize } from '~/utils/formatString'; +import { useStatsEntry } from '~/providers/stats'; -export default function GraphScreen() { - const { entryId } = useStatsEntryContext(); - const { filters } = useModuleFilterContext(); +export default function HomeScreen() { + const { entry } = useStatsEntry(); - const graph = useBundleGraphData(entryId, filters); - - return ( - -
- - -

Bundle

- {!!graph.data && } -
- -
- -
-
- ); -} - -function BundleSummary({ data }: { data: EntryGraphData }) { - return ( -
- - - - {data.metadata.modulesCount} modules - - - {formatFileSize(data.metadata.size)} - {data.metadata.modulesCount !== data.data.modulesCount && ( -
- - visible: - {data.data.modulesCount} modules - - - {formatFileSize(data.data.size)} -
- )} -
- ); -} - -/** Load the bundle graph data from API, with default or custom filters */ -function useBundleGraphData(entryId: string, filters?: ModuleFilters) { - return useQuery({ - 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`; - - return fetchApi(url) - .then((res) => (res.ok ? res : Promise.reject(res))) - .then((res) => res.json()); - }, - }); + return ; } diff --git a/webui/src/app/folders/[path].tsx b/webui/src/app/stats/[entry]/folders/[path].tsx similarity index 86% rename from webui/src/app/folders/[path].tsx rename to webui/src/app/stats/[entry]/folders/[path].tsx index 332cded..4d57b56 100644 --- a/webui/src/app/folders/[path].tsx +++ b/webui/src/app/stats/[entry]/folders/[path].tsx @@ -1,21 +1,21 @@ import { useQuery } from '@tanstack/react-query'; import { useLocalSearchParams } from 'expo-router'; -import { type FolderGraphData } from '../api/stats/[entry]/folders/index+api'; - +import { type FolderGraphData } from '~/app/api/stats/[entry]/folders/index+api'; import { Page, PageHeader, PageTitle } from '~/components/Page'; import { TreemapGraph } from '~/components/graphs/TreemapGraph'; -import { useStatsEntryContext } from '~/providers/stats'; +import { useStatsEntry } from '~/providers/stats'; import { Skeleton } from '~/ui/Skeleton'; import { Tag } from '~/ui/Tag'; import { fetchApi } from '~/utils/api'; import { formatFileSize } from '~/utils/formatString'; +import { relativeEntryPath } from '~/utils/stats'; import { type PartialStatsEntry } from '~core/data/types'; export default function FolderPage() { - const { entryId, entry, entryFilePath } = useStatsEntryContext(); + const { entry } = useStatsEntry(); const { path: absolutePath } = useLocalSearchParams<{ path: string }>(); - const folder = useFolderData(entryId, absolutePath!); + const folder = useFolderData(entry.id, absolutePath!); if (folder.isLoading) { return ; @@ -39,15 +39,15 @@ export default function FolderPage() { className="text-slate-50 font-bold text-lg mr-4" title={folder.data.metadata.folderPath} > - {entryFilePath(folder.data.metadata.folderPath)}/ + {relativeEntryPath(entry, folder.data.metadata.folderPath)}/ diff --git a/webui/src/app/stats/[entry]/index.tsx b/webui/src/app/stats/[entry]/index.tsx new file mode 100644 index 0000000..c811128 --- /dev/null +++ b/webui/src/app/stats/[entry]/index.tsx @@ -0,0 +1,75 @@ +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'; +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 graph = useBundleGraphData(entry.id, filters); + + return ( + +
+ + +

Bundle

+ {!!graph.data && } +
+ +
+ +
+
+ ); +} + +function BundleSummary({ data }: { data: EntryGraphData }) { + return ( +
+ + - + {data.metadata.modulesCount} modules + - + {formatFileSize(data.metadata.size)} + {data.metadata.modulesCount !== data.data.modulesCount && ( +
+ + visible: + {data.data.modulesCount} modules + - + {formatFileSize(data.data.size)} +
+ )} +
+ ); +} + +/** Load the bundle graph data from API, with default or custom filters */ +function useBundleGraphData(entryId: string, filters?: ModuleFilters) { + return useQuery({ + 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`; + + return fetchApi(url) + .then((res) => (res.ok ? res : Promise.reject(res))) + .then((res) => res.json()); + }, + }); +} diff --git a/webui/src/app/modules/[path].tsx b/webui/src/app/stats/[entry]/modules/[path].tsx similarity index 89% rename from webui/src/app/modules/[path].tsx rename to webui/src/app/stats/[entry]/modules/[path].tsx index 5e54d94..2755d6c 100644 --- a/webui/src/app/modules/[path].tsx +++ b/webui/src/app/stats/[entry]/modules/[path].tsx @@ -2,18 +2,19 @@ import { useQuery } from '@tanstack/react-query'; import { Link, useLocalSearchParams } from 'expo-router'; import { Page, PageHeader, PageTitle } from '~/components/Page'; -import { useStatsEntryContext } from '~/providers/stats'; +import { useStatsEntry } from '~/providers/stats'; import { CodeBlock, CodeBlockSectionWithPrettier, guessLanguageFromPath } from '~/ui/CodeBlock'; import { Skeleton } from '~/ui/Skeleton'; import { Tag } from '~/ui/Tag'; import { fetchApi } from '~/utils/api'; import { formatFileSize } from '~/utils/formatString'; +import { relativeEntryPath } from '~/utils/stats'; import { type PartialStatsEntry, type StatsModule } from '~core/data/types'; export default function ModulePage() { - const { entryId, entry, entryFilePath } = useStatsEntryContext(); + const { entry } = useStatsEntry(); const { path: absolutePath } = useLocalSearchParams<{ path: string }>(); - const module = useModuleData(entryId, absolutePath!); + const module = useModuleData(entry.id, absolutePath!); const outputCode = module.data?.output?.map((output) => output.data.code).join('\n'); @@ -35,7 +36,7 @@ export default function ModulePage() {

- {entryFilePath(module.data.path)} + {relativeEntryPath(entry, module.data.path)}

@@ -50,9 +51,12 @@ export default function ModulePage() {
  • - {entryFilePath(path)} + {relativeEntryPath(entry, path)}
  • ))} diff --git a/webui/src/components/forms/StatsEntrySelect.tsx b/webui/src/components/forms/StatsEntrySelect.tsx index 01fb94a..09c3327 100644 --- a/webui/src/components/forms/StatsEntrySelect.tsx +++ b/webui/src/components/forms/StatsEntrySelect.tsx @@ -1,26 +1,25 @@ import * as Select from '@radix-ui/react-select'; import cn from 'classnames'; +import { useRouter } from 'expo-router'; // @ts-expect-error import ChevronDownIcon from 'lucide-react/dist/esm/icons/chevron-down'; // @ts-expect-error import ChevronUpIcon from 'lucide-react/dist/esm/icons/chevron-up'; -import { useStatsEntryContext } from '~/providers/stats'; +import { useStatsEntry } from '~/providers/stats'; import { Button } from '~/ui/Button'; import { Tag } from '~/ui/Tag'; +import { relativeEntryPath } from '~/utils/stats'; export function StatsEntrySelect() { - const { entryId, setEntryId, entry, entries } = useStatsEntryContext(); - - function onEntryChange(value: string) { - setEntryId(value); - } + const router = useRouter(); + const { entry, entries } = useStatsEntry(); return ( - + router.setParams({ entry })}> diff --git a/webui/src/components/graphs/TreemapGraph.tsx b/webui/src/components/graphs/TreemapGraph.tsx index 28c4306..48ba088 100644 --- a/webui/src/components/graphs/TreemapGraph.tsx +++ b/webui/src/components/graphs/TreemapGraph.tsx @@ -1,10 +1,11 @@ import * as echarts from 'echarts'; import { useRouter } from 'expo-router'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { Graph } from './Graph'; import type { ModuleMetadata } from '~/app/api/stats/[entry]/modules/index+api'; +import { useStatsEntry } from '~/providers/stats'; import { formatFileSize } from '~/utils/formatString'; type TreemapGraphProps = { @@ -18,16 +19,23 @@ const ICON_STRINGS = { pkg: ``, }; -export function TreemapGraph(props: TreemapGraphProps) { +function useInspectCallback() { const router = useRouter(); + const { entry } = useStatsEntry(); + + return useCallback( + (type: 'folder' | 'module', path: string) => { + router.push({ + pathname: + type === 'module' ? '/stats/[entry]/modules/[path]' : '/stats/[entry]/folders/[path]', + params: { entry: entry.id, path }, + }); + }, + [entry.id] + ); +} - function onInspectPath(type: 'folder' | 'module', absolutePath: string) { - router.push({ - pathname: type === 'module' ? '/modules/[path]' : '/folders/[path]', - params: { path: absolutePath }, - }); - } - +export function TreemapGraph(props: TreemapGraphProps) { const { data, maxDepth, maxNodeModules } = useMemo( () => createModuleTree(props.modules.filter((module) => module.path.startsWith('/'))), [props.modules] @@ -55,6 +63,8 @@ export function TreemapGraph(props: TreemapGraphProps) { ...getLabelObj({ multiLevel: false }), }; + const onInspectPath = useInspectCallback(); + return ( void; - entries?: ReturnType; - entry?: PartialStatsEntry; - entryFilePath: (absolutePath: string) => string; + entries: NonNullable['data']>; }; export const statsEntryContext = createContext({ - entryId: '2', - setEntryId: () => {}, - entries: undefined, - entry: undefined, - entryFilePath: (absolutePath) => absolutePath, + entries: [], }); -export const useStatsEntryContext = () => useContext(statsEntryContext); +export const useStatsEntry = () => { + const { entries } = useContext(statsEntryContext); + const { entry: entryId } = useLocalSearchParams<{ entry?: string }>(); + const entry = useMemo( + () => entries.find((entry) => entry.id === entryId) || entries[0], + [entries, entryId] + ); + + return { entry, entries }; +}; export function StatsEntryProvider({ children }: PropsWithChildren) { const entries = useStatsEntriesData(); - const [entryId, setEntryId] = useState(); - const entryIdOrFirstEntry = entryId ?? entries.data?.[0]?.id; - const entry = useMemo( - () => entries.data?.find((entry) => entry.id === entryIdOrFirstEntry), - [entries, entryIdOrFirstEntry] - ); - - function entryFilePath(absolutePath: string) { - return entry?.projectRoot ? absolutePath.replace(entry.projectRoot + '/', '') : absolutePath; + if (entries.data?.length) { + return ( + + {children} + + ); } // TODO: add better UX for loading @@ -57,21 +56,11 @@ export function StatsEntryProvider({ children }: PropsWithChildren) { } // TODO: add better UX for error state - if (!entryIdOrFirstEntry) { - return ( -
    -

    Unable to load stats.

    -

    Make sure you configured Expo Atlas properly.

    -
    - ); - } - return ( - - {children} - +
    +

    No stats source.

    +

    Try restarting Expo Atlas. If this error keeps happening, open a bug report.

    +
    ); } diff --git a/webui/src/utils/stats.ts b/webui/src/utils/stats.ts new file mode 100644 index 0000000..d9ef516 --- /dev/null +++ b/webui/src/utils/stats.ts @@ -0,0 +1,9 @@ +import { PartialStatsEntry } from '~core/data/types'; + +/** + * Translate an absolute path to a relative path, based on the entry's project root. + * This is a simple replace operation. + */ +export function relativeEntryPath(entry: Pick, path: string) { + return path.replace(entry.projectRoot + '/', ''); +}