Skip to content

Commit

Permalink
OPIK-20 [UX] Add simple keyboard shortcuts (#178)
Browse files Browse the repository at this point in the history
  • Loading branch information
andriidudar authored Sep 4, 2024
1 parent b549cb1 commit de51c1b
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 47 deletions.
10 changes: 10 additions & 0 deletions apps/opik-frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/opik-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"react-complex-tree": "^2.4.4",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hotkeys-hook": "^4.5.1",
"react-resizable-panels": "^2.0.20",
"react18-json-view": "^0.2.8",
"stylelint-scss": "^6.4.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import IdCell from "@/components/shared/DataTableCells/IdCell";
import DataTableNoData from "@/components/shared/DataTableNoData/DataTableNoData";
import DataTableRowHeightSelector from "@/components/shared/DataTableRowHeightSelector/DataTableRowHeightSelector";
import useCompareExperimentsList from "@/api/datasets/useCompareExperimentsList";
import { DatasetItem, ExperimentsCompare } from "@/types/datasets";
import { ExperimentsCompare } from "@/types/datasets";
import Loader from "@/components/shared/Loader/Loader";
import useAppStore from "@/store/AppStore";
import { useDatasetIdFromURL } from "@/hooks/useDatasetIdFromURL";
Expand All @@ -38,6 +38,8 @@ import ColumnsButton from "@/components/shared/ColumnsButton/ColumnsButton";
import TraceDetailsPanel from "@/components/shared/TraceDetailsPanel/TraceDetailsPanel";
import useExperimentById from "@/api/datasets/useExperimentById";

const getRowId = (d: ExperimentsCompare) => d.id;

const getRowHeightClass = (height: ROW_HEIGHT) => {
switch (height) {
case ROW_HEIGHT.small:
Expand Down Expand Up @@ -65,6 +67,7 @@ export const DEFAULT_COLUMNS: ColumnData<ExperimentsCompare>[] = [
label: "Input",
size: 400,
type: COLUMN_TYPE.string,
iconType: COLUMN_TYPE.dictionary,
accessorFn: (row) =>
isObject(row.input)
? JSON.stringify(row.input, null, 2)
Expand Down Expand Up @@ -227,7 +230,7 @@ const DatasetCompareExperimentsPage: React.FunctionComponent = () => {
: `Compare (${experimentsIds.length})`;

const handleRowClick = useCallback(
(row: DatasetItem) => {
(row: ExperimentsCompare) => {
setActiveRowId((state) => (row.id === state ? "" : row.id));
},
[setActiveRowId],
Expand Down Expand Up @@ -289,7 +292,9 @@ const DatasetCompareExperimentsPage: React.FunctionComponent = () => {
columns={columns}
data={rows}
onRowClick={handleRowClick}
activeRowId={activeRowId ?? ""}
resizeConfig={resizeConfig}
getRowId={getRowId}
rowHeight={height as ROW_HEIGHT}
getRowHeightClass={getRowHeightClass}
noData={<DataTableNoData title={noDataText} />}
Expand All @@ -312,6 +317,7 @@ const DatasetCompareExperimentsPage: React.FunctionComponent = () => {
openTrace={setTraceId as OnChangeFn<string>}
onClose={handleClose}
onRowChange={handleRowChange}
isTraceDetailsOpened={Boolean(traceId)}
/>
<TraceDetailsPanel
traceId={traceId as string}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type DatasetCompareExperimentsPanelProps = {
hasNextRow?: boolean;
onClose: () => void;
onRowChange?: (shift: number) => void;
isTraceDetailsOpened: boolean;
};

const DatasetCompareExperimentsPanel: React.FunctionComponent<
Expand All @@ -42,6 +43,7 @@ const DatasetCompareExperimentsPanel: React.FunctionComponent<
hasNextRow,
onClose,
onRowChange,
isTraceDetailsOpened,
}) => {
const { toast } = useToast();

Expand Down Expand Up @@ -151,13 +153,15 @@ const DatasetCompareExperimentsPanel: React.FunctionComponent<
return (
<ResizableSidePanel
panelId="compare-experiments"
entity="item"
open={Boolean(experimentsCompareId)}
headerContent={renderHeaderContent()}
hasPreviousRow={hasPreviousRow}
hasNextRow={hasNextRow}
onClose={onClose}
onRowChange={onRowChange}
initialWidth={0.8}
ignoreHotkeys={isTraceDetailsOpened}
>
{renderContent()}
</ResizableSidePanel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ const DatasetItemsPage = () => {
</div>
<ResizableSidePanel
panelId="dataset-items"
entity="item"
open={Boolean(activeRowId)}
hasPreviousRow={hasPrevious}
hasNextRow={hasNext}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import React, { useCallback, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { ChevronDown, ChevronsRight, ChevronUp } from "lucide-react";
import { Separator } from "@/components/ui/separator";
import isFunction from "lodash/isFunction";
import { useHotkeys } from "react-hotkeys-hook";

import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import TooltipWrapper from "@/components/shared/TooltipWrapper/TooltipWrapper";

const INITIAL_WIDTH = 0.75;
const MIN_LEFT_POSITION = 0.1;
Expand All @@ -12,25 +15,29 @@ const MAX_LEFT_POSITION = 0.8;
type ResizableSidePanelProps = {
panelId: string;
children: React.ReactNode;
entity?: string;
headerContent?: React.ReactNode;
open?: boolean;
hasPreviousRow?: boolean;
hasNextRow?: boolean;
onClose: () => void;
onRowChange?: (shift: number) => void;
initialWidth?: number;
ignoreHotkeys?: boolean;
};

const ResizableSidePanel: React.FunctionComponent<ResizableSidePanelProps> = ({
panelId,
children,
entity = "",
headerContent,
open = false,
hasPreviousRow,
hasNextRow,
onClose,
onRowChange,
initialWidth = INITIAL_WIDTH,
ignoreHotkeys = false,
}) => {
const localStorageKey = `${panelId}-side-panel-width`;

Expand All @@ -46,7 +53,28 @@ const ResizableSidePanel: React.FunctionComponent<ResizableSidePanelProps> = ({
resizeHandleRef.current.setAttribute("data-resize-handle-active", "true");
}, []);

React.useEffect(() => {
useHotkeys(
"ArrowUp,ArrowDown,Escape",
(keyboardEvent: KeyboardEvent) => {
if (!open) return;
keyboardEvent.stopPropagation();
switch (keyboardEvent.code) {
case "ArrowUp":
isFunction(onRowChange) && hasPreviousRow && onRowChange(-1);
break;
case "ArrowDown":
isFunction(onRowChange) && hasNextRow && onRowChange(1);
break;
case "Escape":
onClose();
break;
}
},
{ ignoreEventWhen: () => ignoreHotkeys },
[onRowChange, onClose, open, ignoreHotkeys],
);

useEffect(() => {
const handleMouseMove = (event: MouseEvent) => {
if (resizeHandleRef.current) {
leftRef.current = event.pageX / window.innerWidth;
Expand Down Expand Up @@ -90,22 +118,26 @@ const ResizableSidePanel: React.FunctionComponent<ResizableSidePanelProps> = ({
return (
<>
<Separator orientation="vertical" className="mx-4 h-8" />
<Button
variant="outline"
size="icon-sm"
disabled={!hasPreviousRow}
onClick={() => onRowChange(-1)}
>
<ChevronUp className="size-4" />
</Button>
<Button
variant="outline"
size="icon-sm"
disabled={!hasNextRow}
onClick={() => onRowChange(1)}
>
<ChevronDown className="size-4" />
</Button>
<TooltipWrapper content={`Previous ${entity}`} hotkey="↑">
<Button
variant="outline"
size="icon-sm"
disabled={!hasPreviousRow}
onClick={() => onRowChange(-1)}
>
<ChevronUp className="size-4" />
</Button>
</TooltipWrapper>
<TooltipWrapper content={`Next ${entity}`} hotkey="↓">
<Button
variant="outline"
size="icon-sm"
disabled={!hasNextRow}
onClick={() => onRowChange(1)}
>
<ChevronDown className="size-4" />
</Button>
</TooltipWrapper>
</>
);
};
Expand All @@ -124,14 +156,16 @@ const ResizableSidePanel: React.FunctionComponent<ResizableSidePanelProps> = ({
<div className="relative flex size-full">
<div className="absolute inset-x-0 top-0 flex h-[60px] items-center justify-between gap-6 pl-6 pr-5">
<div className="flex gap-2">
<Button
data-testid="side-panel-close"
variant="outline"
size="icon-sm"
onClick={onClose}
>
<ChevronsRight className="size-4" />
</Button>
<TooltipWrapper content={`Close ${entity}`} hotkey="Esc">
<Button
data-testid="side-panel-close"
variant="outline"
size="icon-sm"
onClick={onClose}
>
<ChevronsRight className="size-4" />
</Button>
</TooltipWrapper>
{renderNavigation()}
</div>
{headerContent && <div>{headerContent}</div>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,27 @@ type TooltipWrapperProps = {
content: string;
children?: React.ReactNode;
side?: "top" | "right" | "bottom" | "left";
hotkey?: React.ReactNode;
};

const TooltipWrapper: React.FunctionComponent<TooltipWrapperProps> = ({
content,
children,
side,
hotkey = null,
}) => {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipPortal>
<TooltipContent side={side}>
<TooltipContent side={side} variant={hotkey ? "hotkey" : "default"}>
{content}
{hotkey && (
<div className="flex h-5 min-w-5 items-center justify-center rounded-s border border-light-slate px-1 text-light-slate">
{hotkey}
</div>
)}
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ const AnnotateRow: React.FunctionComponent<AnnotateRowProps> = ({
);
useEffect(() => {
setCategoryName(feedbackScore?.category_name);
}, [feedbackScore?.category_name]);
}, [feedbackScore?.category_name, traceId, spanId]);

const [value, setValue] = useState(feedbackScore?.value);
useEffect(() => {
setValue(feedbackScore?.value);
}, [feedbackScore?.value]);
}, [feedbackScore?.value, spanId, traceId]);

const feedbackScoreDeleteMutation = useTraceFeedbackScoreDeleteMutation();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useMemo, useState } from "react";
import sortBy from "lodash/sortBy";
import { Plus } from "lucide-react";
import { useHotkeys } from "react-hotkeys-hook";

import {
FEEDBACK_SCORE_TYPE,
Expand All @@ -12,22 +13,38 @@ import { Button } from "@/components/ui/button";
import useAppStore from "@/store/AppStore";
import useFeedbackDefinitionsList from "@/api/feedback-definitions/useFeedbackDefinitionsList";
import { FeedbackDefinition } from "@/types/feedback-definitions";
import TooltipWrapper from "@/components/shared/TooltipWrapper/TooltipWrapper";
import AddFeedbackDefinitionDialog from "@/components/shared/AddFeedbackDefinitionDialog/AddFeedbackDefinitionDialog";
import AnnotateRow from "./AnnotateRow";

type TraceAnnotateViewerProps = {
data: Trace | Span;
spanId?: string;
traceId: string;
annotateOpen: boolean;
setAnnotateOpen: (open: boolean) => void;
};

const TraceAnnotateViewer: React.FunctionComponent<
TraceAnnotateViewerProps
> = ({ data, spanId, traceId, setAnnotateOpen }) => {
> = ({ data, spanId, traceId, annotateOpen, setAnnotateOpen }) => {
const [feedbackDefinitionDialogOpen, setFeedbackDefinitionDialogOpen] =
useState(false);

useHotkeys(
"Escape",
(keyboardEvent: KeyboardEvent) => {
if (!annotateOpen) return;
keyboardEvent.stopPropagation();
switch (keyboardEvent.code) {
case "Escape":
setAnnotateOpen(false);
break;
}
},
[annotateOpen],
);

const workspaceName = useAppStore((state) => state.activeWorkspaceName);
const { data: feedbackDefinitionsData } = useFeedbackDefinitionsList({
workspaceName,
Expand Down Expand Up @@ -78,13 +95,15 @@ const TraceAnnotateViewer: React.FunctionComponent<
<div className="flex items-center gap-2 overflow-x-hidden">
<div className="comet-title-m truncate">Annotate</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setAnnotateOpen(false)}
>
Close
</Button>
<TooltipWrapper content="Close annotate" hotkey="Esc">
<Button
variant="ghost"
size="sm"
onClick={() => setAnnotateOpen(false)}
>
Close
</Button>
</TooltipWrapper>
</div>
<div className="mt-4 flex flex-col gap-4">
<div className="grid max-w-full grid-cols-[minmax(0,5fr)_minmax(0,10fr)_minmax(0,2fr)] border-t border-border">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ const TraceDetailsPanel: React.FunctionComponent<TraceDetailsPanelProps> = ({
data={dataToView}
spanId={spanId}
traceId={traceId}
annotateOpen={annotateOpen as boolean}
setAnnotateOpen={setAnnotateOpen}
/>
</ResizablePanel>
Expand Down Expand Up @@ -195,6 +196,7 @@ const TraceDetailsPanel: React.FunctionComponent<TraceDetailsPanelProps> = ({
return (
<ResizableSidePanel
panelId="traces"
entity="trace"
open={Boolean(traceId)}
headerContent={renderHeaderContent()}
hasPreviousRow={hasPreviousRow}
Expand Down
Loading

0 comments on commit de51c1b

Please sign in to comment.