mirror of
https://github.com/Kong/insomnia
synced 2024-11-12 17:26:32 +00:00
539 lines
15 KiB
JavaScript
539 lines
15 KiB
JavaScript
// @flow
|
|
import * as React from 'react';
|
|
import autobind from 'autobind-decorator';
|
|
import classnames from 'classnames';
|
|
import { Dropdown, DropdownButton, DropdownDivider, DropdownItem } from '../base/dropdown';
|
|
import type { Workspace } from '../../../models/workspace';
|
|
import { showAlert, showModal } from '../modals';
|
|
import SyncStagingModal from '../modals/sync-staging-modal';
|
|
import HelpTooltip from '../help-tooltip';
|
|
import Link from '../base/link';
|
|
import SyncHistoryModal from '../modals/sync-history-modal';
|
|
import SyncShareModal from '../modals/sync-share-modal';
|
|
import SyncBranchesModal from '../modals/sync-branches-modal';
|
|
import VCS from '../../../sync/vcs';
|
|
import type { Project, Snapshot, Status, StatusCandidate } from '../../../sync/types';
|
|
import ErrorModal from '../modals/error-modal';
|
|
import Tooltip from '../tooltip';
|
|
import LoginModal from '../modals/login-modal';
|
|
import * as session from '../../../account/session';
|
|
import PromptButton from '../base/prompt-button';
|
|
import * as db from '../../../common/database';
|
|
|
|
// Stop refreshing if user hasn't been active in this long
|
|
const REFRESH_USER_ACTIVITY = 1000 * 60 * 10;
|
|
|
|
// Refresh dropdown periodically
|
|
const REFRESH_PERIOD = 1000 * 60 * 1;
|
|
|
|
type Props = {
|
|
workspace: Workspace,
|
|
vcs: VCS,
|
|
syncItems: Array<StatusCandidate>,
|
|
|
|
// Optional
|
|
className?: string,
|
|
};
|
|
|
|
type State = {
|
|
currentBranch: string,
|
|
localBranches: Array<string>,
|
|
compare: {
|
|
ahead: number,
|
|
behind: number,
|
|
},
|
|
status: Status,
|
|
initializing: boolean,
|
|
historyCount: number,
|
|
loadingPull: boolean,
|
|
loadingProjectPull: boolean,
|
|
loadingPush: boolean,
|
|
remoteProjects: Array<Project>,
|
|
};
|
|
|
|
@autobind
|
|
class SyncDropdown extends React.PureComponent<Props, State> {
|
|
checkInterval: IntervalID;
|
|
refreshOnNextSyncItems = false;
|
|
lastUserActivity = Date.now();
|
|
|
|
constructor(props: Props) {
|
|
super(props);
|
|
this.state = {
|
|
localBranches: [],
|
|
currentBranch: '',
|
|
compare: {
|
|
ahead: 0,
|
|
behind: 0,
|
|
},
|
|
historyCount: 0,
|
|
initializing: true,
|
|
loadingPull: false,
|
|
loadingPush: false,
|
|
loadingProjectPull: false,
|
|
status: {
|
|
key: 'n/a',
|
|
stage: {},
|
|
unstaged: {},
|
|
},
|
|
remoteProjects: [],
|
|
};
|
|
}
|
|
|
|
async refreshMainAttributes(extraState?: Object = {}) {
|
|
const { vcs, syncItems, workspace } = this.props;
|
|
|
|
if (!vcs.hasProject()) {
|
|
const remoteProjects = await vcs.remoteProjects();
|
|
const matchedProjects = remoteProjects.filter(p => p.rootDocumentId === workspace._id);
|
|
this.setState({ remoteProjects: matchedProjects });
|
|
return;
|
|
}
|
|
|
|
const localBranches = (await vcs.getBranches()).sort();
|
|
const currentBranch = await vcs.getBranch();
|
|
const historyCount = await vcs.getHistoryCount();
|
|
const status = await vcs.status(syncItems, {});
|
|
|
|
const newState = {
|
|
status,
|
|
historyCount,
|
|
localBranches,
|
|
currentBranch,
|
|
...extraState,
|
|
};
|
|
|
|
// Do the remote stuff
|
|
if (session.isLoggedIn()) {
|
|
try {
|
|
newState.compare = await vcs.compareRemoteBranch();
|
|
} catch (err) {
|
|
console.log('Failed to compare remote branches', err.message);
|
|
}
|
|
}
|
|
|
|
this.setState(newState);
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.setState({ initializing: true });
|
|
this.refreshMainAttributes()
|
|
.catch(err => console.log('[sync_menu] Error refreshing sync state', err))
|
|
.finally(() => this.setState({ initializing: false }));
|
|
|
|
// Refresh but only if the user has been active in the last n minutes
|
|
this.checkInterval = setInterval(async () => {
|
|
if (Date.now() - this.lastUserActivity < REFRESH_USER_ACTIVITY) {
|
|
await this.refreshMainAttributes();
|
|
}
|
|
}, REFRESH_PERIOD);
|
|
|
|
document.addEventListener('mousemove', this._handleUserActivity);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
clearInterval(this.checkInterval);
|
|
document.removeEventListener('mousemove', this._handleUserActivity);
|
|
}
|
|
|
|
componentDidUpdate(prevProps: Props) {
|
|
const { vcs, syncItems } = this.props;
|
|
|
|
// Update if new sync items
|
|
if (syncItems !== prevProps.syncItems) {
|
|
if (vcs.hasProject()) {
|
|
vcs.status(syncItems, {}).then(status => {
|
|
this.setState({ status });
|
|
});
|
|
}
|
|
|
|
if (this.refreshOnNextSyncItems) {
|
|
this.refreshMainAttributes();
|
|
this.refreshOnNextSyncItems = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
_handleUserActivity() {
|
|
this.lastUserActivity = Date.now();
|
|
}
|
|
|
|
_handleShowBranchesModal() {
|
|
showModal(SyncBranchesModal, {
|
|
onHide: this.refreshMainAttributes,
|
|
});
|
|
}
|
|
|
|
_handleShowStagingModal() {
|
|
showModal(SyncStagingModal, {
|
|
onSnapshot: async () => {
|
|
await this.refreshMainAttributes();
|
|
},
|
|
handlePush: async () => {
|
|
await this._handlePushChanges();
|
|
},
|
|
});
|
|
}
|
|
|
|
static _handleShowSharingModal() {
|
|
showModal(SyncShareModal);
|
|
}
|
|
|
|
static _handleShowLoginModal() {
|
|
showModal(LoginModal);
|
|
}
|
|
|
|
async _handlePushChanges() {
|
|
const { vcs } = this.props;
|
|
this.setState({ loadingPush: true });
|
|
|
|
try {
|
|
await vcs.push();
|
|
} catch (err) {
|
|
showModal(ErrorModal, {
|
|
title: 'Push Error',
|
|
message: err.message,
|
|
});
|
|
}
|
|
|
|
await this.refreshMainAttributes({ loadingPush: false });
|
|
}
|
|
|
|
async _handlePullChanges() {
|
|
const { vcs, syncItems } = this.props;
|
|
|
|
this.setState({ loadingPull: true });
|
|
try {
|
|
const delta = await vcs.pull(syncItems);
|
|
await db.batchModifyDocs(delta);
|
|
this.refreshOnNextSyncItems = true;
|
|
} catch (err) {
|
|
showModal(ErrorModal, {
|
|
title: 'Pull Error',
|
|
message: err.message,
|
|
});
|
|
}
|
|
this.setState({ loadingPull: false });
|
|
}
|
|
|
|
async _handleRollback(snapshot: Snapshot) {
|
|
const { vcs, syncItems } = this.props;
|
|
const delta = await vcs.rollback(snapshot.id, syncItems);
|
|
await db.batchModifyDocs(delta);
|
|
this.refreshOnNextSyncItems = true;
|
|
}
|
|
|
|
async _handleRevert() {
|
|
const { vcs, syncItems } = this.props;
|
|
|
|
try {
|
|
const delta = await vcs.rollbackToLatest(syncItems);
|
|
await db.batchModifyDocs(delta);
|
|
} catch (err) {
|
|
showModal(ErrorModal, {
|
|
title: 'Revert Error',
|
|
message: err.message,
|
|
});
|
|
}
|
|
}
|
|
|
|
_handleShowHistoryModal() {
|
|
showModal(SyncHistoryModal, {
|
|
handleRollback: this._handleRollback,
|
|
});
|
|
}
|
|
|
|
async _handleOpen() {
|
|
await this.refreshMainAttributes();
|
|
}
|
|
|
|
async _handleEnableSync() {
|
|
const { vcs, workspace } = this.props;
|
|
await vcs.switchAndCreateProjectIfNotExist(workspace._id, workspace.name);
|
|
}
|
|
|
|
async _handleSetProject(p: Project) {
|
|
const { vcs } = this.props;
|
|
this.setState({ loadingProjectPull: true });
|
|
await vcs.setProject(p);
|
|
await vcs.checkout([], 'master');
|
|
|
|
// Pull changes
|
|
await vcs.pull([]); // There won't be any existing docs since it's a new pull
|
|
const flushId = await db.bufferChanges();
|
|
for (const doc of await vcs.allDocuments()) {
|
|
await db.upsert(doc);
|
|
}
|
|
await db.flushChanges(flushId);
|
|
|
|
await this.refreshMainAttributes({ loadingProjectPull: false });
|
|
}
|
|
|
|
async _handleSwitchBranch(branch: string) {
|
|
const { vcs, syncItems } = this.props;
|
|
try {
|
|
const delta = await vcs.checkout(syncItems, branch);
|
|
await db.batchModifyDocs(delta);
|
|
} catch (err) {
|
|
showAlert({
|
|
title: 'Branch Switch Error',
|
|
message: err.message,
|
|
});
|
|
}
|
|
|
|
// We can't refresh now because we won't yet have the new syncItems
|
|
this.refreshOnNextSyncItems = true;
|
|
|
|
// Still need to do this in case sync items don't change
|
|
this.setState({ currentBranch: branch });
|
|
}
|
|
|
|
renderBranch(branch: string) {
|
|
const { currentBranch } = this.state;
|
|
|
|
const icon =
|
|
branch === currentBranch ? <i className="fa fa-tag" /> : <i className="fa fa-empty" />;
|
|
|
|
const isCurrentBranch = branch === currentBranch;
|
|
return (
|
|
<DropdownItem
|
|
key={branch}
|
|
onClick={isCurrentBranch ? null : () => this._handleSwitchBranch(branch)}
|
|
className={classnames({ bold: isCurrentBranch })}
|
|
title={isCurrentBranch ? null : `Switch to "${branch}"`}>
|
|
{icon}
|
|
{branch}
|
|
</DropdownItem>
|
|
);
|
|
}
|
|
|
|
renderButton() {
|
|
const {
|
|
currentBranch,
|
|
compare: { ahead, behind },
|
|
initializing,
|
|
status,
|
|
loadingPull,
|
|
loadingPush,
|
|
} = this.state;
|
|
|
|
const canPush = ahead > 0;
|
|
const canPull = behind > 0;
|
|
const canCreateSnapshot =
|
|
Object.keys(status.stage).length > 0 || Object.keys(status.unstaged).length > 0;
|
|
|
|
const loadIcon = <i className="fa fa-spin fa-refresh fa--fixed-width" />;
|
|
|
|
const pullToolTipMsg = canPull
|
|
? `There ${behind === 1 ? 'is' : 'are'} ${behind} snapshot${behind === 1 ? '' : 's'} to pull`
|
|
: 'No changes to pull';
|
|
|
|
const pushToolTipMsg = canPush
|
|
? `There ${ahead === 1 ? 'is' : 'are'} ${ahead} snapshot${ahead === 1 ? '' : 's'} to push`
|
|
: 'No changes to push';
|
|
|
|
const snapshotToolTipMsg = canCreateSnapshot ? 'Local changes made' : 'No local changes made';
|
|
|
|
if (currentBranch === null) {
|
|
return <React.Fragment>Sync</React.Fragment>;
|
|
}
|
|
|
|
return (
|
|
<DropdownButton className="btn btn--compact wide text-left overflow-hidden row-spaced">
|
|
<div className="ellipsis">
|
|
<i className="fa fa-code-fork space-right" />{' '}
|
|
{initializing ? 'Initializing...' : currentBranch}
|
|
</div>
|
|
<div className="space-left">
|
|
<Tooltip message={snapshotToolTipMsg} delay={800}>
|
|
<i
|
|
className={classnames('icon fa fa-cube fa--fixed-width', {
|
|
'super-duper-faint': !canCreateSnapshot,
|
|
})}
|
|
/>
|
|
</Tooltip>
|
|
|
|
{/* Only show cloud icons if logged in */}
|
|
{session.isLoggedIn() && (
|
|
<React.Fragment>
|
|
{loadingPull ? (
|
|
loadIcon
|
|
) : (
|
|
<Tooltip message={pullToolTipMsg} delay={800}>
|
|
<i
|
|
className={classnames('fa fa-cloud-download fa--fixed-width', {
|
|
'super-duper-faint': !canPull,
|
|
})}
|
|
/>
|
|
</Tooltip>
|
|
)}
|
|
|
|
{loadingPush ? (
|
|
loadIcon
|
|
) : (
|
|
<Tooltip message={pushToolTipMsg} delay={800}>
|
|
<i
|
|
className={classnames('fa fa-cloud-upload fa--fixed-width', {
|
|
'super-duper-faint': !canPush,
|
|
})}
|
|
/>
|
|
</Tooltip>
|
|
)}
|
|
</React.Fragment>
|
|
)}
|
|
</div>
|
|
</DropdownButton>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
if (!session.isLoggedIn()) {
|
|
return null;
|
|
}
|
|
|
|
const { className, vcs } = this.props;
|
|
const {
|
|
localBranches,
|
|
currentBranch,
|
|
status,
|
|
historyCount,
|
|
loadingPull,
|
|
loadingPush,
|
|
loadingProjectPull,
|
|
remoteProjects,
|
|
compare: { ahead, behind },
|
|
} = this.state;
|
|
|
|
const canCreateSnapshot =
|
|
Object.keys(status.stage).length > 0 || Object.keys(status.unstaged).length > 0;
|
|
|
|
const visibleBranches = localBranches.filter(b => !b.match(/\.hidden$/));
|
|
|
|
const syncMenuHeader = (
|
|
<DropdownDivider>
|
|
Insomnia Sync{' '}
|
|
<HelpTooltip>
|
|
Sync and collaborate on workspaces{' '}
|
|
<Link href="https://support.insomnia.rest/article/67-version-control">
|
|
<span className="no-wrap">
|
|
<br />
|
|
Documentation <i className="fa fa-external-link" />
|
|
</span>
|
|
</Link>
|
|
</HelpTooltip>
|
|
</DropdownDivider>
|
|
);
|
|
|
|
if (loadingProjectPull) {
|
|
return (
|
|
<div className={className}>
|
|
<button className="btn btn--compact wide">
|
|
<i className="fa fa-refresh fa-spin" /> Initializing
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!vcs.hasProject()) {
|
|
return (
|
|
<div className={className}>
|
|
<Dropdown className="wide tall" onOpen={this._handleOpen}>
|
|
<DropdownButton className="btn btn--compact wide">
|
|
<i className="fa fa-code-fork " /> Setup Sync
|
|
</DropdownButton>
|
|
{syncMenuHeader}
|
|
{remoteProjects.length === 0 && (
|
|
<DropdownItem onClick={this._handleEnableSync}>
|
|
<i className="fa fa-plus-circle" /> Create Local Project
|
|
</DropdownItem>
|
|
)}
|
|
{remoteProjects.map(p => (
|
|
<DropdownItem key={p.id} onClick={() => this._handleSetProject(p)}>
|
|
<i className="fa fa-cloud-download" /> Pull <strong>{p.name}</strong>
|
|
</DropdownItem>
|
|
))}
|
|
</Dropdown>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={className}>
|
|
<Dropdown className="wide tall" onOpen={this._handleOpen}>
|
|
{this.renderButton()}
|
|
|
|
{syncMenuHeader}
|
|
|
|
{!session.isLoggedIn() && (
|
|
<DropdownItem onClick={SyncDropdown._handleShowLoginModal}>
|
|
<i className="fa fa-sign-in" /> Log In
|
|
</DropdownItem>
|
|
)}
|
|
|
|
<DropdownItem onClick={SyncDropdown._handleShowSharingModal}>
|
|
<i className="fa fa-users" />
|
|
Share Settings
|
|
</DropdownItem>
|
|
|
|
<DropdownItem onClick={this._handleShowBranchesModal}>
|
|
<i className="fa fa-code-fork" />
|
|
Branches
|
|
</DropdownItem>
|
|
|
|
<DropdownDivider>Local Branches</DropdownDivider>
|
|
{visibleBranches.map(this.renderBranch)}
|
|
|
|
<DropdownDivider>{currentBranch}</DropdownDivider>
|
|
|
|
<DropdownItem onClick={this._handleShowHistoryModal} disabled={historyCount === 0}>
|
|
<i className="fa fa-clock-o" />
|
|
History
|
|
</DropdownItem>
|
|
|
|
<DropdownItem
|
|
onClick={this._handleRevert}
|
|
buttonClass={PromptButton}
|
|
stayOpenAfterClick
|
|
disabled={!canCreateSnapshot || historyCount === 0}>
|
|
<i className="fa fa-undo" />
|
|
Revert Changes
|
|
</DropdownItem>
|
|
|
|
<DropdownItem onClick={this._handleShowStagingModal} disabled={!canCreateSnapshot}>
|
|
<i className="fa fa-cube" />
|
|
Create Snapshot
|
|
</DropdownItem>
|
|
|
|
<DropdownItem onClick={this._handlePullChanges} disabled={behind === 0 || loadingPull}>
|
|
{loadingPull ? (
|
|
<React.Fragment>
|
|
<i className="fa fa-spin fa-refresh" /> Pulling Snapshots...
|
|
</React.Fragment>
|
|
) : (
|
|
<React.Fragment>
|
|
<i className="fa fa-cloud-upload" /> Pull {behind || ''} Snapshot
|
|
{behind === 1 ? '' : 's'}
|
|
</React.Fragment>
|
|
)}
|
|
</DropdownItem>
|
|
|
|
<DropdownItem onClick={this._handlePushChanges} disabled={ahead === 0 || loadingPush}>
|
|
{loadingPush ? (
|
|
<React.Fragment>
|
|
<i className="fa fa-spin fa-refresh" /> Pushing Snapshots...
|
|
</React.Fragment>
|
|
) : (
|
|
<React.Fragment>
|
|
<i className="fa fa-cloud-upload" /> Push {ahead || ''} Snapshot
|
|
{ahead === 1 ? '' : 's'}
|
|
</React.Fragment>
|
|
)}
|
|
</DropdownItem>
|
|
</Dropdown>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default SyncDropdown;
|