Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Root-Only Filter Feature in History Tab #872

73 changes: 66 additions & 7 deletions tools/devtools/src/devtools/contexts/YorkieSource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(null);
const YorkieDocContext = createContext(null);
const TransactionEventsContext = createContext<Array<TransactionEvent>>(null);
const TransactionEventsContext = createContext<{
events: Array<TransactionEvent>;
hidePresenceEvents: boolean;
setHidePresenceEvents: Dispatch<SetStateAction<boolean>>;
}>(null);

type Props = {
children?: ReactNode;
Expand All @@ -41,6 +50,10 @@ export function YorkieSourceProvider({ children }: Props) {
Array<TransactionEvent>
>([]);

// filter out presence events
const [hideTransactionPresenceEvents, setHideTransactionPresenceEvents] =
useState(false);

const resetDocument = () => {
setCurrentDocKey('');
setTransactionEvents([]);
Expand Down Expand Up @@ -94,7 +107,13 @@ export function YorkieSourceProvider({ children }: Props) {

return (
<DocKeyContext.Provider value={currentDocKey}>
<TransactionEventsContext.Provider value={transactionEvents}>
<TransactionEventsContext.Provider
value={{
events: transactionEvents,
hidePresenceEvents: hideTransactionPresenceEvents,
setHidePresenceEvents: setHideTransactionPresenceEvents,
}}
>
<YorkieDocContext.Provider value={[doc, setDoc]}>
{children}
</YorkieDocContext.Provider>
Expand All @@ -121,12 +140,52 @@ 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;

const filteredEvents = useMemo(
() =>
events.filter(
(event) =>
!hidePresenceEvents ||
getTransactionEventType(event) === TransactionEventType.Document,
),
chacha912 marked this conversation as resolved.
Show resolved Hide resolved
[events, hidePresenceEvents],
);

return {
events: filteredEvents,
hidePresenceEvents,
setHidePresenceEvents,
};
}
40 changes: 29 additions & 11 deletions tools/devtools/src/devtools/panel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@
*/

import { createRoot } from 'react-dom/client';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, 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 {
Expand All @@ -34,7 +33,7 @@ import { Separator } from '../components/ResizableSeparator';

const Panel = () => {
const currentDocKey = useCurrentDocKey();
const events = useTransactionEvents();
const { events } = useTransactionEvents();
const [, setDoc] = useYorkieDoc();
const [selectedEventIndexInfo, setSelectedEventIndexInfo] = useState({
index: null,
Expand All @@ -57,6 +56,7 @@ const Panel = () => {
axis: 'x',
initial: 300,
});
const [hidePresenceTab, setHidePresenceTab] = useState(false);

useEffect(() => {
if (events.length === 0) {
Expand Down Expand Up @@ -117,22 +117,40 @@ const Panel = () => {
selectedEventIndexInfo={selectedEventIndexInfo}
setSelectedEventIndexInfo={setSelectedEventIndexInfo}
/>

<Separator
dir={'horizontal'}
isDragging={isHistoryDragging}
{...historySeparatorProps}
/>

<div className="devtools-data">
<SelectedNodeProvider>
<Document style={{ width: documentW }} />
<Document
style={{
width: hidePresenceTab ? '100%' : documentW,
maxWidth: hidePresenceTab ? '100%' : '90%',
borderRight: hidePresenceTab
? 'none'
: '1px solid var(--gray-300)',
}}
hidePresenceTab={hidePresenceTab}
setHidePresenceTab={setHidePresenceTab}
/>
</SelectedNodeProvider>
<Separator
isDragging={isDocumentDragging}
{...documentSeparatorProps}
/>
<SelectedPresenceProvider>
<Presence />
</SelectedPresenceProvider>

{!hidePresenceTab && (
<>
<Separator
isDragging={isDocumentDragging}
{...documentSeparatorProps}
/>

<SelectedPresenceProvider>
<Presence />
</SelectedPresenceProvider>
</>
)}
chacha912 marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
);
Expand Down
4 changes: 4 additions & 0 deletions tools/devtools/src/devtools/panel/slider.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
9 changes: 3 additions & 6 deletions tools/devtools/src/devtools/panel/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -100,7 +100,7 @@
font-size: 10px;
}

.toggle-history-btn:hover {
.toggle-tab-btn:hover {
background: var(--gray-200);
}

Expand Down Expand Up @@ -152,9 +152,6 @@

.yorkie-root {
min-width: 10%;
max-width: 90%;
width: 60%;
border-right: 1px solid var(--gray-300);
}

.yorkie-presence {
Expand Down
15 changes: 13 additions & 2 deletions tools/devtools/src/devtools/tabs/Document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -60,7 +60,18 @@ export function Document({ style }) {

return (
<div className="yorkie-root content-wrap" style={{ ...style }}>
<div className="title">{currentDocKey || 'Document'}</div>
<div className="devtools-tab-toolbar">
<span className="title">{currentDocKey || 'Document'}</span>
<button
className="toggle-tab-btn"
onClick={() => {
setHidePresenceTab((v: boolean) => !v);
}}
>
{hidePresenceTab ? '⏴' : '⏵'}
chacha912 marked this conversation as resolved.
Show resolved Hide resolved
</button>
</div>
chacha912 marked this conversation as resolved.
Show resolved Hide resolved

<div className="content">
<RootTree root={root} />
{selectedNode && (
Expand Down
52 changes: 34 additions & 18 deletions tools/devtools/src/devtools/tabs/History.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@
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,
getTransactionEventType,
useTransactionEvents,
} from '../contexts/YorkieSource';

const SLIDER_MARK_WIDTH = 24;

Expand Down Expand Up @@ -64,10 +68,11 @@ export function History({
selectedEventIndexInfo,
setSelectedEventIndexInfo,
}) {
const events = useTransactionEvents();
const [openHistory, setOpenHistory] = useState(false);
const [sliderMarks, setSliderMarks] = useState({});
const scrollRef = useRef(null);
const { events, hidePresenceEvents, setHidePresenceEvents } =
useTransactionEvents();

const handleSliderEvent = (value) => {
setSelectedEventIndexInfo({
Expand All @@ -76,6 +81,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) {
Expand All @@ -87,23 +100,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 transactionEventType = getTransactionEventType(event);

marks[index] = (
<span className={`mark-history mark-${source} mark-${type}`}>
{type === 'presence' ? <CursorIcon /> : <DocumentIcon />}
<span
className={`mark-history mark-${source} mark-${transactionEventType}`}
>
{transactionEventType === TransactionEventType.Presence ? (
<CursorIcon />
) : (
<DocumentIcon />
)}
</span>
);
}
Expand All @@ -120,11 +131,11 @@ export function History({
}}
>
<div className="content-wrap">
<div className="devtools-history-toolbar">
<div className="devtools-tab-toolbar">
<span className="title">
History
<button
className="toggle-history-btn"
className="toggle-tab-btn"
onClick={() => {
setOpenHistory((v) => !v);
}}
Expand Down Expand Up @@ -180,6 +191,9 @@ export function History({
>
</button>
<button onClick={toggleHidePresenceEvent}>
chacha912 marked this conversation as resolved.
Show resolved Hide resolved
{hidePresenceEvents ? '¥' : 'Y'}
</button>
</span>
</span>
)}
Expand All @@ -189,14 +203,16 @@ export function History({
<div
ref={scrollRef}
style={{ width: '100%', overflowX: 'auto', minHeight: '46px' }}
className="history-slider-wrap"
data-length={events.length}
>
<Slider
dots
min={0}
marks={sliderMarks}
max={events.length - 1}
value={selectedEventIndexInfo.index}
step={1}
step={0}
chacha912 marked this conversation as resolved.
Show resolved Hide resolved
onChange={handleSliderEvent}
style={{
width: events.length * SLIDER_MARK_WIDTH + 'px',
Expand Down
Loading