Skip to content

Commit

Permalink
Connect cells across states (#88)
Browse files Browse the repository at this point in the history
* Replace implementation with xarrow

* don't let xarrow lines overlap header and footer
  • Loading branch information
keckelt authored Mar 5, 2024
1 parent 4bd115f commit 99b97d8
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 190 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"d3-selection": "^3.0.0",
"html-react-parser": "^4.0.0",
"monaco-editor": "^0.41.0",
"react-xarrows": "^2.0.2",
"tabletojson": "^4.0.1",
"zustand": "^4.3.8"
},
Expand Down
75 changes: 37 additions & 38 deletions src/Overview/Cells/CodeCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,45 +123,44 @@ export function CodeCell({

// create a cell with input and output
return (
<>
<div
data-cell-id={cellId}
onClick={setActiveCell}
onDoubleClick={toggleFullwidth}
className={cx(
'jp-Cell',
{ ['active']: isActiveCell },
{ ['added']: added },
{ ['executed']: executions > 0 },
{ ['changed']: !unchanged && !added }
)}
>
<TypeIcon type={type} executions={executions} />
{multiUser && fullWidth ? <CellUsers cellUsers={cellExecutions.get(cellId)?.user ?? []} /> : <></>}
<ExecutionBadge executions={executions} />
{
// Add CompareBadge if old, oldStateNo, and oldTimestamp are defined
previousCell && previousStateNo && previousStateTimestamp && (
<CompareBadge
old={previousCell}
oldStateNo={previousStateNo}
oldTimestamp={previousStateTimestamp}
current={cell}
currentStateNo={stateNo}
currentTimestamp={timestamp}
/>
)
}
<div
id={`${stateNo}-${cellId}`}
data-cell-id={cellId}
onClick={setActiveCell}
onDoubleClick={toggleFullwidth}
className={cx(
'jp-Cell',
{ ['active']: isActiveCell },
{ ['added']: added },
{ ['executed']: executions > 0 },
{ ['changed']: !unchanged && !added }
)}
>
<TypeIcon type={type} executions={executions} />
{multiUser && fullWidth ? <CellUsers cellUsers={cellExecutions.get(cellId)?.user ?? []} /> : <></>}
<ExecutionBadge executions={executions} />
{
// Add CompareBadge if old, oldStateNo, and oldTimestamp are defined
previousCell && previousStateNo && previousStateTimestamp && (
<CompareBadge
old={previousCell}
oldStateNo={previousStateNo}
oldTimestamp={previousStateTimestamp}
current={cell}
currentStateNo={stateNo}
currentTimestamp={timestamp}
/>
)
}

{input}
{split}
{fullWidth && (outputChanged || isActiveCell) ? (
detailDiffOutput
) : (
<div className={cx('unchanged', 'transparent', 'output')}></div>
)}
</div>
</>
{input}
{split}
{fullWidth && (outputChanged || isActiveCell) ? (
detailDiffOutput
) : (
<div className={cx('unchanged', 'transparent', 'output')}></div>
)}
</div>
);
}

Expand Down
9 changes: 7 additions & 2 deletions src/Overview/Cells/DeletedCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import React from 'react';
export interface IDeletedCellProps {
cellId: string;
isActiveCell: boolean;
stateNo: number;
}

export function DeletedCell({ cellId, isActiveCell }: IDeletedCellProps): JSX.Element {
export function DeletedCell({ cellId, isActiveCell, stateNo }: IDeletedCellProps): JSX.Element {
// console.log('render deleted cell');
return (
<div data-cell-id={cellId} className={`jp-Cell deleted ${isActiveCell === true ? 'active' : ''}`}>
<div
id={`${stateNo}-${cellId}`}
data-cell-id={cellId}
className={`jp-Cell deleted ${isActiveCell === true ? 'active' : ''}`}
>
<div style={{ height: '12.8px' }}></div>
</div>
);
Expand Down
76 changes: 37 additions & 39 deletions src/Overview/Cells/MarkDownCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,44 +83,8 @@ export function MarkdownCell({

if (fullWidth) {
return (
<>
<div
data-cell-id={cellId}
onClick={setActiveCell}
onDoubleClick={toggleFullwidth}
className={cx(
'jp-Cell',
{ ['active']: isActiveCell === true },
{ ['added']: added },
{ ['executed']: executions > 0 },
{ ['changed']: changed }
)}
>
<TypeIcon type={'markdown'} executions={executions} />
{multiUser ? <CellUsers cellUsers={cellExecutions.get(cellId)?.user ?? []} /> : <></>}
<ExecutionBadge executions={executions} />
{
// Add CompareBadge if old, oldStateNo, and oldTimestamp are defined
previousCell && previousStateNo && previousStateTimestamp && (
<CompareBadge
old={previousCell}
oldStateNo={previousStateNo}
oldTimestamp={previousStateTimestamp}
current={cell}
currentStateNo={stateNo}
currentTimestamp={timestamp}
/>
)
}
{detailDiffContent}
</div>
</>
);
}
//else: compact
return (
<>
<div
id={`${stateNo}-${cellId}`}
data-cell-id={cellId}
onClick={setActiveCell}
onDoubleClick={toggleFullwidth}
Expand All @@ -133,9 +97,43 @@ export function MarkdownCell({
)}
>
<TypeIcon type={'markdown'} executions={executions} />
{multiUser ? <CellUsers cellUsers={cellExecutions.get(cellId)?.user ?? []} /> : <></>}
<ExecutionBadge executions={executions} />
<div className={cx(classes.tinyHeight)}></div>
{
// Add CompareBadge if old, oldStateNo, and oldTimestamp are defined
previousCell && previousStateNo && previousStateTimestamp && (
<CompareBadge
old={previousCell}
oldStateNo={previousStateNo}
oldTimestamp={previousStateTimestamp}
current={cell}
currentStateNo={stateNo}
currentTimestamp={timestamp}
/>
)
}
{detailDiffContent}
</div>
</>
);
}
//else: compact
return (
<div
id={`${stateNo}-${cellId}`}
data-cell-id={cellId}
onClick={setActiveCell}
onDoubleClick={toggleFullwidth}
className={cx(
'jp-Cell',
{ ['active']: isActiveCell === true },
{ ['added']: added },
{ ['executed']: executions > 0 },
{ ['changed']: changed }
)}
>
<TypeIcon type={'markdown'} executions={executions} />
<ExecutionBadge executions={executions} />
<div className={cx(classes.tinyHeight)}></div>
</div>
);
}
83 changes: 34 additions & 49 deletions src/Overview/State.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ import { getScrollParent, mergeArrays } from '../util';
import { CodeCell } from './Cells/CodeCell';
import { DeletedCell } from './Cells/DeletedCell';
import { MarkdownCell } from './Cells/MarkDownCell';
import { useXarrow } from 'react-xarrows';

const useStyles = createStyles((theme, _params) => ({
header: {
borderBottom: 'var(--jp-border-width) solid var(--jp-toolbar-border-color)'
borderBottom: 'var(--jp-border-width) solid var(--jp-toolbar-border-color)',
zIndex: 1,
backgroundColor: 'white'
},
stateWrapper: {
label: 'wrapper',
Expand Down Expand Up @@ -181,7 +184,9 @@ const useStyles = createStyles((theme, _params) => ({
label: 'version-split',
borderTop: '1px solid var(--jp-toolbar-border-color)',
marginTop: '1em',
textAlign: 'center'
textAlign: 'center',
zIndex: 1, // higher than the xarrow lines
backgroundColor: 'white'
},
dashedBorder: {
// borderLeft: 'var(--jp-border-width) dotted var(--jp-toolbar-border-color)',
Expand All @@ -203,7 +208,6 @@ interface IStateProps {
timestamp: Date;
numStates: number;
nbTracker: INotebookTracker;
handleScroll: (stateNo: number) => void;
multiUser: boolean;
}

Expand All @@ -218,10 +222,10 @@ export function State({
timestamp,
numStates,
nbTracker,
handleScroll,
multiUser
}: IStateProps): JSX.Element {
const { classes, cx } = useStyles();
const updateXarrow = useXarrow();

const [fullWidth, setFullWidth] = useState(stateDoI === 1); // on first render, initialize with stateDoI
useEffect(() => {
Expand Down Expand Up @@ -292,50 +296,35 @@ export function State({
const activeCellTop = useLoopsStore(state => state.activeCellTop);
const stateScrollerRef = useRef<HTMLDivElement>(null);

const scrollToElement = () => {
// provCellTop = distance of the provenance's corresponding cell to the top of the extension
// console.log(`state ${stateNo} scroll to active cell ID with top position`, activeCellId, activeCellTop);
const provCellTop = stateScrollerRef.current?.querySelector<HTMLDivElement>(
`[data-cell-id="${activeCellId}"]`
)?.offsetTop;

const versionSplit = 35;
// activeCellTop and provCellTop are calculated relative to different elements, align them by adding the height of the top panel
const jpTopPanelHeight = document.querySelector<HTMLDivElement>('#jp-top-panel')?.offsetHeight || 0;
// the notebook cells have some padding at the top that needs to be considered in order to align the cells properly
const jpCellPadding =
parseInt(getComputedStyle(document.documentElement).getPropertyValue('--jp-cell-padding')) || 0;

if (activeCellTop && provCellTop) {
// console.log('scroll to element', activeCellTop, provCellTop, jpTopPanelHeight, jpCellPadding);
const scrollPos = provCellTop - activeCellTop + jpTopPanelHeight - jpCellPadding + versionSplit;
// console.log('scrollpos', scrollPos);
stateScrollerRef.current?.scrollTo({ top: scrollPos, behavior: 'instant' });
}
};

useEffect(
() => {
const scrollToElement = () => {
// provCellTop = distance of the provenance's corresponding cell to the top of the extension
// console.log(`state ${stateNo} scroll to active cell ID with top position`, activeCellId, activeCellTop);
const provCellTop = stateScrollerRef.current?.querySelector<HTMLDivElement>(
`[data-cell-id="${activeCellId}"]`
)?.offsetTop;

const versionSplit = 35;
// activeCellTop and provCellTop are calculated relative to different elements, align them by adding the height of the top panel
const jpTopPanelHeight = document.querySelector<HTMLDivElement>('#jp-top-panel')?.offsetHeight || 0;
// the notebook cells have some padding at the top that needs to be considered in order to align the cells properly
const jpCellPadding =
parseInt(getComputedStyle(document.documentElement).getPropertyValue('--jp-cell-padding')) || 0;

if (activeCellTop && provCellTop) {
// console.log('scroll to element', activeCellTop, provCellTop, jpTopPanelHeight, jpCellPadding);
const scrollPos = provCellTop - activeCellTop + jpTopPanelHeight - jpCellPadding + versionSplit;
// console.log('scrollpos', scrollPos);
stateScrollerRef.current?.scrollTo({ top: scrollPos, behavior: 'instant' });
}
};
scrollToElement();
} //, [activeCellTop] // commented out: dpeend on activeCellTop --> run if the value changes
//currently: no dependency --> run on every render (at the end of the render cycle)
},
[activeCellId, activeCellTop] //depend on activeCellTop --> run if the value changes
////currently: no dependency --> run on every render (at the end of the render cycle)
);

// useEffect(() => {
// const element = stateScrollerRef.current;
// const handleScrollWrapper = () => handleScroll(stateNo);

// if (element !== null) {
// element.addEventListener('scroll', handleScrollWrapper);
// }

// return () => {
// if (element !== null) {
// element.removeEventListener('scroll', handleScrollWrapper);
// }
// };
// }, []); // Empty dependency array means this effect runs once on mount and cleanup on unmount

if (!state) {
return <div>State {stateNo} not found</div>;
}
Expand All @@ -354,7 +343,7 @@ export function State({

if (cell === undefined && previousCell !== undefined) {
// cell was deleted in current state
return <DeletedCell key={cellId} cellId={cellId} isActiveCell={isActiveCell} />;
return <DeletedCell key={cellId} cellId={cellId} isActiveCell={isActiveCell} stateNo={stateNo} />;
} else if (cell === undefined && previousCell === undefined) {
// cell is in none of the states
// weird, but nothing to do
Expand Down Expand Up @@ -478,7 +467,7 @@ export function State({
</ActionIcon>
</Center>
</header>
<div ref={stateScrollerRef} className={classes.stateScroller}>
<div ref={stateScrollerRef} className={classes.stateScroller} onScroll={updateXarrow}>
<div className={cx(classes.state, 'state')}>
<div style={{ height: '100vh' }} className={classes.dashedBorder}></div>
{cells}
Expand Down Expand Up @@ -510,7 +499,3 @@ export function State({
</div>
);
}

interface IScrollableElement extends Element {
onscrollend: ((this: IScrollableElement, ev: Event) => any) | null;
}
Loading

0 comments on commit 99b97d8

Please sign in to comment.