UNPKG

@jupyterlab/git

Version:

A JupyterLab extension for version control using git

241 lines (240 loc) 12.2 kB
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 })); } }