Skip to content

Commit

Permalink
feat(ui): object comparison
Browse files Browse the repository at this point in the history
  • Loading branch information
jamie-rasmussen committed Nov 7, 2024
1 parent 6f65b1c commit 8ff8314
Show file tree
Hide file tree
Showing 28 changed files with 2,153 additions and 21 deletions.
4 changes: 2 additions & 2 deletions weave-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -232,14 +232,14 @@
"tailwindcss": "^3.3.2",
"ts-jest": "^27.1.4",
"ts-node": "^10.9.1",
"tsd": "^0.30.4",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"tslint-plugin-prettier": "^2.3.0",
"typescript": "4.7.4",
"uuid": "^9.0.0",
"vite": "5.2.9",
"vitest": "^1.6.0",
"tsd": "^0.30.4"
"vitest": "^1.6.0"
},
"resolutions": {
"@types/react": "^17.0.26",
Expand Down
10 changes: 10 additions & 0 deletions weave-js/src/components/PagePanelComponents/Home/Browse3.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {Button} from '../../Button';
import {ErrorBoundary} from '../../ErrorBoundary';
import {Browse2EntityPage} from './Browse2/Browse2EntityPage';
import {Browse2HomePage} from './Browse2/Browse2HomePage';
import {ComparePage} from './Browse3/compare/ComparePage';
import {
baseContext,
browse2Context,
Expand Down Expand Up @@ -532,6 +533,9 @@ const Browse3ProjectRoot: FC<{
]}>
<PlaygroundPageBinding />
</Route>
<Route path={`${projectRoot}/compare`}>
<ComparePageBinding />
</Route>
</Switch>
</Box>
);
Expand Down Expand Up @@ -1030,6 +1034,12 @@ const TablesPageBinding = () => {
return <TablesPage entity={params.entity} project={params.project} />;
};

const ComparePageBinding = () => {
const params = useParamsDecoded<Browse3TabItemParams>();

return <ComparePage entity={params.entity} project={params.project} />;
};

