Skip to content

Commit

Permalink
refactor(webui): group bundle selection by platform and introduce env…
Browse files Browse the repository at this point in the history
…ironment icons
  • Loading branch information
byCedric committed Aug 28, 2024
1 parent 8861987 commit b20ec48
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 87 deletions.
127 changes: 76 additions & 51 deletions webui/src/components/BundleSelectForm.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Select.Root value={bundle.id} onValueChange={(bundle) => router.setParams({ bundle })}>
<Select.Trigger asChild>
<Button variant="quaternary" size="sm">
<BundleTag
className="mr-2"
size="xs"
platform={bundle.platform}
environment={bundle.environment}
/>
<Select.Value placeholder="Select bundle to inspect" />
<Select.Icon className="text-icon-default">
<ChevronDownIcon size={16} className="m-1 mr-0 align-middle" />
</Select.Icon>
</Button>
</Select.Trigger>
<Select.Portal>
<Select.Content
side="bottom"
collisionPadding={{ left: 16, right: 16 }}
className={cx(
'flex min-w-[220px] flex-col gap-0.5 rounded-md border border-default bg-default p-1 shadow-md',
'transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-200 data-[state=open]:duration-300',
'data-[state=closed]:fade-out data-[state=closed]:slide-out-to-top-1/3 data-[state=open]:fade-in data-[state=open]:slide-in-from-top-1/3'
)}
>
<Select.Viewport className="py-2">
{bundles.map((item) => (
<div key={item.id}>
<Select.Item value={item.id} asChild>
<Button variant="quaternary" size="sm" className="w-full !justify-start my-0.5">
<BundleTag
className="mr-2"
size="xs"
platform={item.platform}
environment={item.environment}
/>
<Select.ItemText>{relativeBundlePath(item, item.entryPoint)}</Select.ItemText>
</Button>
</Select.Item>
</div>
))}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
<Select value={bundle.id} onValueChange={(bundle) => router.setParams({ bundle })}>
<SelectTrigger className="!w-auto">
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent side="bottom" collisionPadding={{ left: 16, right: 16 }}>
{bundlesByPlatform.map(([platform, bundles]) => {
// Hide empty `unknown` platforms. If there are unknown platforms, render them.
if (platform === 'unknown' && bundles.length === 0) {
return null;
}

return (
<SelectGroup key={platform}>
<SelectLabel className="m-0.5 capitalize">
<PlatformName platform={platform} />
</SelectLabel>
{bundles.length === 0 ? (
<SelectItem disabled value="none" className="italic mb-1">
No bundle available for this platform
</SelectItem>
) : (
bundles.map((item) => (
<SelectItem key={item.id} value={item.id}>
<span className="inline-flex items-center select-none mb-0.5">
<PlatformName platform={item.platform}>
<EnvironmentIcon environment={item.environment} size={16} />
</PlatformName>
<span className="ml-2 mr-1">{relativeBundlePath(item, item.entryPoint)}</span>
</span>
</SelectItem>
))
)}
</SelectGroup>
);
})}
</SelectContent>
</Select>
);
}

function groupBundlesByPlatform(bundles: PartialAtlasBundle[]) {
const groups: Record<PartialAtlasBundle['platform'], PartialAtlasBundle[]> = {
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);
}
52 changes: 16 additions & 36 deletions webui/src/components/BundleTag.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,32 @@
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<AtlasBundle['platform'], string>,
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<AtlasBundle['environment'], string>,
},
defaultVariants: {
platform: 'unknown', // Default platform value, see MetroGraphSource
environment: 'client', // Default environment value, see MetroGraphSource
},
});

const platformChildren: Record<AtlasBundle['platform'], string> = {
android: 'Android',
ios: 'iOS',
web: 'Web',
unknown: '???',
};

const environmentChildren: Record<AtlasBundle['environment'], string> = {
client: 'Client',
node: 'SSR',
'react-server': 'RSC',
};

type BundelTagProps = Omit<
ComponentProps<typeof Tag> & VariantProps<typeof bundleTagVariants>,
'children'
Expand All @@ -49,22 +39,12 @@ export function BundleTag({ className, platform, environment, ...props }: Bundel
variant="none"
{...props}
>
{getBundelTagChildren({ platform, environment })}
<PlatformName platform={platform!} className="inline-flex items-center gap-1.5">
<PlatformName platform={platform!} />
<span>×</span>
<EnvironmentName environment={environment!} />
<EnvironmentIcon environment={environment!} size={14} />
</PlatformName>
</Tag>
);
}

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(' × ');
}
21 changes: 21 additions & 0 deletions webui/src/components/EnvironmentIcon.tsx
Original file line number Diff line number Diff line change
@@ -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<AtlasBundle['environment'], any> = {
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 <Icon {...props} />;
}
18 changes: 18 additions & 0 deletions webui/src/components/EnvironmentName.tsx
Original file line number Diff line number Diff line change
@@ -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<AtlasBundle['environment'], string> = {
client: 'Client',
node: 'SSR',
'react-server': 'RSC',
};

export function EnvironmentName({ children, environment, ...props }: EnvironmentNameProps) {
return <span {...props}>{children || environmentNames[environment]}</span>;
}
38 changes: 38 additions & 0 deletions webui/src/components/PlatformName.tsx
Original file line number Diff line number Diff line change
@@ -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<AtlasBundle['platform'], string>,
},
defaultVariants: {
platform: 'unknown',
},
});

export const platformNames: Record<AtlasBundle['platform'], string> = {
android: 'Android',
ios: 'iOS',
web: 'Web',
unknown: 'Unknown',
};

export function PlatformName({ children, className, platform }: PlatformNameProps) {
return (
<span className={platformVariants({ className, platform })}>
{children || platformNames[platform]}
</span>
);
}
Loading

0 comments on commit b20ec48

Please sign in to comment.