From 7edae23b7525353a282781ab4832084ce1908397 Mon Sep 17 00:00:00 2001 From: Gunwoo Baik Date: Thu, 25 Jul 2024 13:45:09 +0900 Subject: [PATCH] Add Root-Only Filter Feature in History Tab (#872) This commit adds a Root-Only Filter in the History tab, allowing users to focus exclusively on changes to the Root of the Document. Previously, the History tab logs both content changes and Presence events (such as cursor movements), which can result in a high volume of events that may not always be relevant for debugging content changes. --- .../src/devtools/contexts/YorkieSource.tsx | 85 +++++++++++++++++-- tools/devtools/src/devtools/panel/index.tsx | 56 +++++++++--- tools/devtools/src/devtools/panel/slider.css | 4 + tools/devtools/src/devtools/panel/styles.css | 9 +- tools/devtools/src/devtools/tabs/Document.tsx | 15 +++- tools/devtools/src/devtools/tabs/History.tsx | 64 ++++++++++---- 6 files changed, 187 insertions(+), 46 deletions(-) diff --git a/tools/devtools/src/devtools/contexts/YorkieSource.tsx b/tools/devtools/src/devtools/contexts/YorkieSource.tsx index 456c180f5..3c61f592d 100644 --- a/tools/devtools/src/devtools/contexts/YorkieSource.tsx +++ b/tools/devtools/src/devtools/contexts/YorkieSource.tsx @@ -14,21 +14,30 @@ * limitations under the License. */ -import type { ReactNode } from 'react'; +import type { Dispatch, ReactNode, SetStateAction } from 'react'; import { createContext, useCallback, useContext, useEffect, + useMemo, useState, } from 'react'; -import type { SDKToPanelMessage, TransactionEvent } from 'yorkie-js-sdk'; +import { + DocEventType, + type SDKToPanelMessage, + type TransactionEvent, +} from 'yorkie-js-sdk'; import { connectPort, sendToSDK } from '../../port'; const DocKeyContext = createContext(null); const YorkieDocContext = createContext(null); -const TransactionEventsContext = createContext>(null); +const TransactionEventsContext = createContext<{ + events: Array; + hidePresenceEvents: boolean; + setHidePresenceEvents: Dispatch>; +}>(null); type Props = { children?: ReactNode; @@ -41,6 +50,10 @@ export function YorkieSourceProvider({ children }: Props) { Array >([]); + // filter out presence events + const [hideTransactionPresenceEvents, setHideTransactionPresenceEvents] = + useState(false); + const resetDocument = () => { setCurrentDocKey(''); setTransactionEvents([]); @@ -94,7 +107,13 @@ export function YorkieSourceProvider({ children }: Props) { return ( - + {children} @@ -121,12 +140,64 @@ export function useYorkieDoc() { return value; } +export enum TransactionEventType { + Document = 'document', + Presence = 'presence', +} + +export const getTransactionEventType = ( + event: TransactionEvent, +): TransactionEventType => { + for (const docEvent of event) { + if ( + docEvent.type === DocEventType.StatusChanged || + docEvent.type === DocEventType.Snapshot || + docEvent.type === DocEventType.LocalChange || + docEvent.type === DocEventType.RemoteChange + ) { + return TransactionEventType.Document; + } + } + + return TransactionEventType.Presence; +}; + export function useTransactionEvents() { - const value = useContext(TransactionEventsContext); - if (value === undefined) { + const { events, hidePresenceEvents, setHidePresenceEvents } = useContext( + TransactionEventsContext, + ); + + if (events === undefined) { throw new Error( 'useTransactionEvents should be used within YorkieSourceProvider', ); } - return value; + + // create an enhanced events with metadata + const enhancedEvents = useMemo(() => { + return events.map((event) => { + const transactionEventType = getTransactionEventType(event); + + return { + event, + transactionEventType, + isFiltered: + hidePresenceEvents && + transactionEventType === TransactionEventType.Presence, + }; + }); + }, [hidePresenceEvents, events]); + + // filter out presence events from the original events + const presenceFilteredEvents = useMemo(() => { + if (!hidePresenceEvents) return enhancedEvents; + return enhancedEvents.filter((e) => !e.isFiltered); + }, [enhancedEvents]); + + return { + originalEvents: enhancedEvents, + presenceFilteredEvents, + hidePresenceEvents, + setHidePresenceEvents, + }; } diff --git a/tools/devtools/src/devtools/panel/index.tsx b/tools/devtools/src/devtools/panel/index.tsx index 04ccd3690..4ced813a3 100644 --- a/tools/devtools/src/devtools/panel/index.tsx +++ b/tools/devtools/src/devtools/panel/index.tsx @@ -18,7 +18,6 @@ import { createRoot } from 'react-dom/client'; import { useEffect, useState } from 'react'; import yorkie from 'yorkie-js-sdk'; import { useResizable } from 'react-resizable-layout'; - import { SelectedNodeProvider } from '../contexts/SelectedNode'; import { SelectedPresenceProvider } from '../contexts/SelectedPresence'; import { @@ -34,7 +33,8 @@ import { Separator } from '../components/ResizableSeparator'; const Panel = () => { const currentDocKey = useCurrentDocKey(); - const events = useTransactionEvents(); + const { originalEvents, presenceFilteredEvents, hidePresenceEvents } = + useTransactionEvents(); const [, setDoc] = useYorkieDoc(); const [selectedEventIndexInfo, setSelectedEventIndexInfo] = useState({ index: null, @@ -57,6 +57,8 @@ const Panel = () => { axis: 'x', initial: 300, }); + const [hidePresenceTab, setHidePresenceTab] = useState(false); + const events = hidePresenceEvents ? presenceFilteredEvents : originalEvents; useEffect(() => { if (events.length === 0) { @@ -78,13 +80,23 @@ const Panel = () => { useEffect(() => { if (selectedEventIndexInfo.index === null) return; + const doc = new yorkie.Document(currentDocKey); - for (let i = 0; i <= selectedEventIndexInfo.index; i++) { - doc.applyTransactionEvent(events[i]); + + let eventIndex = 0; + let filteredEventIndex = 0; + + while (filteredEventIndex <= selectedEventIndexInfo.index) { + if (!originalEvents[eventIndex].isFiltered) { + filteredEventIndex++; + } + + doc.applyTransactionEvent(originalEvents[eventIndex].event); + eventIndex++; } setDoc(doc); - setSelectedEvent(events[selectedEventIndexInfo.index]); + setSelectedEvent(events[selectedEventIndexInfo.index].event); }, [selectedEventIndexInfo]); if (!currentDocKey) { @@ -117,22 +129,40 @@ const Panel = () => { selectedEventIndexInfo={selectedEventIndexInfo} setSelectedEventIndexInfo={setSelectedEventIndexInfo} /> + +
- + - - - - + + {!hidePresenceTab && ( + <> + + + + + + + )}
); diff --git a/tools/devtools/src/devtools/panel/slider.css b/tools/devtools/src/devtools/panel/slider.css index d17ad48d5..b050ff2fb 100644 --- a/tools/devtools/src/devtools/panel/slider.css +++ b/tools/devtools/src/devtools/panel/slider.css @@ -91,3 +91,7 @@ .rc-slider-mark-text-active .mark-remote { color: var(--blue-0); } + +.history-slider-wrap[data-length='1'] .rc-slider-rail { + display: none; +} diff --git a/tools/devtools/src/devtools/panel/styles.css b/tools/devtools/src/devtools/panel/styles.css index 1e2ae0105..272ecaf2a 100644 --- a/tools/devtools/src/devtools/panel/styles.css +++ b/tools/devtools/src/devtools/panel/styles.css @@ -83,13 +83,13 @@ width: 100%; } -.devtools-history-toolbar { +.devtools-tab-toolbar { display: flex; justify-content: space-between; align-items: center; } -.toggle-history-btn { +.toggle-tab-btn { margin-left: 4px; padding: 2px 6px; border: 1px solid var(--gray-300); @@ -100,7 +100,7 @@ font-size: 10px; } -.toggle-history-btn:hover { +.toggle-tab-btn:hover { background: var(--gray-200); } @@ -152,9 +152,6 @@ .yorkie-root { min-width: 10%; - max-width: 90%; - width: 60%; - border-right: 1px solid var(--gray-300); } .yorkie-presence { diff --git a/tools/devtools/src/devtools/tabs/Document.tsx b/tools/devtools/src/devtools/tabs/Document.tsx index 6b3eb16e9..3b72cea67 100644 --- a/tools/devtools/src/devtools/tabs/Document.tsx +++ b/tools/devtools/src/devtools/tabs/Document.tsx @@ -23,7 +23,7 @@ import { useSelectedNode } from '../contexts/SelectedNode'; import { useCurrentDocKey, useYorkieDoc } from '../contexts/YorkieSource'; import { CloseIcon } from '../icons'; -export function Document({ style }) { +export function Document({ style, hidePresenceTab, setHidePresenceTab }) { const currentDocKey = useCurrentDocKey(); const [doc] = useYorkieDoc(); const [selectedNode, setSelectedNode] = useSelectedNode(); @@ -60,7 +60,18 @@ export function Document({ style }) { return (
-
{currentDocKey || 'Document'}
+
+ {currentDocKey || 'Document'} + +
+
{selectedNode && ( diff --git a/tools/devtools/src/devtools/tabs/History.tsx b/tools/devtools/src/devtools/tabs/History.tsx index 0f9f04053..d01af06c9 100644 --- a/tools/devtools/src/devtools/tabs/History.tsx +++ b/tools/devtools/src/devtools/tabs/History.tsx @@ -17,9 +17,12 @@ import { useEffect, useState, useRef } from 'react'; import { DocEventType, Change, type TransactionEvent } from 'yorkie-js-sdk'; import Slider from 'rc-slider'; -import { useTransactionEvents } from '../contexts/YorkieSource'; import { JSONView } from '../components/JsonView'; import { CursorIcon, DocumentIcon } from '../icons'; +import { + TransactionEventType, + useTransactionEvents, +} from '../contexts/YorkieSource'; const SLIDER_MARK_WIDTH = 24; @@ -64,10 +67,17 @@ export function History({ selectedEventIndexInfo, setSelectedEventIndexInfo, }) { - const events = useTransactionEvents(); const [openHistory, setOpenHistory] = useState(false); const [sliderMarks, setSliderMarks] = useState({}); const scrollRef = useRef(null); + const { + originalEvents, + presenceFilteredEvents, + hidePresenceEvents, + setHidePresenceEvents, + } = useTransactionEvents(); + + const events = hidePresenceEvents ? presenceFilteredEvents : originalEvents; const handleSliderEvent = (value) => { setSelectedEventIndexInfo({ @@ -76,6 +86,14 @@ export function History({ }); }; + const toggleHidePresenceEvent = () => { + setSelectedEventIndexInfo({ + index: null, + isLast: true, + }); + setHidePresenceEvents((prev: boolean) => !prev); + }; + useEffect(() => { if (!openHistory || selectedEventIndexInfo.index === null) return; if (scrollRef.current) { @@ -87,23 +105,21 @@ export function History({ useEffect(() => { if (!openHistory || events.length === 0) return; + const marks = {}; for (const [index, event] of events.entries()) { - const source = event[0].source; - let type = 'presence'; - for (const docEvent of event) { - if ( - docEvent.type === DocEventType.StatusChanged || - docEvent.type === DocEventType.Snapshot || - docEvent.type === DocEventType.LocalChange || - docEvent.type === DocEventType.RemoteChange - ) { - type = 'document'; - } - } + const source = event.event[0].source; + const transactionEventType = event.transactionEventType; + marks[index] = ( - - {type === 'presence' ? : } + + {transactionEventType === TransactionEventType.Presence ? ( + + ) : ( + + )} ); } @@ -120,11 +136,11 @@ export function History({ }} >
-
+
History + )} @@ -189,6 +215,8 @@ export function History({