const AppBarLink = (props: ComponentProps<typeof RouterLink>) => (
<MaterialLink
sx={{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import * as DropdownMenu from '@wandb/weave/components/DropdownMenu';
import classNames from 'classnames';
import React, {useState} from 'react';
import {SortableElement, SortableHandle} from 'react-sortable-hoc';

import {Button} from '../../../../Button';
import {Icon} from '../../../../Icon';
import {BadgeDef} from './types';

// TODO: Change cursor to grabbing when dragging
const DragHandle = SortableHandle(() => (
<div className="cursor-grab">
<Icon name="drag-grip" />
</div>
));

type BadgeProps = {
numBadges: number;
idx: number;
badge: BadgeDef;
useBaseline: boolean;
isSelected: boolean;
onClickBadgeLabel: (value: string) => void;
onSetBaseline: (value: string | null) => void;
onRemoveBadge: (value: string) => void;
};

export const Badge = SortableElement(
({
numBadges,
idx,
badge,
useBaseline,
isSelected,
onClickBadgeLabel,
onSetBaseline,
onRemoveBadge,
}: BadgeProps) => {
const [isOpen, setIsOpen] = useState(false);

const onClickLabel =
idx === 0
? undefined
: () => {
onClickBadgeLabel(badge.value);
};

const onMakeBaseline = () => {
onSetBaseline(badge.value);
};

const onRemoveItem = () => {
onRemoveBadge(badge.value);
};

return (
<div className="tw-style">
<div
className={classNames(
'flex select-none items-center gap-4 whitespace-nowrap rounded-[4px] border-[1px] border-moon-250 bg-white px-4 py-8 text-sm font-semibold',
{
'border-teal-350 text-teal-500 outline outline-1 outline-teal-350':
isSelected,
}
)}>
<DragHandle />
<div
className={classNames(
{
'underline decoration-teal-450': useBaseline && idx === 0,
},
{
'cursor-pointer': idx !== 0,
}
)}
onClick={onClickLabel}>
{badge.label ?? badge.value}
</div>
<DropdownMenu.Root open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenu.Trigger>
<Button
variant="ghost"
icon="overflow-vertical"
active={isOpen}
size="small"
/>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content align="start">
{useBaseline && idx === 0 && (
<DropdownMenu.Item
onClick={() => {
onSetBaseline(null);
}}>
<Icon name="baseline-alt" />
Remove baseline
</DropdownMenu.Item>
)}
{(!useBaseline || idx !== 0) && (
<DropdownMenu.Item onClick={onMakeBaseline}>
<Icon name="baseline-alt" />
Make baseline
</DropdownMenu.Item>
)}
{numBadges > 2 && (
<>
<DropdownMenu.Separator />
<DropdownMenu.Item onClick={onRemoveItem}>
<Icon name="delete" />
Remove from comparison
</DropdownMenu.Item>
</>
)}
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</div>
</div>
);
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import React from 'react';
import {useHistory} from 'react-router-dom';
import {SortableContainer} from 'react-sortable-hoc';

import {queryToggleString, searchParamsSetArray} from '../urlQueryUtil';
import {Badge} from './Badge';
import {BadgeDefs} from './types';

type BadgesProps = {
badges: BadgeDefs;
useBaseline: boolean;
selected: string | null;
};

type SortableBadgesProps = BadgesProps & {
onClickBadgeLabel: (value: string) => void;
onSetBaseline: (value: string | null) => void;
onRemoveBadge: (value: string) => void;
};

const SortableList = SortableContainer(
({
badges,
useBaseline,
selected,
onClickBadgeLabel,
onSetBaseline,
onRemoveBadge,
}: SortableBadgesProps) => {
return (
<div className="flex flex-wrap items-center gap-8">
{badges.map((badge, index) => (
<Badge
key={`item-${badge.value}`}
index={index}
idx={index}
badge={badge}
useBaseline={useBaseline}
onClickBadgeLabel={onClickBadgeLabel}
onSetBaseline={onSetBaseline}
onRemoveBadge={onRemoveBadge}
numBadges={badges.length}
isSelected={badge.value === selected}
/>
))}
</div>
);
}
);

// Create a copy of the specified array, moving an item from one index to another.
function arrayMove<T>(array: readonly T[], from: number, to: number) {
const slicedArray = array.slice();
slicedArray.splice(
to < 0 ? array.length + to : to,
0,
slicedArray.splice(from, 1)[0]
);
return slicedArray;
}

export const Badges = ({badges, useBaseline, selected}: BadgesProps) => {
const history = useHistory();
const onSortEnd = ({
oldIndex,
newIndex,
}: {
oldIndex: number;
newIndex: number;
}) => {
if (oldIndex === newIndex) {
return;
}
const {search} = history.location;
const params = new URLSearchParams(search);
params.delete('baseline');
const newBadges = arrayMove(badges, oldIndex, newIndex);
const values = newBadges.map(b => b.value);
searchParamsSetArray(params, badges[0].key, values);
history.replace({
search: params.toString(),
});
};

function moveToFront<T>(list: T[], item: T): T[] {
const index = list.indexOf(item);
if (index !== -1) {
list.splice(index, 1); // Remove the item from its current position
list.unshift(item); // Add the item to the front
}
return list;
}

const onClickBadgeLabel = (value: string) => {
queryToggleString(history, 'sel', value);
};

const onSetBaseline = (value: string | null) => {
const {search} = history.location;
const params = new URLSearchParams(search);
if (value === null) {
params.delete('baseline');
} else {
let values = badges.map(b => b.value);
values = moveToFront(values, value);
searchParamsSetArray(params, badges[0].key, values);
params.set('baseline', '1');
}
history.replace({
search: params.toString(),
});
};

const onRemoveBadge = (value: string) => {
const newBadges = badges.filter(b => b.value !== value);
const {search} = history.location;
const params = new URLSearchParams(search);
searchParamsSetArray(
params,
newBadges[0].key,
newBadges.map(b => b.value)
);
if (selected === value || selected === newBadges[0].value) {
params.delete('sel');
}
history.replace({
search: params.toString(),
});
};

return (
<SortableList
useDragHandle
axis="xy"
badges={badges}
useBaseline={useBaseline}
selected={selected}
onSortEnd={onSortEnd}
onClickBadgeLabel={onClickBadgeLabel}
onSetBaseline={onSetBaseline}
onRemoveBadge={onRemoveBadge}
/>
);
};
Loading

0 comments on commit 8ff8314

Please sign in to comment.