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

Provide UI feedback during Git command execution #630

Merged
merged 55 commits into from
Jul 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
06300ff
Add UI feedback during toolbar actions
kgryte Apr 30, 2020
4ced5a8
Ensure a minimum duration
kgryte May 4, 2020
da2ec0e
Add setting for toggling UI suspension
kgryte May 11, 2020
1bace21
Document setting
kgryte May 11, 2020
4437e60
Document new setting
kgryte May 11, 2020
1e36470
Update description
kgryte May 18, 2020
6a93b34
Update setting description
kgryte May 18, 2020
f58b51f
Toggle UI suspension based on plugin setting
kgryte May 18, 2020
f3e25ec
Fix broken tests
kgryte May 18, 2020
71442cb
Fix capitalization
kgryte May 18, 2020
a579a42
Fix capitalization
kgryte May 18, 2020
0169c2a
Merge branch 'master' of https://github.com/jupyterlab/jupyterlab-git…
kgryte May 18, 2020
3b3b915
Fix capitalization
kgryte May 18, 2020
d373478
Wire up model event logging
kgryte May 18, 2020
2f155e5
Refactor and clean-up the extension model
kgryte May 19, 2020
ccbfb70
Add lumino collections dependency
kgryte May 19, 2020
8d471f4
Fix task queue management
kgryte May 19, 2020
cf3d9ac
Update task names and map log events to status messages
kgryte May 19, 2020
6bdba11
Throttle status widget to prevent flashing updates
kgryte May 19, 2020
5576d73
Restore prior refresh status behavior when non-200 response
kgryte May 19, 2020
c34c33e
Update lockfile
kgryte May 19, 2020
062a2a1
Move status widget to separate file and add widget style
kgryte May 19, 2020
b0a1c44
Fix operation order bug
kgryte May 19, 2020
c51a449
Allow the user to dismiss the modal
kgryte May 19, 2020
2b9da5e
Add setting to toggle status bar updates
kgryte May 26, 2020
697bab8
Add support for toggling display of status bar updates
kgryte May 26, 2020
1596dee
Remove console.log
kgryte May 26, 2020
62d88de
Document function
kgryte May 26, 2020
fc2edb0
Merge branch 'master' of https://github.com/jupyterlab/jupyterlab-git…
kgryte May 26, 2020
b1ec725
Add backticks
kgryte May 26, 2020
126389f
Provide UI feedback when switching branches
kgryte May 27, 2020
29b9917
Add support for providing feedback when creating a new branch and ref…
kgryte May 27, 2020
018444b
Fix broken tests
kgryte May 27, 2020
4d84996
Add src documentation and rename variables to be in line with project…
kgryte May 27, 2020
bf5c52a
Fix broken tests
kgryte May 27, 2020
e0e6a5e
Add support for providing UI feedback when committing changes
kgryte May 27, 2020
1d5c364
Add Material UI dep for displaying alert messages within toast notifi…
kgryte May 27, 2020
d203738
Refactor to use "toast" notifications
kgryte May 27, 2020
2d48dda
Rename file and refactor into smaller components to support toasts
kgryte Jun 1, 2020
3daf4ba
Refactor to support log message alerts
kgryte Jun 1, 2020
b1d79d6
Merge branch 'master' of https://github.com/jupyterlab/jupyterlab-git…
kgryte Jun 1, 2020
99d6766
Refactor to support toast alerts
kgryte Jun 1, 2020
ab68b01
Refactor to support toast alerts
kgryte Jun 1, 2020
b4d5513
Fix broken test
kgryte Jun 1, 2020
f2d40d8
Refactor to support UI feedback
kgryte Jun 1, 2020
28be7d5
Reorder methods
kgryte Jun 1, 2020
c132ced
Add private annotation
kgryte Jun 1, 2020
c4f0813
Fix broken tests
kgryte Jun 1, 2020
cab4132
Fix broken tests
kgryte Jun 1, 2020
e5d86d7
Merge branch 'master' of https://github.com/jupyterlab/jupyterlab-git…
kgryte Jun 8, 2020
9f2d7ca
Fix response consumption bug
kgryte Jun 8, 2020
2cd0408
Reorder properties and methods
kgryte Jun 15, 2020
ae71d8d
Use finally blocks
kgryte Jun 15, 2020
5ec36d7
Merge with master
fcollonval Jul 24, 2020
1c2cb43
Post Merge branch 'master' corrections
fcollonval Jul 24, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/issue_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Welcome! Before creating a new issue:
* Look at the README *Troubleshooting* section
* Search for relevant issues
* Check that you have updated both the jupyterlab extension and the python package to the same version
Check that you have installed Git version 2 or higher
-->

