Skip to content

Commit 36790f9

Browse files
authored
Refactor user feedbacks (#777)
* Extract the task handler from the model Refactor user logger * Avoid focus capture by suspend modal * Propagate error not related to authentication
1 parent 951b745 commit 36790f9

28 files changed

+1117
-1484
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@
6363
"@jupyterlab/terminal": "^2.0.0",
6464
"@jupyterlab/ui-components": "^2.0.0",
6565
"@lumino/collections": "^1.2.3",
66+
"@lumino/commands": "^1.11.0",
67+
"@lumino/coreutils": "^1.5.0",
6668
"@lumino/polling": "^1.0.4",
69+
"@lumino/signaling": "^1.4.0",
6770
"@lumino/widgets": "^1.11.1",
6871
"@material-ui/core": "^4.8.2",
6972
"@material-ui/icons": "^4.5.1",

src/commandsAndMenu.tsx

Lines changed: 176 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ import {
2121
RenderMimeProvider
2222
} from './components/diff/Diff';
2323
import { getRefValue, IDiffContext } from './components/diff/model';
24+
import { AUTH_ERROR_MESSAGES } from './git';
25+
import { logger } from './logger';
2426
import { GitExtension } from './model';
2527
import { diffIcon } from './style/icons';
26-
import { Git } from './tokens';
28+
import { Git, Level } from './tokens';
2729
import { GitCredentialsForm } from './widgets/CredentialsBox';
28-
import { doGitClone } from './widgets/gitClone';
29-
import { GitPullPushDialog, Operation } from './widgets/gitPushPull';
30+
import { GitCloneForm } from './widgets/GitCloneForm';
3031

3132
const RESOURCES = [
3233
{
@@ -39,6 +40,26 @@ const RESOURCES = [
3940
}
4041
];
4142

43+
interface IGitCloneArgs {
44+
/**
45+
* Path in which to clone the Git repository
46+
*/
47+
path: string;
48+
/**
49+
* Git repository url
50+
*/
51+
url: string;
52+
}
53+
54+
/**
55+
* Git operations requiring authentication
56+
*/
57+
enum Operation {
58+
Clone = 'Clone',
59+
Pull = 'Pull',
60+
Push = 'Push'
61+
}
62+
4263
/**
4364
* The command IDs used by the git plugin.
4465
*/
@@ -133,8 +154,28 @@ export function addCommands(
133154
});
134155

135156
if (result.button.accept) {
136-
await model.init(currentPath);
137-
model.pathRepository = currentPath;
157+
logger.log({
158+
message: 'Initializing...',
159+
level: Level.RUNNING
160+
});
161+
try {
162+
await model.init(currentPath);
163+
model.pathRepository = currentPath;
164+
logger.log({
165+
message: 'Git repository initialized.',
166+
level: Level.SUCCESS
167+
});
168+
} catch (error) {
169+
console.error(
170+
'Encountered an error when initializing the repository. Error: ',
171+
error
172+
);
173+
logger.log({
174+
message: 'Failed to initialize the Git repository',
175+
level: Level.ERROR,
176+
error
177+
});
178+
}
138179
}
139180
},
140181
isEnabled: () => model.pathRepository === null
@@ -208,8 +249,41 @@ export function addCommands(
208249
caption: 'Clone a repository from a URL',
209250
isEnabled: () => model.pathRepository === null,
210251
execute: async () => {
211-
await doGitClone(model, fileBrowser.model.path);
212-
fileBrowser.model.refresh();
252+
const result = await showDialog({
253+
title: 'Clone a repo',
254+
body: new GitCloneForm(),
255+
focusNodeSelector: 'input',
256+
buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'CLONE' })]
257+
});
258+
259+
if (result.button.accept && result.value) {
260+
logger.log({
261+
level: Level.RUNNING,
262+
message: 'Cloning...'
263+
});
264+
try {
265+
await Private.showGitOperationDialog<IGitCloneArgs>(
266+
model,
267+
Operation.Clone,
268+
{ path: fileBrowser.model.path, url: result.value }
269+
);
270+
logger.log({
271+
message: 'Successfully cloned',
272+
level: Level.SUCCESS
273+
});
274+
await fileBrowser.model.refresh();
275+
} catch (error) {
276+
console.error(
277+
'Encountered an error when cloning the repository. Error: ',
278+
error
279+
);
280+
logger.log({
281+
message: 'Failed to clone',
282+
level: Level.ERROR,
283+
error
284+
});
285+
}
286+
}
213287
}
214288
});
215289

@@ -229,13 +303,27 @@ export function addCommands(
229303
caption: 'Push code to remote repository',
230304
isEnabled: () => model.pathRepository !== null,
231305
execute: async () => {
232-
await Private.showGitOperationDialog(model, Operation.Push).catch(
233-
reason => {
234-
console.error(
235-
`Encountered an error when pushing changes. Error: ${reason}`
236-
);
237-
}
238-
);
306+
logger.log({
307+
level: Level.RUNNING,
308+
message: 'Pushing...'
309+
});
310+
try {
311+
await Private.showGitOperationDialog(model, Operation.Push);
312+
logger.log({
313+
message: 'Successfully pushed',
314+
level: Level.SUCCESS
315+
});
316+
} catch (error) {
317+
console.error(
318+
'Encountered an error when pushing changes. Error: ',
319+
error
320+
);
321+
logger.log({
322+
message: 'Failed to push',
323+
level: Level.ERROR,
324+
error
325+
});
326+
}
239327
}
240328
});
241329

