From 8861987161dbc3be09b98a9ec4ed8878a1ad5790 Mon Sep 17 00:00:00 2001 From: Cedric van Putten Date: Wed, 28 Aug 2024 17:25:18 +0200 Subject: [PATCH 1/3] refactor(webui): drop legacy bundle sorting from api route --- src/data/types.ts | 2 +- webui/src/app/--/bundles/index+api.ts | 9 +-------- webui/src/components/BundleTag.tsx | 6 ++---- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/data/types.ts b/src/data/types.ts index b82b692..6e46f24 100644 --- a/src/data/types.ts +++ b/src/data/types.ts @@ -19,7 +19,7 @@ export type AtlasBundle = { /** The unique reference or ID to this entry */ id: string; /** The platform for which the bundle was created */ - platform: 'android' | 'ios' | 'web' | 'server' | 'unknown'; + platform: 'android' | 'ios' | 'web' | 'unknown'; /** The environment this bundle is compiled for */ environment: 'client' | 'node' | 'react-server'; /** The absolute path to the root of the project */ diff --git a/webui/src/app/--/bundles/index+api.ts b/webui/src/app/--/bundles/index+api.ts index 78254b8..3aef289 100644 --- a/webui/src/app/--/bundles/index+api.ts +++ b/webui/src/app/--/bundles/index+api.ts @@ -1,9 +1,8 @@ import { getSource } from '~/utils/atlas'; -import type { PartialAtlasBundle } from '~core/data/types'; export async function GET() { try { - const bundles = (await getSource().listBundles()).sort(sortBundlesByPlatform); + const bundles = await getSource().listBundles(); const bundlesWithRelativeEntry = bundles.map((bundle) => ({ ...bundle, // TODO(cedric): this is a temporary workaround to make entry points look better on Windows @@ -19,9 +18,3 @@ export async function GET() { return Response.json({ error: error.message }, { status: 406 }); } } - -function sortBundlesByPlatform(a: PartialAtlasBundle, b: PartialAtlasBundle) { - if (a.platform === 'server') return 1; - if (b.platform === 'server') return -1; - return 0; -} diff --git a/webui/src/components/BundleTag.tsx b/webui/src/components/BundleTag.tsx index 0b389f6..cde692d 100644 --- a/webui/src/components/BundleTag.tsx +++ b/webui/src/components/BundleTag.tsx @@ -10,14 +10,13 @@ const bundleTagVariants = cva('', { android: 'bg-palette-green3 text-palette-green11', ios: 'bg-palette-blue3 text-palette-blue11', web: 'bg-palette-orange3 text-palette-orange11', - server: 'bg-palette-orange3 text-palette-orange11', unknown: '', - }, + } satisfies typeof platformChildren, environment: { client: '', node: 'bg-palette-orange3 text-palette-orange11', 'react-server': 'bg-palette-green3 text-palette-green11', - }, + } satisfies typeof environmentChildren, }, defaultVariants: { platform: 'unknown', // Default platform value, see MetroGraphSource @@ -28,7 +27,6 @@ const bundleTagVariants = cva('', { const platformChildren: Record = { android: 'Android', ios: 'iOS', - server: 'Server', web: 'Web', unknown: '???', }; From b20ec4889d06f44fde643798bce7771d519f485c Mon Sep 17 00:00:00 2001 From: Cedric van Putten Date: Thu, 29 Aug 2024 01:05:54 +0200 Subject: [PATCH 2/3] refactor(webui): group bundle selection by platform and introduce environment icons --- webui/src/components/BundleSelectForm.tsx | 127 +++++++++++------- webui/src/components/BundleTag.tsx | 52 +++----- webui/src/components/EnvironmentIcon.tsx | 21 +++ webui/src/components/EnvironmentName.tsx | 18 +++ webui/src/components/PlatformName.tsx | 38 ++++++ webui/src/ui/Select.tsx | 154 ++++++++++++++++++++++ 6 files changed, 323 insertions(+), 87 deletions(-) create mode 100644 webui/src/components/EnvironmentIcon.tsx create mode 100644 webui/src/components/EnvironmentName.tsx create mode 100644 webui/src/components/PlatformName.tsx create mode 100644 webui/src/ui/Select.tsx diff --git a/webui/src/components/BundleSelectForm.tsx b/webui/src/components/BundleSelectForm.tsx index b23fdd0..2eeeec0 100644 --- a/webui/src/components/BundleSelectForm.tsx +++ b/webui/src/components/BundleSelectForm.tsx @@ -1,63 +1,88 @@ -import * as Select from '@radix-ui/react-select'; -import { cx } from 'class-variance-authority'; import { useRouter } from 'expo-router'; -// @ts-expect-error -import ChevronDownIcon from 'lucide-react/dist/esm/icons/chevron-down'; +import { useMemo } from 'react'; -import { BundleTag } from '~/components/BundleTag'; +import { EnvironmentIcon } from '~/components/EnvironmentIcon'; +import { PlatformName } from '~/components/PlatformName'; import { useBundle } from '~/providers/bundle'; -import { Button } from '~/ui/Button'; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from '~/ui/Select'; import { relativeBundlePath } from '~/utils/bundle'; +import type { PartialAtlasBundle } from '~core/data/types'; export function BundleSelectForm() { const router = useRouter(); const { bundle, bundles } = useBundle(); + const bundlesByPlatform = useMemo(() => groupBundlesByPlatform(bundles), [bundles]); return ( - router.setParams({ bundle })}> - - - - - - - {bundles.map((item) => ( -
- - - -
- ))} -
-
-
-
+ + ); +} + +function groupBundlesByPlatform(bundles: PartialAtlasBundle[]) { + const groups: Record = { + android: [], + ios: [], + web: [], + unknown: [], + }; + + for (const bundle of bundles) { + if (groups[bundle.platform]) { + groups[bundle.platform]!.push(bundle); + } + } + + return Object.entries(groups).map( + ([platform, bundles]) => + [platform as PartialAtlasBundle['platform'], bundles.sort(sortBundlesByEnvironment)] as const ); } + +/** Sort all bundles by environment, in alphabetical order "client -> node -> react-server" */ +function sortBundlesByEnvironment(a: PartialAtlasBundle, b: PartialAtlasBundle) { + return a.environment.localeCompare(b.environment); +} diff --git a/webui/src/components/BundleTag.tsx b/webui/src/components/BundleTag.tsx index cde692d..04c0797 100644 --- a/webui/src/components/BundleTag.tsx +++ b/webui/src/components/BundleTag.tsx @@ -1,22 +1,25 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { type ComponentProps } from 'react'; +import { EnvironmentIcon } from '~/components/EnvironmentIcon'; +import { EnvironmentName } from '~/components/EnvironmentName'; +import { PlatformName } from '~/components/PlatformName'; import { Tag } from '~/ui/Tag'; import type { AtlasBundle } from '~core/data/types'; const bundleTagVariants = cva('', { variants: { platform: { - android: 'bg-palette-green3 text-palette-green11', - ios: 'bg-palette-blue3 text-palette-blue11', - web: 'bg-palette-orange3 text-palette-orange11', + android: 'bg-palette-green3', + ios: 'bg-palette-blue3', + web: 'bg-palette-orange3', unknown: '', - } satisfies typeof platformChildren, + } satisfies Record, environment: { client: '', - node: 'bg-palette-orange3 text-palette-orange11', - 'react-server': 'bg-palette-green3 text-palette-green11', - } satisfies typeof environmentChildren, + node: 'bg-palette-orange3', + 'react-server': 'bg-palette-orange3', + } satisfies Record, }, defaultVariants: { platform: 'unknown', // Default platform value, see MetroGraphSource @@ -24,19 +27,6 @@ const bundleTagVariants = cva('', { }, }); -const platformChildren: Record = { - android: 'Android', - ios: 'iOS', - web: 'Web', - unknown: '???', -}; - -const environmentChildren: Record = { - client: 'Client', - node: 'SSR', - 'react-server': 'RSC', -}; - type BundelTagProps = Omit< ComponentProps & VariantProps, 'children' @@ -49,22 +39,12 @@ export function BundleTag({ className, platform, environment, ...props }: Bundel variant="none" {...props} > - {getBundelTagChildren({ platform, environment })} + + + × + + + ); } - -function getBundelTagChildren(props: BundelTagProps) { - const children: string[] = []; - - if (props.platform) { - children.push(platformChildren[props.platform]); - } - - // Only add the environment specifier if it's not bundled for the client - if (props.environment && props.environment !== 'client') { - children.push(environmentChildren[props.environment]); - } - - return children.join(' × '); -} diff --git a/webui/src/components/EnvironmentIcon.tsx b/webui/src/components/EnvironmentIcon.tsx new file mode 100644 index 0000000..7726be9 --- /dev/null +++ b/webui/src/components/EnvironmentIcon.tsx @@ -0,0 +1,21 @@ +import type { LucideProps } from 'lucide-react'; + +import type { AtlasBundle } from '~core/data/types'; + +type EnvironmentIconProps = Omit< + LucideProps & { + environment: AtlasBundle['environment']; + }, + 'children' +>; + +const iconsByEnvironment: Record = { + client: require('lucide-react/dist/esm/icons/tablet-smartphone').default, + node: require('lucide-react/dist/esm/icons/hexagon').default, + 'react-server': require('lucide-react/dist/esm/icons/server').default, +}; + +export function EnvironmentIcon({ className, environment, ...props }: EnvironmentIconProps) { + const Icon = iconsByEnvironment[environment]; + return ; +} diff --git a/webui/src/components/EnvironmentName.tsx b/webui/src/components/EnvironmentName.tsx new file mode 100644 index 0000000..d070576 --- /dev/null +++ b/webui/src/components/EnvironmentName.tsx @@ -0,0 +1,18 @@ +import type { PropsWithChildren } from 'react'; + +import type { AtlasBundle } from '~core/data/types'; + +type EnvironmentNameProps = PropsWithChildren<{ + environment: AtlasBundle['environment']; + className?: string; +}>; + +export const environmentNames: Record = { + client: 'Client', + node: 'SSR', + 'react-server': 'RSC', +}; + +export function EnvironmentName({ children, environment, ...props }: EnvironmentNameProps) { + return {children || environmentNames[environment]}; +} diff --git a/webui/src/components/PlatformName.tsx b/webui/src/components/PlatformName.tsx new file mode 100644 index 0000000..455d47f --- /dev/null +++ b/webui/src/components/PlatformName.tsx @@ -0,0 +1,38 @@ +import { cva } from 'class-variance-authority'; +import type { PropsWithChildren } from 'react'; + +import type { AtlasBundle } from '~core/data/types'; + +type PlatformNameProps = PropsWithChildren<{ + platform: AtlasBundle['platform']; + className?: string; +}>; + +export const platformVariants = cva('', { + variants: { + platform: { + android: 'text-palette-green11', + ios: 'text-palette-blue11', + web: 'text-palette-orange11', + unknown: 'text-secondary', + } satisfies Record, + }, + defaultVariants: { + platform: 'unknown', + }, +}); + +export const platformNames: Record = { + android: 'Android', + ios: 'iOS', + web: 'Web', + unknown: 'Unknown', +}; + +export function PlatformName({ children, className, platform }: PlatformNameProps) { + return ( + + {children || platformNames[platform]} + + ); +} diff --git a/webui/src/ui/Select.tsx b/webui/src/ui/Select.tsx new file mode 100644 index 0000000..cf4a6a3 --- /dev/null +++ b/webui/src/ui/Select.tsx @@ -0,0 +1,154 @@ +// see: https://ui.shadcn.com/docs/components/select + +import * as SelectPrimitive from '@radix-ui/react-select'; +import { cx } from 'class-variance-authority'; +// @ts-expect-error +import CheckIcon from 'lucide-react/dist/esm/icons/check'; +// @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 { type ComponentPropsWithoutRef, type ElementRef, forwardRef } from 'react'; + +export const Select = SelectPrimitive.Root; + +export const SelectGroup = SelectPrimitive.Group; + +export const SelectValue = SelectPrimitive.Value; + +export const SelectTrigger = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1', + className + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +export const SelectScrollUpButton = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +export const SelectScrollDownButton = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName; + +export const SelectContent = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, children, position = 'popper', ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +export const SelectLabel = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +export const SelectItem = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +export const SelectSeparator = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; From 5e7a3bb1644149b2cc68c3f3a0d37dbd3f219ae3 Mon Sep 17 00:00:00 2001 From: Cedric van Putten Date: Thu, 29 Aug 2024 11:52:13 +0200 Subject: [PATCH 3/3] feature(webui): add explanation of bundle platform and environment --- webui/src/app/_layout.tsx | 15 +++++--- webui/src/components/BundleTag.tsx | 62 ++++++++++++++++++++++++------ webui/src/components/FileSize.tsx | 52 ++++++++++++------------- 3 files changed, 84 insertions(+), 45 deletions(-) diff --git a/webui/src/app/_layout.tsx b/webui/src/app/_layout.tsx index d01d338..ae957ad 100644 --- a/webui/src/app/_layout.tsx +++ b/webui/src/app/_layout.tsx @@ -5,6 +5,7 @@ import { HmrProvider } from '~/providers/hmr'; import { QueryProvider } from '~/providers/query'; import { ThemeProvider } from '~/providers/theme'; import { ToastProvider } from '~/ui/Toast'; +import { TooltipProvider } from '~/ui/Tooltip'; // Import the Expo-required radix styles import '@radix-ui/colors/green.css'; @@ -34,12 +35,14 @@ export default function RootLayout() { return ( - - - - - - + + + + + + + + ); diff --git a/webui/src/components/BundleTag.tsx b/webui/src/components/BundleTag.tsx index 04c0797..f902d3e 100644 --- a/webui/src/components/BundleTag.tsx +++ b/webui/src/components/BundleTag.tsx @@ -5,6 +5,7 @@ import { EnvironmentIcon } from '~/components/EnvironmentIcon'; import { EnvironmentName } from '~/components/EnvironmentName'; import { PlatformName } from '~/components/PlatformName'; import { Tag } from '~/ui/Tag'; +import { Tooltip, TooltipContent, TooltipTrigger } from '~/ui/Tooltip'; import type { AtlasBundle } from '~core/data/types'; const bundleTagVariants = cva('', { @@ -34,17 +35,54 @@ type BundelTagProps = Omit< export function BundleTag({ className, platform, environment, ...props }: BundelTagProps) { return ( - - - - × - - - - + + + + + + × + + + + + + +

+ Expo creates bundles for every platform containing only{' '} + + platform-specific code + + , like Android, iOS, and Web. Some platforms can also run in multiple environments. +

+

+ Atlas marks every bundle with both the platform and target environment for which the + bundle is built. +

+

+

    +
  • + Client — Bundles that run on + device. +
  • +
  • + SSR — Bundles that only run on + server. +
  • +
  • + RSC — React server component + bundles. +
  • +
+

+
+
); } diff --git a/webui/src/components/FileSize.tsx b/webui/src/components/FileSize.tsx index a4a8e90..01ddd1c 100644 --- a/webui/src/components/FileSize.tsx +++ b/webui/src/components/FileSize.tsx @@ -1,7 +1,7 @@ // @ts-expect-error import AsteriskIcon from 'lucide-react/dist/esm/icons/asterisk'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '~/ui/Tooltip'; +import { Tooltip, TooltipContent, TooltipTrigger } from '~/ui/Tooltip'; type BundleFileSizeProps = { /** The size of the files or bundle, in bytes */ @@ -12,32 +12,30 @@ export function FileSize(props: BundleFileSizeProps) { return (
{formatByteSize(props.byteSize)} - - - - - - -

- All file sizes are calculated based on the transpiled JavaScript byte size. -

-

- While these sizes might differ from actual bundle size when using{' '} - - Hermes Bytecode (HBC) - - , the relative proportions are still correct. -

-
-
-
+ + + + + +

+ All file sizes are calculated based on the transpiled JavaScript byte size. +

+

+ While these sizes might differ from actual bundle size when using{' '} + + Hermes Bytecode (HBC) + + , the relative proportions are still correct. +

+
+
); }