Skip to content

Commit

Permalink
Virtualized files list (#767)
Browse files Browse the repository at this point in the history
* Virtualized files list

* Windowing on history files list

* Window on branches list

* Handle filter branch list

* Correct unit test
  • Loading branch information
fcollonval committed Oct 4, 2020
1 parent f0cf037 commit 3a24870
Show file tree
Hide file tree
Showing 16 changed files with 652 additions and 429 deletions.
3 changes: 3 additions & 0 deletions jupyterlab_git/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ async def post(self):
selected_hash = data["selected_hash"]
current_path = data["current_path"]
result = await self.git.detailed_log(selected_hash, current_path)

if result["code"] != 0:
self.set_status(500)
self.finish(json.dumps(result))


Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
"react": "~16.8.4",
"react-dom": "~16.8.4",
"react-textarea-autosize": "^7.1.2",
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.5",
"typestyle": "^2.0.1"
},
"devDependencies": {
Expand All @@ -83,6 +85,8 @@
"@types/react": "~16.8.13",
"@types/react-dom": "~16.0.5",
"@types/react-textarea-autosize": "^4.3.5",
"@types/react-virtualized-auto-sizer": "^1.0.0",
"@types/react-window": "^1.8.2",
"@typescript-eslint/eslint-plugin": "^2.25.0",
"@typescript-eslint/parser": "^2.25.0",
"all-contributors-cli": "^6.14.0",
Expand Down
65 changes: 35 additions & 30 deletions src/components/BranchMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as React from 'react';
import { classes } from 'typestyle';
import { Dialog, showDialog, showErrorMessage } from '@jupyterlab/apputils';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ClearIcon from '@material-ui/icons/Clear';
import { Dialog, showDialog, showErrorMessage } from '@jupyterlab/apputils';
import { Git, IGitExtension, ILogMessage } from '../tokens';
import * as React from 'react';
import { FixedSizeList, ListChildComponentProps } from 'react-window';
import { classes } from 'typestyle';
import {
activeListItemClass,
filterClass,
Expand All @@ -13,17 +13,20 @@ import {
filterWrapperClass,
listItemClass,
listItemIconClass,
listWrapperClass,
newBranchButtonClass,
wrapperClass
} from '../style/BranchMenu';
import { Git, IGitExtension, ILogMessage } from '../tokens';
import { sleep } from '../utils';
import { Alert } from './Alert';
import { NewBranchDialog } from './NewBranchDialog';
import { SuspendModal } from './SuspendModal';

const CHANGES_ERR_MSG =
'The current branch contains files with uncommitted changes. Please commit or discard these changes before switching to or creating another branch.';
const ITEM_HEIGHT = 24.8; // HTML element height for a single branch
const MIN_HEIGHT = 150; // Minimal HTML element height for the branches list
const MAX_HEIGHT = 400; // Maximal HTML element height for the branches list

/**
* Callback invoked upon encountering an error when switching branches.
Expand Down Expand Up @@ -237,53 +240,55 @@ export class BranchMenu extends React.Component<
* @returns React element
*/
private _renderBranchList(): React.ReactElement {
// Perform a "simple" filter... (TODO: consider implementing fuzzy filtering)
const filter = this.state.filter;
const branches = this.state.branches.filter(
branch => !filter || branch.name.includes(filter)
);
return (
<div className={listWrapperClass}>
<List disablePadding>{this._renderItems()}</List>
</div>
<FixedSizeList
height={Math.min(
Math.max(MIN_HEIGHT, branches.length * ITEM_HEIGHT),
MAX_HEIGHT
)}
itemCount={branches.length}
itemData={branches}
itemKey={(index, data) => data[index].name}
itemSize={ITEM_HEIGHT}
style={{ overflowX: 'hidden', paddingTop: 0, paddingBottom: 0 }}
width={'auto'}
>
{this._renderItem}
</FixedSizeList>
);
}

/**
* Renders menu items.
*
* @returns array of React elements
*/
private _renderItems(): React.ReactElement[] {
return this.state.branches.map(this._renderItem, this);
}

/**
* Renders a menu item.
*
* @param branch - branch
* @param idx - item index
* @param props Row properties
* @returns React element
*/
private _renderItem(
branch: Git.IBranch,
idx: number
): React.ReactElement | null {
// Perform a "simple" filter... (TODO: consider implementing fuzzy filtering)
if (this.state.filter && !branch.name.includes(this.state.filter)) {
return null;
}
private _renderItem = (props: ListChildComponentProps): JSX.Element => {
const { data, index, style } = props;
const branch = data[index] as Git.IBranch;
const isActive = branch.name === this.state.current;
return (
<ListItem
button
title={`Switch to branch: ${branch.name}`}
className={classes(
listItemClass,
branch.name === this.state.current ? activeListItemClass : null
isActive ? activeListItemClass : null
)}
key={branch.name}
onClick={this._onBranchClickFactory(branch.name)}
style={style}
>
<span className={listItemIconClass} />
{branch.name}
</ListItem>
);
}
};

/**
* Renders a dialog for creating a new branch.
Expand Down
147 changes: 96 additions & 51 deletions src/components/FileItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,110 @@ export const STATUS_CODES = {
'!': 'Ignored'
};

/**
* File marker properties
*/
interface IGitMarkBoxProps {
/**
* Filename
*/
fname: string;
/**
* Git repository model
*/
model: GitExtension;
/**
* File status
*/
stage: Git.Status;
}

/**
* Render the selection box in simple mode
*/
class GitMarkBox extends React.PureComponent<IGitMarkBoxProps> {
protected _onClick = (): void => {
// toggle will emit a markChanged signal
this.props.model.toggleMark(this.props.fname);

// needed if markChanged doesn't force an update of a parent
this.forceUpdate();
};

protected _onDoubleClick = (
event: React.MouseEvent<HTMLInputElement>
): void => {
event.stopPropagation();
};

render(): JSX.Element {
// idempotent, will only run once per file
this.props.model.addMark(
this.props.fname,
this.props.stage !== 'untracked'
);

return (
<input
name="gitMark"
className={gitMarkBoxStyle}
type="checkbox"
checked={this.props.model.getMark(this.props.fname)}
onChange={this._onClick}
onDoubleClick={this._onDoubleClick}
/>
);
}
}

/**
* File item properties
*/
export interface IFileItemProps {
/**
* Action buttons on the file
*/
actions?: React.ReactElement;
/**
* Callback to open a context menu on the file
*/
contextMenu?: (file: Git.IStatusFile, event: React.MouseEvent) => void;
/**
* File model
*/
file: Git.IStatusFile;
/**
* Is the file marked?
*/
markBox?: boolean;
/**
* Git repository model
*/
model: GitExtension;
/**
* Callback on double click
*/
onDoubleClick: () => void;
/**
* Is the file selected?
*/
selected?: boolean;
/**
* Callback to select the file
*/
selectFile?: (file: Git.IStatusFile | null) => void;
/**
* Inline styling for the windowing
*/
style: React.CSSProperties;
}

export interface IGitMarkBoxProps {
fname: string;
model: GitExtension;
stage: string;
}

export class FileItem extends React.Component<IFileItemProps> {
getFileChangedLabel(change: keyof typeof STATUS_CODES): string {
export class FileItem extends React.PureComponent<IFileItemProps> {
protected _getFileChangedLabel(change: keyof typeof STATUS_CODES): string {
return STATUS_CODES[change];
}

getFileChangedLabelClass(change: string) {
protected _getFileChangedLabelClass(change: string): string {
if (change === 'M') {
return this.props.selected
? classes(
Expand All @@ -67,20 +148,20 @@ export class FileItem extends React.Component<IFileItemProps> {
}
}

getFileClass() {
protected _getFileClass(): string {
return this.props.selected
? classes(fileStyle, selectedFileStyle)
: fileStyle;
}

render() {
render(): JSX.Element {
const { file } = this.props;
const status_code = file.status === 'staged' ? file.x : file.y;
const status = this.getFileChangedLabel(status_code as any);
const status = this._getFileChangedLabel(status_code as any);

return (
<li
className={this.getFileClass()}
className={this._getFileClass()}
onClick={
this.props.selectFile &&
(() => this.props.selectFile(this.props.file))
Expand All @@ -92,6 +173,7 @@ export class FileItem extends React.Component<IFileItemProps> {
})
}
onDoubleClick={this.props.onDoubleClick}
style={this.props.style}
title={`${this.props.file.to}${status}`}
>
{this.props.markBox && (
Expand All @@ -106,47 +188,10 @@ export class FileItem extends React.Component<IFileItemProps> {
selected={this.props.selected}
/>
{this.props.actions}
<span className={this.getFileChangedLabelClass(this.props.file.y)}>
<span className={this._getFileChangedLabelClass(this.props.file.y)}>
{this.props.file.y === '?' ? 'U' : status_code}
</span>
</li>
);
}
}

export class GitMarkBox extends React.Component<IGitMarkBoxProps> {
constructor(props: IGitMarkBoxProps) {
super(props);
}

protected _onClick = (event: React.ChangeEvent<HTMLInputElement>) => {
// toggle will emit a markChanged signal
this.props.model.toggleMark(this.props.fname);

// needed if markChanged doesn't force an update of a parent
this.forceUpdate();
};

protected _onDoubleClick = (event: React.MouseEvent<HTMLInputElement>) => {
event.stopPropagation();
};

render() {
// idempotent, will only run once per file
this.props.model.addMark(
this.props.fname,
this.props.stage !== 'untracked'
);

return (
<input
name="gitMark"
className={gitMarkBoxStyle}
type="checkbox"
checked={this.props.model.getMark(this.props.fname)}
onChange={this._onClick}
onDoubleClick={this._onDoubleClick}
/>
);
}
}
Loading

0 comments on commit 3a24870

Please sign in to comment.