## Description
Expand Down Expand Up @@ -35,6 +36,9 @@ Welcome! Before creating a new issue:
<!-- Results of `conda list jupyterlab-git` or `pip show jupyterlab-git` -->
- Extension version:
<!-- Results of `jupyter labextension list` -->
- Git version:
<!-- Results of `git --version` -->
- Operating System and its version:

<details><summary>Command Line Output</summary>
<pre>
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ jupyter lab build

Once installed, extension behavior can be modified via the following settings which can be set in JupyterLab's advanced settings editor:

- **blockWhileCommandExecutes**: suspend JupyterLab user interaction until Git commands (e.g., `commit`, `pull`, `reset`, `revert`) finish executing. Setting this to `true` helps mitigate potential race conditions leading to data loss, conflicts, and a broken Git history. Unless running a slow network, UI suspension should not interfere with standard workflows. Setting this to `false` allows for actions to trigger multiple concurrent Git actions.
- **cancelPullMergeConflict**: cancel pulling changes from a remote repository if there exists a merge conflict. If set to `true`, when fetching and integrating changes from a remote repository, a conflicting merge is canceled and the working tree left untouched.
- **disableBranchWithChanges**: disable all branch operations, such as creating a new branch or switching to a different branch, when there are changed/staged files. When set to `true`, this setting guards against overwriting and/or losing uncommitted changes.
- **doubleClickDiff**: double click a file in the Git UI to open a diff of the file instead of opening the file for editing.
- **displayStatus**: display Git extension status updates in the JupyterLab status bar. If `true`, the extension displays status updates in the JupyterLab status bar, such as when pulling and pushing changes, switching branches, and polling for changes. Depending on the level of extension activity, some users may find the status updates distracting. In which case, setting this to `false` should reduce visual noise.
- **doubleClickDiff**: double click a file in the Git extension panel to open a diff of the file instead of opening the file for editing.
- **historyCount**: number of commits shown in the history log, beginning with the most recent. Displaying a larger number of commits can lead to performance degradation, so use caution when modifying this setting.
- **refreshInterval**: number of milliseconds between polling the file system for changes. In order to ensure that the UI correctly displays the current repository status, the extension must poll the file system for changes. Longer polling times increase the likelihood that the UI does not reflect the current status; however, longer polling times also incur less performance overhead.
- **simpleStaging**: enable a simplified concept of staging. When this setting is `true`, all files with changes are automatically staged. When we develop in JupyterLab, we often only care about what files have changed (in the broadest sense) and don't need to distinguish between "tracked" and "untracked" files. Accordingly, this setting allows us to simplify the visual presentation of changes, which is especially useful for those less acquainted with Git.
Expand Down Expand Up @@ -105,9 +108,9 @@ cd jupyterlab-git
pip install -e .[test]
jupyter serverextension enable --py jupyterlab_git --sys-prefix

