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) => (
-
-
-
-
- {relativeBundlePath(item, item.entryPoint)}
-
-
-
- ))}
-
-
-
-
+ router.setParams({ bundle })}>
+
+
+
+
+ {bundlesByPlatform.map(([platform, bundles]) => {
+ // Hide empty `unknown` platforms. If there are unknown platforms, render them.
+ if (platform === 'unknown' && bundles.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {bundles.length === 0 ? (
+
+ No bundle available for this platform
+
+ ) : (
+ bundles.map((item) => (
+
+
+
+
+
+ {relativeBundlePath(item, item.entryPoint)}
+
+
+ ))
+ )}
+
+ );
+ })}
+
+
+ );
+}
+
+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.
+
+
+
);
}