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({