# Build the labextension and dev-mode link it to jlab
# Build and install your development version of the extension
jlpm
jupyter labextension link .
jupyter labextension install .
```

To rebuild the package after a change and the JupyterLab app:
Expand Down
3 changes: 2 additions & 1 deletion binder/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ channels:
dependencies:
# Jupyter
- jupyterlab~=2.0
- nodejs>=11
- nodejs>=12
- nbgitpuller
4 changes: 3 additions & 1 deletion binder/postBuild
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
#!/usr/bin/env bash
pip install .
jupyter lab build
jupyter serverextension enable --sys-prefix --py nbgitpuller
jupyter lab build
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,12 @@
"@jupyterlab/settingregistry": "^2.0.0",
"@jupyterlab/terminal": "^2.0.0",
"@jupyterlab/ui-components": "^2.0.0",
"@lumino/collections": "^1.2.3",
"@lumino/polling": "^1.0.4",
"@lumino/widgets": "^1.11.1",
"@material-ui/core": "^4.8.2",
"@material-ui/icons": "^4.5.1",
"@material-ui/lab": "^4.0.0-alpha.54",
"diff-match-patch": "^1.0.4",
"nbdime": "^6.0.0",
"react": "~16.9.0",
Expand Down
14 changes: 13 additions & 1 deletion schema/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
"description": "jupyterlab-git settings.",
"type": "object",
"properties": {
"blockWhileCommandExecutes": {
"type": "boolean",
"title": "Suspend user interaction until commands finish",
"description": "Suspend JupyterLab user interaction until Git commands (e.g., commit, pull, reset, revert) finish executing. Setting this to true helps mitigate potential race conditions leading to data loss, conflicts, and a broken Git history. Unless running a slow network, UI suspension should not interfere with standard workflows. Setting this to false allows for actions to trigger multiple concurrent Git actions.",
"default": true
},
"cancelPullMergeConflict": {
"type": "boolean",
"title": "Cancel pull merge conflict",
Expand All @@ -17,10 +23,16 @@
"description": "Disable all branch operations (new, switch) when there are changed/staged files",
"default": false
},
"displayStatus": {
"type": "boolean",
"title": "Display Git status updates",
"description": "Display Git extension status updates in the JupyterLab status bar. If true, the extension displays status updates in the JupyterLab status bar, such as when pulling and pushing changes, switching branches, and polling for changes. Depending on the level of extension activity, some users may find the status updates distracting. In which case, setting this to false should reduce visual noise.",
"default": true
},
"doubleClickDiff": {
"type": "boolean",
"title": "Show diff on double click",
"description": "If true, doubling clicking a file in the list of changed files will open a diff",
"description": "If true, doubling clicking a file in the list of changed files will open a diff.",
"default": false
},
"historyCount": {
Expand Down
155 changes: 149 additions & 6 deletions src/gitMenuCommands.ts → src/commandsAndMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,23 @@ import {
import { FileBrowser } from '@jupyterlab/filebrowser';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { ITerminal } from '@jupyterlab/terminal';
import { CommandRegistry } from '@lumino/commands';
import { Menu } from '@lumino/widgets';
import { IGitExtension } from './tokens';
import { GitCredentialsForm } from './widgets/CredentialsBox';
import { doGitClone } from './widgets/gitClone';
import { GitPullPushDialog, Operation } from './widgets/gitPushPull';

const RESOURCES = [
{
text: 'Set Up Remotes',
url: 'https://www.atlassian.com/git/tutorials/setting-up-a-repository'
},
{
text: 'Git Documentation',
url: 'https://git-scm.com/doc'
}
];

/**
* The command IDs used by the git plugin.
Expand All @@ -24,6 +39,8 @@ export namespace CommandIDs {
export const gitToggleDoubleClickDiff = 'git:toggle-double-click-diff';
export const gitAddRemote = 'git:add-remote';
export const gitClone = 'git:clone';
export const gitPush = 'git:push';
export const gitPull = 'git:pull';
}

/**
Expand Down Expand Up @@ -63,7 +80,8 @@ export function addCommands(
console.error(e);
main.dispose();
}
}
},
isEnabled: () => model.pathRepository !== null
});

/** Add open/go to git interface command */
Expand All @@ -81,8 +99,8 @@ export function addCommands(

/** Add git init command */
commands.addCommand(CommandIDs.gitInit, {
label: 'Init',
caption: ' Create an empty Git repository or reinitialize an existing one',
label: 'Initialize a Repository',
caption: 'Create an empty Git repository or reinitialize an existing one',
execute: async () => {
const currentPath = fileBrowser.model.path;
const result = await showDialog({
Expand All @@ -95,7 +113,8 @@ export function addCommands(
await model.init(currentPath);
model.pathRepository = currentPath;
}
}
},
isEnabled: () => model.pathRepository === null
});

/** Open URL externally */
Expand Down Expand Up @@ -127,7 +146,7 @@ export function addCommands(

/** Command to add a remote Git repository */
commands.addCommand(CommandIDs.gitAddRemote, {
label: 'Add remote repository',
label: 'Add Remote Repository',
caption: 'Add a Git remote repository',
isEnabled: () => model.pathRepository !== null,
execute: async args => {
Expand Down Expand Up @@ -162,12 +181,136 @@ export function addCommands(

/** Add git clone command */
commands.addCommand(CommandIDs.gitClone, {
label: 'Clone',
label: 'Clone a Repository',
caption: 'Clone a repository from a URL',
isEnabled: () => model.pathRepository === null,
execute: async () => {
await doGitClone(model, fileBrowser.model.path);
fileBrowser.model.refresh();
}
});

/** Add git push command */
commands.addCommand(CommandIDs.gitPush, {
label: 'Push to Remote',
caption: 'Push code to remote repository',
isEnabled: () => model.pathRepository !== null,
execute: async () => {
await Private.showGitOperationDialog(model, Operation.Push).catch(
reason => {
console.error(
`Encountered an error when pushing changes. Error: ${reason}`
);
}
);
}
});

/** Add git pull command */
commands.addCommand(CommandIDs.gitPull, {
label: 'Pull from Remote',
caption: 'Pull latest code from remote repository',
isEnabled: () => model.pathRepository !== null,
execute: async () => {
await Private.showGitOperationDialog(model, Operation.Pull).catch(
reason => {
console.error(
`Encountered an error when pulling changes. Error: ${reason}`
);
}
);
}
});
}

/**
* Adds commands and menu items.
*
* @private
* @param app - Jupyter front end
* @param gitExtension - Git extension instance
* @param fileBrowser - file browser instance
* @param settings - extension settings
* @returns menu
*/
export function createGitMenu(commands: CommandRegistry): Menu {
const menu = new Menu({ commands });
menu.title.label = 'Git';
[
CommandIDs.gitInit,
CommandIDs.gitClone,
CommandIDs.gitPush,
CommandIDs.gitPull,
CommandIDs.gitAddRemote,
CommandIDs.gitTerminalCommand
].forEach(command => {
menu.addItem({ command });
});

menu.addItem({ type: 'separator' });

menu.addItem({ command: CommandIDs.gitToggleSimpleStaging });

menu.addItem({ command: CommandIDs.gitToggleDoubleClickDiff });

menu.addItem({ type: 'separator' });

const tutorial = new Menu({ commands });
tutorial.title.label = ' Help ';
RESOURCES.map(args => {
tutorial.addItem({
args,
command: CommandIDs.gitOpenUrl
});
});

menu.addItem({ type: 'submenu', submenu: tutorial });

return menu;
}

/* eslint-disable no-inner-declarations */
namespace Private {
/**
* Displays an error dialog when a Git operation fails.
*
* @private
* @param model - Git extension model
* @param operation - Git operation name
* @returns Promise for displaying a dialog
*/
export async function showGitOperationDialog(
model: IGitExtension,
operation: Operation
): Promise<void> {
const title = `Git ${operation}`;
let result = await showDialog({
title: title,
body: new GitPullPushDialog(model, operation),
buttons: [Dialog.okButton({ label: 'DISMISS' })]
});
let retry = false;
while (!result.button.accept) {
const credentials = await showDialog({
title: 'Git credentials required',
body: new GitCredentialsForm(
'Enter credentials for remote repository',
retry ? 'Incorrect username or password.' : ''
),
buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'OK' })]
});

if (!credentials.button.accept) {
break;
}

result = await showDialog({
title: title,
body: new GitPullPushDialog(model, operation, credentials.value),
buttons: [Dialog.okButton({ label: 'DISMISS' })]
});
retry = true;
}
}
}
/* eslint-enable no-inner-declarations */
17 changes: 9 additions & 8 deletions src/components/ActionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LabIcon } from '@jupyterlab/ui-components';
import * as React from 'react';
import { classes } from 'typestyle';
import { LabIcon } from '@jupyterlab/ui-components';
import { actionButtonStyle } from '../style/ActionButtonStyle';

/**
Expand All @@ -16,9 +16,9 @@ export interface IActionButtonProps {
*/
disabled?: boolean;
/**
* Icon name
* Icon
*/
iconName: string;
icon: LabIcon;
/**
* Button title
*/
Expand All @@ -37,14 +37,15 @@ export interface IActionButtonProps {
export const ActionButton: React.FunctionComponent<IActionButtonProps> = (
props: IActionButtonProps
) => {
const { disabled, className, title, onClick, icon } = props;
return (
<button
disabled={props.disabled}
className={classes(actionButtonStyle, props.className)}
title={props.title}
onClick={props.onClick}
disabled={disabled}
className={classes(actionButtonStyle, className)}
title={title}
onClick={onClick}
>
<LabIcon.resolveReact icon={props.iconName} />
<icon.react elementPosition="center" tag="span" />
</button>
);
};