@@ -245,13 +333,27 @@ export function addCommands(
245333
caption: 'Pull latest code from remote repository',
246334
isEnabled: () => model.pathRepository !== null,
247335
execute: async () => {
248-
await Private.showGitOperationDialog(model, Operation.Pull).catch(
249-
reason => {
250-
console.error(
251-
`Encountered an error when pulling changes. Error: ${reason}`
252-
);
253-
}
254-
);
336+
logger.log({
337+
level: Level.RUNNING,
338+
message: 'Pulling...'
339+
});
340+
try {
341+
await Private.showGitOperationDialog(model, Operation.Pull);
342+
logger.log({
343+
message: 'Successfully pulled',
344+
level: Level.SUCCESS
345+
});
346+
} catch (error) {
347+
console.error(
348+
'Encountered an error when pulling changes. Error: ',
349+
error
350+
);
351+
logger.log({
352+
message: 'Failed to pull',
353+
level: Level.ERROR,
354+
error
355+
});
356+
}
255357
}
256358
});
257359

@@ -515,44 +617,69 @@ export function createGitMenu(commands: CommandRegistry): Menu {
515617
/* eslint-disable no-inner-declarations */
516618
namespace Private {
517619
/**
518-
* Displays an error dialog when a Git operation fails.
620+
* Handle Git operation that may require authentication.
519621
*
520622
* @private
521623
* @param model - Git extension model
522624
* @param operation - Git operation name
625+
* @param args - Git operation arguments
626+
* @param authentication - Git authentication information
627+
* @param retry - Is this operation retried?
523628
* @returns Promise for displaying a dialog
524629
*/
525-
export async function showGitOperationDialog(
630+
export async function showGitOperationDialog<T>(
526631
model: GitExtension,
527-
operation: Operation
632+
operation: Operation,
633+
args?: T,
634+
authentication?: Git.IAuth,
635+
retry = false
528636
): Promise<void> {
529-
const title = `Git ${operation}`;
530-
let result = await showDialog({
531-
title: title,
532-
body: new GitPullPushDialog(model, operation),
533-
buttons: [Dialog.okButton({ label: 'DISMISS' })]
534-
});
535-
let retry = false;
536-
while (!result.button.accept) {
537-
const credentials = await showDialog({
538-
title: 'Git credentials required',
539-
body: new GitCredentialsForm(
540-
'Enter credentials for remote repository',
541-
retry ? 'Incorrect username or password.' : ''
542-
),
543-
buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'OK' })]
544-
});
545-
546-
if (!credentials.button.accept) {
547-
break;
637+
try {
638+
// the Git action
639+
switch (operation) {
640+
case Operation.Clone:
641+
// eslint-disable-next-line no-case-declarations
642+
const { path, url } = (args as any) as IGitCloneArgs;
643+
await model.clone(path, url, authentication);
644+
break;
645+
case Operation.Pull:
646+
await model.pull(authentication);
647+
break;
648+
case Operation.Push:
649+
await model.push(authentication);
650+
break;
651+
default:
652+
return;
548653
}
654+
} catch (error) {
655+
if (
656+
AUTH_ERROR_MESSAGES.some(
657+
errorMessage => error.message.indexOf(errorMessage) > -1
658+
)
659+
) {
660+
// If the error is an authentication error, ask the user credentials
661+
const credentials = await showDialog({
662+
title: 'Git credentials required',
663+
body: new GitCredentialsForm(
664+
'Enter credentials for remote repository',
665+
retry ? 'Incorrect username or password.' : ''
666+
)
667+
});
549668

550-
result = await showDialog({
551-
title: title,
552-
body: new GitPullPushDialog(model, operation, credentials.value),
553-
buttons: [Dialog.okButton({ label: 'DISMISS' })]
554-
});
555-
retry = true;
669+
if (credentials.button.accept) {
670+
// Retry the operation if the user provides its credentials
671+
return await showGitOperationDialog<T>(
672+
model,
673+
operation,
674+
args,
675+
credentials.value,
676+
true
677+
);
678+
}
679+
}
680+
// Throw the error if it cannot be handled or
681+
// if the user did not accept to provide its credentials
682+
throw error;
556683
}
557684
}
558685
}

src/components/Alert.tsx

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import * as React from 'react';
1+
import { showErrorMessage } from '@jupyterlab/apputils';
2+
import { Button } from '@material-ui/core';
23
import Portal from '@material-ui/core/Portal';
3-
import Snackbar from '@material-ui/core/Snackbar';
44
import Slide from '@material-ui/core/Slide';
5-
import { default as MuiAlert } from '@material-ui/lab/Alert';
6-
import { Severity } from '../tokens';
5+
import Snackbar from '@material-ui/core/Snackbar';
6+
import { Color, default as MuiAlert } from '@material-ui/lab/Alert';
7+
import * as React from 'react';
78

89
/**
910
* Returns a React component for "sliding-in" an alert.
@@ -30,10 +31,15 @@ export interface IAlertProps {
3031
*/
3132
message: string;
3233

34+
/**
35+
* Error object
36+
*/
37+
error?: Error;
38+
3339
/**
3440
* Alert severity.
3541
*/
36-
severity?: Severity;
42+
severity?: Color;
3743

3844
/**
3945
* Alert duration (in milliseconds).
@@ -91,7 +97,23 @@ export class Alert extends React.Component<IAlertProps> {
9197
onClick={this._onClick}
9298
onClose={this._onClose}
9399
>
94-
<MuiAlert variant="filled" severity={severity}>
100+
<MuiAlert
101+
action={
102+
this.props.error && (
103+
<Button
104+
color="inherit"
105+
size="small"
106+
onClick={() => {
107+
showErrorMessage('Error', this.props.error);
108+
}}
109+
>
110+
SHOW
111+
</Button>
112+
)
113+
}
114+
variant="filled"
115+
severity={severity}
116+
>
95117
{this.props.message || '(missing message)'}
96118
</MuiAlert>
97119
</Snackbar>

0 commit comments

Comments
 (0)