@jupyterlab/git
Version:
A JupyterLab extension for version control using git
241 lines (240 loc) • 12.2 kB
JavaScript
import { Notification } from '@jupyterlab/apputils';
import { PageConfig, PathExt } from '@jupyterlab/coreutils';
import { caretDownIcon, caretUpIcon, refreshIcon } from '@jupyterlab/ui-components';
import Badge from '@mui/material/Badge';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import * as React from 'react';
import { classes } from 'typestyle';
import { showError } from '../notifications';
import { selectedTabClass, tabClass, tabIndicatorClass, tabsClass } from '../style/GitPanel';
import { badgeClass, spacer, toolbarButtonClass, toolbarClass, toolbarMenuButtonClass, toolbarMenuButtonEnabledClass, toolbarMenuButtonIconClass, toolbarMenuButtonSubtitleClass, toolbarMenuButtonTitleClass, toolbarMenuButtonTitleWrapperClass, toolbarMenuWrapperClass, toolbarNavClass } from '../style/Toolbar';
import { branchIcon, desktopIcon, pullIcon, pushIcon } from '../style/icons';
import { CommandIDs, Git } from '../tokens';
import { ActionButton } from './ActionButton';
import { BranchMenu } from './BranchMenu';
import { SubmoduleMenu } from './SubmoduleMenu';
import { TagMenu } from './TagMenu';
/**
* React component for rendering a panel toolbar.
*/
export class Toolbar extends React.Component {
/**
* Returns a React component for rendering a panel toolbar.
*
* @param props - component properties
* @returns React component
*/
constructor(props) {
super(props);
/**
* Callback invoked upon clicking a button to pull the latest changes.
*
* @param event - event object
* @returns a promise which resolves upon pulling the latest changes
*/
this._onPullClick = async () => {
await this.props.commands.execute(CommandIDs.gitPull);
};
/**
* Callback invoked upon clicking a button to push the latest changes.
*
* @param event - event object
* @returns a promise which resolves upon pushing the latest changes
*/
this._onPushClick = async () => {
await this.props.commands.execute(CommandIDs.gitPush);
};
/**
* Callback invoked upon clicking a button to change the current branch.
*
* @param event - event object
*/
this._onBranchClick = () => {
// Toggle the branch menu:
this.setState({
branchMenu: !this.state.branchMenu
});
};
this._onRepoClick = () => {
// Toggle the submodule menu:
this.setState({
repoMenu: !this.state.repoMenu
});
};
/**
* Callback invoked upon clicking a button to refresh the model.
*
* @param event - event object
* @returns a promise which resolves upon refreshing the model
*/
this._onRefreshClick = async () => {
const id = Notification.emit(this.props.trans.__('Refreshing…'), 'in-progress', { autoClose: false });
this.setState({ refreshInProgress: true });
try {
await this.props.model.refresh();
Notification.update({
id,
message: this.props.trans.__('Successfully refreshed.'),
type: 'success',
autoClose: 5000
});
}
catch (error) {
console.error(error);
Notification.update({
id,
message: this.props.trans.__('Failed to refresh.'),
type: 'error',
...showError(error, this.props.trans)
});
}
finally {
this.setState({ refreshInProgress: false });
}
};
this.state = {
branchMenu: false,
tab: 0,
refreshInProgress: false,
hasRemote: false,
repoMenu: false
};
}
/**
* Check whether or not the repo has any remotes
*/
async componentDidMount() {
try {
const remotes = await this.props.model.getRemotes();
const hasRemote = remotes.length > 0 ? true : false;
this.setState({ hasRemote });
}
catch (err) {
console.error(err);
}
}
/**
* Renders the component.
*
* @returns React element
*/
render() {
return (React.createElement("div", { className: toolbarClass },
this._renderTopNav(),
this._renderRepoMenu(),
this._renderBranchMenu()));
}
/**
* Renders the top navigation.
*
* @returns React element
*/
_renderTopNav() {
var _a;
const activeBranch = this.props.branches.filter(branch => branch.is_current_branch);
const hasRemote = this.state.hasRemote;
const hasUpstream = ((_a = activeBranch[0]) === null || _a === void 0 ? void 0 : _a.upstream) !== null;
return (React.createElement("div", { className: toolbarNavClass },
React.createElement("span", { className: spacer }),
React.createElement(Badge, { className: badgeClass, variant: "dot", invisible: !hasRemote || this.props.nCommitsBehind === 0 },
React.createElement(ActionButton, { className: toolbarButtonClass, disabled: !hasRemote, icon: pullIcon, onClick: hasRemote ? this._onPullClick : undefined, title: hasRemote
? this.props.trans.__('Pull latest changes') +
(this.props.nCommitsBehind > 0
? this.props.trans.__(' (behind by %1 commits)', this.props.nCommitsBehind)
: '')
: this.props.trans.__('No remote repository defined') })),
React.createElement(Badge, { className: badgeClass, variant: "dot", invisible: !hasRemote || (this.props.nCommitsAhead === 0 && hasUpstream) },
React.createElement(ActionButton, { className: toolbarButtonClass, disabled: !hasRemote, icon: pushIcon, onClick: hasRemote ? this._onPushClick : undefined, title: hasRemote
? hasUpstream
? this.props.trans.__('Push committed changes') +
(this.props.nCommitsAhead > 0
? this.props.trans.__(' (ahead by %1 commits)', this.props.nCommitsAhead)
: '')
: this.props.trans.__('Publish branch')
: this.props.trans.__('No remote repository defined') })),
React.createElement(ActionButton, { className: toolbarButtonClass, icon: refreshIcon, onClick: this._onRefreshClick, disabled: this.state.refreshInProgress, title: this.props.trans.__('Refresh the repository to detect local and remote changes') })));
}
/**
* Renders a repository menu.
*
* @returns React element
*/
_renderRepoMenu() {
const hasSubmodules = !(this.props.submodules.length === 0);
const repositoryName = PathExt.basename(this.props.repository || PageConfig.getOption('serverRoot')) || 'Jupyter Server Root';
return (React.createElement("div", { className: toolbarMenuWrapperClass },
React.createElement("button", { disabled: !hasSubmodules, className: classes(toolbarMenuButtonClass, hasSubmodules && toolbarMenuButtonEnabledClass), title: this.props.trans.__('Current repository: %1', PageConfig.getOption('serverRoot') + '/' + this.props.repository), onClick: this._onRepoClick },
React.createElement(desktopIcon.react, { tag: "span", className: toolbarMenuButtonIconClass }),
React.createElement("div", { className: toolbarMenuButtonTitleWrapperClass },
React.createElement("p", { className: toolbarMenuButtonTitleClass }, this.props.trans.__('Current Repository')),
React.createElement("p", { className: toolbarMenuButtonSubtitleClass }, repositoryName)),
hasSubmodules &&
(this.state.repoMenu ? (React.createElement(caretUpIcon.react, { tag: "span", className: toolbarMenuButtonIconClass })) : (React.createElement(caretDownIcon.react, { tag: "span", className: toolbarMenuButtonIconClass })))),
this.state.repoMenu ? this._renderSubmodules() : null));
}
/**
* Renders a branch menu.
*
* @returns React element
*/
_renderBranchMenu() {
let branchTitle = '';
if (this.props.model.pathRepository === null) {
return null;
}
switch (this.props.model.status.state) {
case Git.State.CHERRY_PICKING:
branchTitle = this.props.trans.__('Cherry picking in');
break;
case Git.State.DETACHED:
branchTitle = this.props.trans.__('Detached Head at');
break;
case Git.State.MERGING:
branchTitle = this.props.trans.__('Merging in');
break;
case Git.State.REBASING:
branchTitle = this.props.trans.__('Rebasing');
break;
default:
branchTitle = this.props.trans.__('Current Branch');
}
return (React.createElement("div", { className: toolbarMenuWrapperClass },
React.createElement("button", { className: classes(toolbarMenuButtonClass, toolbarMenuButtonEnabledClass), title: this.props.trans.__('Manage branches and tags'), onClick: this._onBranchClick },
React.createElement(branchIcon.react, { tag: "span", className: toolbarMenuButtonIconClass }),
React.createElement("div", { className: toolbarMenuButtonTitleWrapperClass },
React.createElement("p", { className: toolbarMenuButtonTitleClass }, branchTitle),
React.createElement("p", { className: toolbarMenuButtonSubtitleClass }, this.props.currentBranch || '')),
this.state.branchMenu ? (React.createElement(caretUpIcon.react, { tag: "span", className: toolbarMenuButtonIconClass })) : (React.createElement(caretDownIcon.react, { tag: "span", className: toolbarMenuButtonIconClass }))),
this.state.branchMenu ? this._renderTabs() : null));
}
_renderTabs() {
return (React.createElement(React.Fragment, null,
React.createElement(Tabs, { classes: {
root: tabsClass,
indicator: tabIndicatorClass
}, value: this.state.tab, onChange: (event, tab) => {
this.setState({
tab: tab
});
} },
React.createElement(Tab, { classes: {
root: tabClass,
selected: selectedTabClass
}, title: this.props.trans.__('View branches'), label: this.props.trans.__('Branches'), disableFocusRipple: true, disableRipple: true }),
React.createElement(Tab, { classes: {
root: tabClass,
selected: selectedTabClass
}, title: this.props.trans.__('View tags'), label: this.props.trans.__('Tags'), disableFocusRipple: true, disableRipple: true })),
this.state.tab === 0 ? this._renderBranches() : this._renderTags()));
}
_renderBranches() {
return (React.createElement(BranchMenu, { currentBranch: this.props.currentBranch || '', branches: this.props.branches, branching: this.props.branching, commands: this.props.commands, model: this.props.model, trans: this.props.trans }));
}
_renderTags() {
return (React.createElement(TagMenu, { pastCommits: this.props.pastCommits, tagsList: this.props.tagsList, model: this.props.model, branching: this.props.branching, trans: this.props.trans }));
}
_renderSubmodules() {
return (React.createElement(SubmoduleMenu, { model: this.props.model, submodules: this.props.submodules, trans: this.props.trans }));
}
}