Cleanup before major team features (#72)

* Started on UI makeover for teams

* Some small tweaks and fixes

* Adjusted settings
This commit is contained in:
Gregory Schier 2016-12-21 15:37:48 -08:00 committed by GitHub
parent c98729e9e3
commit d58edf1d25
41 changed files with 572 additions and 936 deletions

View File

@ -0,0 +1,65 @@
/*
* This is a stupid little mock that basically disabled encryption.
* The reason it is needed is because the Forge module loader doesn't
* play along with Jest.
*/
module.exports = function (options) {
const forge = require('../../node_modules/node-forge/js/forge')(options);
forge.util = {
hexToBytes: forge.hexToBytes,
bytesToHex: forge.bytesToHex,
createBuffer (text) {
return new Buffer(text);
}
};
forge.pkcs5 = {
pbkdf2 () {
}
};
forge.random = {
getBytesSync(n) {
let s = '';
for (let i = 0; i < n; i++) {
s += 'a';
}
return s;
}
};
forge.cipher = {
createCipher(alg, key) {
return {
start(config) {
this._config = config;
},
update(buffer) {
this._data = buffer;
},
finish() {
this.mode = {tag: 'tag'};
this.output = this._data;
}
};
},
createDecipher(alg, key) {
return {
start (config) {
this._config = config;
},
update (buffer) {
this.output = buffer;
},
finish () {
return true;
}
};
}
};
return forge;
};

View File

@ -9,12 +9,10 @@ export async function init (accountId) {
return;
}
process.nextTick(() => {
google.init(accountId, getAppPlatform(), getAppVersion());
segment.init();
await segment.init();
await google.init(accountId, getAppPlatform(), getAppVersion());
initialized = true;
});
initialized = true;
ipcRenderer.on('analytics-track-event', (_, args) => {
trackEvent(...args);

View File

@ -121,7 +121,7 @@ export const CONTENT_TYPE_OTHER = '';
export const contentTypesMap = {
[CONTENT_TYPE_JSON]: 'JSON',
[CONTENT_TYPE_XML]: 'XML',
[CONTENT_TYPE_FORM_DATA]: 'Form Data',
[CONTENT_TYPE_FORM_DATA]: 'Multipart Form',
[CONTENT_TYPE_FORM_URLENCODED]: 'Url Encoded',
[CONTENT_TYPE_FILE]: 'Binary File',
[CONTENT_TYPE_OTHER]: 'Other'

View File

@ -107,6 +107,8 @@ export function updateMimeType (request, mimeType, doCreate = false) {
body = newBodyForm(request.body.params || []);
} else if (mimeType === CONTENT_TYPE_FILE) {
body = newBodyFile('');
} else if (typeof mimeType !== 'string') {
body = newBodyRaw('');
} else {
body = newBodyRaw(request.body.text || '', mimeType);
}

View File

@ -22,11 +22,10 @@
"mime-types": "^2.1.12",
"mkdirp": "^0.5.1",
"nedb": "^1.8.0",
"node-forge": "^0.6.43",
"node-forge": "^0.6.46",
"nunjucks": "^3.0.0",
"raven": "^0.12.1",
"request": "^2.71.0",
"sjcl": "^1.0.6",
"srp": "git@github.com:getinsomnia/srp-js.git#6ebd8c3acfbcf69645e4c6e6f8f6b55138ff22c3",
"tough-cookie": "^2.3.1",
"traverse": "^0.6.6",

View File

@ -117,16 +117,5 @@
// SOME HELPERS
document.body.setAttribute('data-platform', process.platform);
</script>
<!-- Other Scripts -->
<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
<script>
(function () {
const key = process.env.INSOMNIA_ENV === 'development' ?
'pk_test_MbOhGu5jCPvr7Jt4VC6oySdH' :
'pk_live_lntbVSXY3v1RAytACIQJ5BBH';
Stripe.setPublishableKey(key);
})();
</script>
</body>
</html>

View File

@ -0,0 +1,30 @@
import * as crypt from '../crypt';
describe('deriveKey()', () => {
it('derives a key properly', async () => {
const result = await crypt.deriveKey('Password', 'email', 'salt');
const expected = '';
expect(result).toBe(expected);
})
});
describe('encryptRSA', () => {
it('encrypts and decrypts', () => {
const resultEncrypted = crypt.encryptAES('rawkey', 'Hello World!', 'additional data');
const resultDecrypted = crypt.decryptAES('rawkey', resultEncrypted);
const expectedEncrypted = {
ad: '6164646974696f6e616c2064617461',
d: '48656c6c6f253230576f726c6421',
iv: '616161616161616161616161',
t: '746167'
};
const expectedDecrypted = 'Hello World!';
expect(resultEncrypted).toEqual(expectedEncrypted);
expect(resultDecrypted).toEqual(expectedDecrypted);
})
});

View File

@ -1,7 +1,9 @@
import HKDF from 'hkdf';
import sjcl from 'sjcl';
import srp from 'srp';
import forge from 'node-forge';
import initForge from 'node-forge';
// Initialize Forge
const forge = initForge();
const DEFAULT_BYTE_LENGTH = 32;
const DEFAULT_PBKDF2_ITERATIONS = 1E5; // 100,000
@ -182,7 +184,7 @@ export async function generateAES256Key () {
);
return subtle.exportKey('jwk', key);
} else {
console.log('-- Using Falback Forge AES Key Generation --');
console.log('-- Using Fallback Forge AES Key Generation --');
const key = forge.util.bytesToHex(forge.random.getBytesSync(32));
return {
kty: 'oct',
@ -200,7 +202,7 @@ export async function generateAES256Key () {
* @returns Object
*/
export async function generateKeyPairJWK () {
// NOTE: Safari has crypto.webkitSubtle, but does not support RSA-OAEP-SHA256
// NOTE: Safari has crypto.webkitSubtle, but it does not support RSA-OAEP-SHA256
const subtle = window.crypto && window.crypto.subtle;
if (subtle) {
@ -298,9 +300,6 @@ function _b64UrlToHex (s) {
return forge.util.bytesToHex(atob(b64));
}
window.b64urltohex = _b64UrlToHex;
window.forge = forge;
/**
* Derive key from password
*
@ -329,26 +328,15 @@ async function _pbkdf2Passphrase (passphrase, salt) {
const derivedKeyRaw = await window.crypto.subtle.deriveBits(algo, k, DEFAULT_BYTE_LENGTH * 8);
return new Buffer(derivedKeyRaw).toString('hex');
} else {
console.log('-- Using SJCL PBKDF2 --');
const derivedKeyRaw = sjcl.misc.pbkdf2(
console.log('-- Using Forge PBKDF2 --');
const derivedKeyRaw = forge.pkcs5.pbkdf2(
passphrase,
sjcl.codec.hex.toBits(salt),
forge.util.hexToBytes(salt),
DEFAULT_PBKDF2_ITERATIONS,
DEFAULT_BYTE_LENGTH * 8,
sjcl.hash.sha1
DEFAULT_BYTE_LENGTH,
forge.md.sha256.create()
);
return sjcl.codec.hex.fromBits(derivedKeyRaw);
// NOTE: SJCL (above) is about 10x faster than Forge
// const derivedKeyRaw = forge.pkcs5.pbkdf2(
// passphrase,
// forge.util.hexToBytes(salt),
// DEFAULT_PBKDF2_ITERATIONS,
// DEFAULT_BYTE_LENGTH,
// forge.md.sha256.create()
// );
// const derivedKey = forge.util.bytesToHex(derivedKeyRaw);
return forge.util.bytesToHex(derivedKeyRaw);
}
}

View File

@ -233,7 +233,10 @@ export async function logout () {
trackEvent('Session', 'Logout');
}
/** Cancel the user's subscription Account */
export async function listTeams () {
return util.get('/api/teams');
}
export async function cancelAccount () {
await util.del('/api/billing/subscriptions');
trackEvent('Session', 'Cancel Account');

View File

@ -17,6 +17,7 @@ class RequestUrlBar extends Component {
_handleFormSubmit = e => {
e.preventDefault();
e.stopPropagation();
this.props.handleSend();
};
@ -117,8 +118,8 @@ class RequestUrlBar extends Component {
clearTimeout(this._sendInterval);
if (this.state.currentInterval) {
this.setState({currentInterval: null});
trackEvent('Request', 'Stop Send Interval');
}
trackEvent('Request', 'Stop Send Interval');
};
_handleStopTimeout = () => {
@ -129,6 +130,20 @@ class RequestUrlBar extends Component {
trackEvent('Request', 'Stop Send Timeout');
};
_handleClickSend = e => {
const metaPressed = isMac() ? e.metaKey : e.ctrlKey;
// If we're pressing a meta key, let the dropdown open
if (metaPressed) {
e.preventDefault(); // Don't submit the form
return;
}
// If we're not pressing a meta key, cancel dropdown and send the request
e.stopPropagation(); // Don't trigger the dropdown
this._handleFormSubmit(e);
};
componentDidMount () {
document.body.addEventListener('keydown', this._handleKeyDown);
document.body.addEventListener('keyup', this._handleKeyUp);
@ -140,7 +155,7 @@ class RequestUrlBar extends Component {
}
renderSendButton () {
const {showAdvanced, currentInterval, currentTimeout} = this.state;
const {currentInterval, currentTimeout} = this.state;
let cancelButton = null;
if (currentInterval) {
@ -164,22 +179,15 @@ class RequestUrlBar extends Component {
}
let sendButton;
if (!cancelButton && !showAdvanced) {
sendButton = (
<button key="send" type="submit" className="urlbar__send-btn">
Send
</button>
)
}
let dropdown = null;
if (!cancelButton) {
dropdown = (
sendButton = (
<Dropdown key="dropdown" className="tall" right={true}>
<DropdownButton className={classnames('urlbar__send-btn', {hide: !showAdvanced})}>
<i className="fa fa-caret-down"></i>
<DropdownButton className="urlbar__send-btn"
onClick={this._handleClickSend}
type="submit">
Send
</DropdownButton>
<DropdownDivider name="Basic"/>
<DropdownDivider>Basic</DropdownDivider>
<DropdownItem type="submit">
<i className="fa fa-arrow-circle-o-right"/> Send Now
<DropdownHint char="Enter"/>
@ -187,7 +195,7 @@ class RequestUrlBar extends Component {
<DropdownItem onClick={this._handleGenerateCode}>
<i className="fa fa-code"/> Generate Client Code
</DropdownItem>
<DropdownDivider name="Advanced"/>
<DropdownDivider>Advanced</DropdownDivider>
<DropdownItem onClick={this._handleSendAfterDelay}>
<i className="fa fa-clock-o"/> Send After Delay
</DropdownItem>
@ -201,7 +209,6 @@ class RequestUrlBar extends Component {
return [
cancelButton,
sendButton,
dropdown,
]
}

View File

@ -7,7 +7,6 @@ import CookiesModal from '../components/modals/CookiesModal';
import EnvironmentEditModal from '../components/modals/EnvironmentEditModal';
import GenerateCodeModal from '../components/modals/GenerateCodeModal';
import LoginModal from '../components/modals/LoginModal';
import PaymentModal from '../components/modals/PaymentModal';
import PaymentNotificationModal from '../components/modals/PaymentNotificationModal';
import PromptModal from '../components/modals/PromptModal';
import RequestCreateModal from '../components/modals/RequestCreateModal';
@ -16,10 +15,10 @@ import RequestSwitcherModal from '../components/modals/RequestSwitcherModal';
import ResponsePane from './ResponsePane';
import SettingsModal from '../components/modals/SettingsModal';
import Sidebar from './sidebar/Sidebar';
import SignupModal from '../components/modals/SignupModal';
import SyncLogsModal from '../components/modals/SyncLogsModal';
import WorkspaceEnvironmentsEditModal from '../components/modals/WorkspaceEnvironmentsEditModal';
import WorkspaceSettingsModal from '../components/modals/WorkspaceSettingsModal';
import WorkspaceShareSettingsModal from '../components/modals/WorkspaceShareSettingsModal';
import * as models from '../../models/index';
import {updateMimeType} from '../../models/request';
import {trackEvent} from '../../analytics/index';
@ -276,14 +275,15 @@ class Wrapper extends Component {
<SyncLogsModal ref={registerModal}/>
<LoginModal ref={registerModal}/>
<PromptModal ref={registerModal}/>
<SignupModal ref={registerModal}/>
<PaymentModal ref={registerModal}/>
<RequestCreateModal ref={registerModal}/>
<PaymentNotificationModal ref={registerModal}/>
<WorkspaceSettingsModal
ref={registerModal}
workspace={activeWorkspace}
handleRemoveWorkspace={this._handleRemoveActiveWorkspace}/>
<WorkspaceShareSettingsModal
ref={registerModal}
workspace={activeWorkspace}/>
<EnvironmentEditModal
ref={registerModal}
onChange={models.requestGroup.update}

View File

@ -1,5 +1,4 @@
import React, {Component, PropTypes} from 'react';
import * as misc from '../../../common/misc';
class Editable extends Component {
state = {editing: false};
@ -25,7 +24,7 @@ class Editable extends Component {
}
};
_handleEditEnd = async () => {
_handleEditEnd = () => {
const value = this._input.value.trim();
if (!value) {
@ -37,8 +36,7 @@ class Editable extends Component {
// This timeout prevents the UI from showing the old value after submit.
// It should give the UI enough time to redraw the new value.
await misc.delay(100);
this.setState({editing: false});
setTimeout(async () => this.setState({editing: false}), 100);
};
_handleEditKeyDown = e => {

View File

@ -102,7 +102,7 @@ class Dropdown extends Component {
}
render () {
const {right, outline, wide, className, style} = this.props;
const {right, outline, wide, className, style, children} = this.props;
const {dropUp, open} = this.state;
const classes = classnames(
@ -117,7 +117,9 @@ class Dropdown extends Component {
const dropdownButtons = [];
const dropdownItems = [];
for (const child of this._getFlattenedChildren(this.props.children)) {
const allChildren = this._getFlattenedChildren(Array.isArray(children) ? children : [children]);
for (const child of allChildren) {
if (child.type === DropdownButton) {
dropdownButtons.push(child);
} else if (child.type === DropdownItem) {
@ -127,13 +129,11 @@ class Dropdown extends Component {
}
}
let children = [];
let finalChildren = [];
if (dropdownButtons.length !== 1) {
console.error(`Dropdown needs exactly one DropdownButton! Got ${dropdownButtons.length}`, this.props);
} else if (dropdownItems.length === 0) {
children = dropdownButtons;
} else {
children = [
finalChildren = [
dropdownButtons[0],
<ul key="items" ref={this._addDropdownListRef}>
{dropdownItems}
@ -146,7 +146,7 @@ class Dropdown extends Component {
className={classes}
onClick={this._handleClick}
onMouseDown={this._handleMouseDown}>
{children}
{finalChildren}
<div className="dropdown__backdrop"></div>
</div>
)

View File

@ -1,23 +1,21 @@
import React, {PropTypes} from 'react';
import React from 'react';
import classnames from 'classnames';
const DropdownDivider = ({name}) => {
const DropdownDivider = ({children}) => {
const classes = classnames(
'dropdown__divider',
{'dropdown__divider--no-name': !name}
{'dropdown__divider--no-name': !children}
);
return (
<li className={classes}>
<span className="dropdown__divider__label">
{name}
{children}
</span>
</li>
)
};
DropdownDivider.propTypes = {
name: PropTypes.any
};
DropdownDivider.propTypes = {};
export default DropdownDivider;

View File

@ -34,14 +34,14 @@ class ContentTypeDropdown extends Component {
<DropdownButton className={className}>
{children}
</DropdownButton>
<DropdownDivider name={<span><i className="fa fa-bars"></i> Form Data</span>}/>
<DropdownDivider><span><i className="fa fa-bars"></i> Form Data</span></DropdownDivider>
{this._renderDropdownItem(constants.CONTENT_TYPE_FORM_DATA)}
{this._renderDropdownItem(constants.CONTENT_TYPE_FORM_URLENCODED)}
<DropdownDivider name={<span><i className="fa fa-code"></i> Raw Text</span>}/>
<DropdownDivider><span><i className="fa fa-code"></i> Raw Text</span></DropdownDivider>
{this._renderDropdownItem(constants.CONTENT_TYPE_JSON)}
{this._renderDropdownItem(constants.CONTENT_TYPE_XML)}
{this._renderDropdownItem(constants.CONTENT_TYPE_OTHER)}
<DropdownDivider name={<span><i className="fa fa-ellipsis-h"></i> Other</span>}/>
<DropdownDivider><span><i className="fa fa-ellipsis-h"></i> Other</span></DropdownDivider>
{this._renderDropdownItem(constants.CONTENT_TYPE_FILE)}
{this._renderDropdownItem(EMPTY_MIME_TYPE, 'No Body')}
</Dropdown>

View File

@ -2,11 +2,10 @@ import React, {Component, PropTypes} from 'react';
import {ipcRenderer} from 'electron';
import classnames from 'classnames';
import EnvironmentsModal from '../modals/WorkspaceEnvironmentsEditModal';
import {Dropdown, DropdownDivider, DropdownButton, DropdownItem} from '../base/dropdown';
import {Dropdown, DropdownDivider, DropdownButton, DropdownItem, DropdownHint} from '../base/dropdown';
import {showModal} from '../modals/index';
import {trackEvent} from '../../../analytics/index';
const EnvironmentsDropdown = ({
className,
workspace,
@ -17,7 +16,9 @@ const EnvironmentsDropdown = ({
}) => {
// NOTE: Base environment might not exist if the users hasn't managed environments yet.
const baseEnvironment = environments.find(e => e.parentId === workspace._id);
const subEnvironments = environments.filter(e => e.parentId === (baseEnvironment && baseEnvironment._id));
const subEnvironments = environments.filter(
e => e.parentId === (baseEnvironment && baseEnvironment._id)
);
let description;
if (!activeEnvironment || activeEnvironment === baseEnvironment) {
@ -35,7 +36,7 @@ const EnvironmentsDropdown = ({
<i className="fa fa-caret-down"></i>
</div>
</DropdownButton>
<DropdownDivider name="Switch Environment"/>
<DropdownDivider>Switch Environment</DropdownDivider>
{subEnvironments.map(environment => (
<DropdownItem key={environment._id}
onClick={e => {
@ -51,9 +52,10 @@ const EnvironmentsDropdown = ({
}}>
<i className="fa fa-empty"></i> No Environment
</DropdownItem>
<DropdownDivider name="General"/>
<DropdownDivider>General</DropdownDivider>
<DropdownItem onClick={e => showModal(EnvironmentsModal, workspace)}>
<i className="fa fa-wrench"></i> Manage Environments
<DropdownHint char="E"/>
</DropdownItem>
</Dropdown>
)

View File

@ -16,14 +16,14 @@ class PreviewModeDropdown extends PureComponent {
<DropdownButton className="tall">
<i className="fa fa-caret-down"></i>
</DropdownButton>
<DropdownDivider name="Preview Mode"/>
<DropdownDivider>Preview Mode</DropdownDivider>
{PREVIEW_MODES.map(mode => (
<DropdownItem key={mode} onClick={this._handleClick} value={mode}>
{previewMode === mode ? <i className="fa fa-check"/> : <i className="fa fa-empty"/>}
{getPreviewModeName(mode)}
</DropdownItem>
))}
<DropdownDivider name="Actions"/>
<DropdownDivider>Actions</DropdownDivider>
<DropdownItem onClick={download}>
<i className="fa fa-save"></i>
Save to File

View File

@ -5,6 +5,8 @@ import PromptModal from '../modals/PromptModal';
import * as models from '../../../models';
import {showModal} from '../modals/index';
import {trackEvent} from '../../../analytics/index';
import AlertModal from '../modals/AlertModal';
import {MOD_SYM} from '../../../common/constants';
class RequestActionsDropdown extends Component {
@ -19,6 +21,19 @@ class RequestActionsDropdown extends Component {
trackEvent('Request', 'Generate Code', 'Request Action');
};
_handleAdvancedSend = () => {
trackEvent('Request', 'Advanced Send Hint', 'Request Action');
showModal(AlertModal, {
title: 'Advanced Sending',
message: (
<div>
For advanced sending options, hold <code>{MOD_SYM}</code> while
clicking the send button next to the Url.
</div>
)
});
};
_handlePromptUpdateName = async () => {
const {request} = this.props;
@ -57,6 +72,9 @@ class RequestActionsDropdown extends Component {
<DropdownItem onClick={this._handleGenerateCode}>
<i className="fa fa-code"></i> Generate Code
</DropdownItem>
<DropdownItem onClick={this._handleAdvancedSend}>
<i className="fa fa-refresh"></i> Advanced Sending
</DropdownItem>
<DropdownItem buttonClass={PromptButton} onClick={this._handleRemove} addIcon={true}>
<i className="fa fa-trash-o"></i> Delete
</DropdownItem>

View File

@ -82,14 +82,14 @@ class ResponseHistoryDropdown extends Component {
<i className="fa fa-history"/> :
<i className="fa fa-thumb-tack"/>}
</DropdownButton>
<DropdownDivider name="Response History"/>
<DropdownDivider>Response History</DropdownDivider>
<DropdownItem buttonClass={PromptButton}
addIcon={true}
onClick={this._handleDeleteResponses}>
<i className="fa fa-trash-o"/>
Clear History
</DropdownItem>
<DropdownDivider name="Past Responses"/>
<DropdownDivider>Past Responses</DropdownDivider>
{responses.map(this.renderDropdownItem)}
</Dropdown>
)

View File

@ -6,24 +6,21 @@ import * as syncStorage from '../../../sync/storage';
import * as session from '../../../sync/session';
import * as sync from '../../../sync';
import {trackEvent} from '../../../analytics';
import SettingsModal, {TAB_PLUS} from '../modals/SettingsModal';
import LoginModal from '../modals/LoginModal';
import PromptButton from '../base/PromptButton';
class SyncDropdown extends Component {
state = {
loggedIn: null,
syncData: null,
loading: false,
hide: false,
};
_handleHideMenu () {
this.setState({hide: true});
trackEvent('Sync', 'Hide Menu')
}
_trackShowMenu = () => trackEvent('Sync', 'Show Menu', 'Authenticated');
_handleShowLogs = () => showModal(SyncLogsModal);
_handleToggleSyncMode = async () => {
const {syncData} = this.state;
const resourceGroupId = syncData.resourceGroupId;
async _handleToggleSyncMode (resourceGroupId) {
const config = await sync.getOrCreateConfig(resourceGroupId);
let syncMode = config.syncMode === syncStorage.SYNC_MODE_OFF ?
@ -35,13 +32,16 @@ class SyncDropdown extends Component {
// Trigger a sync right away if we're turning it on
if (syncMode === syncStorage.SYNC_MODE_ON) {
await this._handleSyncResourceGroupId(resourceGroupId);
await this._handleSyncResourceGroupId();
}
trackEvent('Sync', 'Change Mode', syncMode);
}
};
_handleSyncResourceGroupId = async () => {
const {syncData} = this.state;
const resourceGroupId = syncData.resourceGroupId;
async _handleSyncResourceGroupId (resourceGroupId) {
// Set loading state
this.setState({loading: true});
@ -55,7 +55,7 @@ class SyncDropdown extends Component {
this.setState({loading: false});
trackEvent('Sync', 'Manual Sync');
}
};
async _reloadData () {
const loggedIn = session.isLoggedIn();
@ -108,45 +108,11 @@ class SyncDropdown extends Component {
render () {
const {className} = this.props;
const {syncData, loading, loggedIn, hide} = this.state;
if (hide) {
return null;
}
const {syncData, loading, loggedIn} = this.state;
// Don't show the sync menu unless we're logged in
if (!loggedIn) {
return (
<div className={className}>
<Dropdown wide={true} className="wide tall">
<DropdownButton className="btn btn--compact wide"
onClick={e => trackEvent('Sync', 'Show Menu', 'Guest')}>
Sync Settings
</DropdownButton>
<DropdownDivider name="Insomnia Cloud Sync"/>
<DropdownItem onClick={e => {
showModal(SettingsModal, TAB_PLUS);
trackEvent('Sync', 'Create Account');
}}>
<i className="fa fa-user"></i>
Create Account
</DropdownItem>
<DropdownItem onClick={e => {
showModal(LoginModal);
trackEvent('Sync', 'Login');
}}>
<i className="fa fa-empty"></i>
Login
</DropdownItem>
<DropdownDivider/>
<DropdownItem buttonClass={PromptButton}
addIcon={true}
onClick={e => this._handleHideMenu()}>
<i className="fa fa-eye-slash"></i>
Hide This Menu
</DropdownItem>
</Dropdown>
</div>
)
return null;
}
if (!syncData) {
@ -158,25 +124,24 @@ class SyncDropdown extends Component {
</div>
)
} else {
const {resourceGroupId, syncMode, syncPercent} = syncData;
const {syncMode, syncPercent} = syncData;
const description = this._getSyncDescription(syncMode, syncPercent);
return (
<div className={className}>
<Dropdown wide={true} className="wide tall">
<DropdownButton className="btn btn--compact wide"
onClick={e => trackEvent('Sync', 'Show Menu', 'Authenticated')}>
<DropdownButton className="btn btn--compact wide" onClick={this._trackShowMenu}>
{description}
</DropdownButton>
<DropdownDivider name={`Workspace Synced ${syncPercent}%`}/>
<DropdownItem onClick={e => this._handleToggleSyncMode(resourceGroupId)}
<DropdownDivider>Workspace Synced {syncPercent}%</DropdownDivider>
<DropdownItem onClick={this._handleToggleSyncMode}
stayOpenAfterClick={true}>
{syncMode === syncStorage.SYNC_MODE_OFF ?
<i className="fa fa-toggle-off"></i> :
<i className="fa fa-toggle-on"></i>}
Automatic Sync
</DropdownItem>
<DropdownItem onClick={e => this._handleSyncResourceGroupId(resourceGroupId)}
<DropdownItem onClick={this._handleSyncResourceGroupId}
disabled={syncPercent === 100}
stayOpenAfterClick={true}>
{loading ?
@ -185,13 +150,8 @@ class SyncDropdown extends Component {
Sync Now {syncPercent === 100 ? '(up to date)' : ''}
</DropdownItem>
<DropdownDivider name="Other"/>
<DropdownItem onClick={e => showModal(SettingsModal, TAB_PLUS)}>
<i className="fa fa-user"></i>
Manage Account
</DropdownItem>
<DropdownItem onClick={e => showModal(SyncLogsModal)}>
<DropdownDivider>Other</DropdownDivider>
<DropdownItem onClick={this._handleShowLogs}>
<i className="fa fa-bug"></i>
Show Debug Logs
</DropdownItem>

View File

@ -10,6 +10,7 @@ import {showModal} from '../modals/index';
import {trackEvent} from '../../../analytics/index';
import Link from '../base/Link';
import WorkspaceSettingsModal from '../modals/WorkspaceSettingsModal';
import WorkspaceShareSettingsModal from '../modals/WorkspaceShareSettingsModal';
class WorkspaceDropdown extends Component {
_handleShowExport = () => showModal(SettingsModal, TAB_INDEX_EXPORT);
@ -19,6 +20,11 @@ class WorkspaceDropdown extends Component {
workspace: this.props.activeWorkspace,
});
};
_handleShowShareSettings = () => {
showModal(WorkspaceShareSettingsModal, {
workspace: this.props.activeWorkspace,
});
};
_handleSwitchWorkspace = workspaceId => {
this.props.handleSetActiveWorkspace(workspaceId);
@ -64,13 +70,16 @@ class WorkspaceDropdown extends Component {
{activeWorkspace.name}
</h1>
</DropdownButton>
<DropdownDivider name={activeWorkspace.name}/>
<DropdownDivider>{activeWorkspace.name}</DropdownDivider>
<DropdownItem onClick={this._handleShowWorkspaceSettings}>
<i className="fa fa-wrench"/> Workspace Settings
<DropdownHint char="&#8679;,"/>
</DropdownItem>
{/*<DropdownItem onClick={this._handleShowShareSettings}>*/}
{/*<i className="fa fa-user"/> Share <strong>{activeWorkspace.name}</strong>*/}
{/*</DropdownItem>*/}
<DropdownDivider name="Switch Workspace"/>
<DropdownDivider>Switch Workspace</DropdownDivider>
{nonActiveWorkspaces.map(w => (
<DropdownItem key={w._id} onClick={this._handleSwitchWorkspace} value={w._id}>
@ -81,10 +90,10 @@ class WorkspaceDropdown extends Component {
<i className="fa fa-empty"/> New Workspace
</DropdownItem>
<DropdownDivider name={`Insomnia Version ${getAppVersion()}`}/>
<DropdownDivider>Insomnia Version {getAppVersion()}</DropdownDivider>
<DropdownItem onClick={this._handleShowSettings}>
<i className="fa fa-cog"/> Preferences
<i className="fa fa-cog"/> App Settings
<DropdownHint char=","/>
</DropdownItem>
<DropdownItem onClick={this._handleShowExport}>

View File

@ -97,23 +97,18 @@ class CookiesModal extends Component {
const {filter} = this.state;
return (
<Modal ref={m => this.modal = m} wide={true} top={true}
tall={true} {...this.props}>
<ModalHeader>
Manage Cookies
</ModalHeader>
<Modal ref={m => this.modal = m} wide={true} top={true} tall={true} {...this.props}>
<ModalHeader>Manage Cookies</ModalHeader>
<ModalBody className="cookie-editor">
<div>
<div
className="cookie-editor__filter form-control form-control--outlined">
<label className="label--small">Filter Cookies</label>
<input
ref={n => this.filterInput = n}
onChange={e => this._onFilterChange(e.target.value)}
type="text"
placeholder="twitter.com"
defaultValue=""
/>
<div className="pad">
<div className="form-control form-control--outlined">
<label>Filter Cookies
<input ref={n => this.filterInput = n}
onChange={e => this._onFilterChange(e.target.value)}
type="text"
placeholder="twitter.com"
defaultValue=""/>
</label>
</div>
</div>
<div className="cookie-editor__editor border-top">

View File

@ -5,10 +5,7 @@ import ModalBody from '../base/ModalBody';
import ModalHeader from '../base/ModalHeader';
import ModalFooter from '../base/ModalFooter';
import * as session from '../../../sync/session';
import {showModal} from './index';
import SignupModal from './SignupModal';
import * as sync from '../../../sync';
import {trackEvent} from '../../../analytics';
class LoginModal extends Component {
state = {
@ -41,14 +38,6 @@ class LoginModal extends Component {
}
};
_handleSignup = e => {
e.preventDefault();
this.modal.hide();
showModal(SignupModal);
trackEvent('Login', 'Switch to Signup');
};
show (options = {}) {
const {title, message} = options;
this.setState({step: 1, error: '', loading: false, title, message});
@ -92,7 +81,7 @@ class LoginModal extends Component {
<div className="margin-left">
Don't have an account yet?
{" "}
<a href="#" onClick={this._handleSignup}>Signup</a>
<Link href="https://insomnia.rest/app/">Signup</Link>
</div>
<button type="submit" className="btn">
{this.state.loading ? (

View File

@ -1,297 +0,0 @@
import React, {Component} from 'react';
import Link from '../base/Link';
import Nl2Br from '../Nl2Br';
import Modal from '../base/Modal';
import ModalBody from '../base/ModalBody';
import ModalHeader from '../base/ModalHeader';
import ModalFooter from '../base/ModalFooter';
import * as session from '../../../sync/session';
import AlertModal from './AlertModal';
import {showModal} from './index';
const MONTHS = [
{name: 'January', value: '01'},
{name: 'February', value: '02'},
{name: 'March', value: '03'},
{name: 'April', value: '04'},
{name: 'May', value: '05'},
{name: 'June', value: '06'},
{name: 'July', value: '07'},
{name: 'August', value: '08'},
{name: 'September', value: '09'},
{name: 'October', value: '10'},
{name: 'November', value: '11'},
{name: 'December', value: '12'},
];
const YEARS = [];
for (let i = 0; i < 20; i++) {
YEARS.push(2016 + i);
}
class PaymentModal extends Component {
state = {
title: '',
message: '',
error: '',
loading: false,
cardType: '',
selectedPlan: 'plus-monthly-1'
};
show (options = {}) {
const {message, title} = options;
this.setState({error: '', message, title});
this.modal.show();
setTimeout(() => this._nameInput.focus(), 100);
}
hide () {
this.modal.hide();
}
_handlePlanChange (selectedPlan) {
this.setState({selectedPlan});
}
_handleCreditCardCvcChange (e) {
const value = e.target.value.trim();
if (!value) {
return;
}
e.target.value = value.replace(/[^0-9]/g, '').trim()
}
_handleCreditCardNumberChange (e) {
const value = e.target.value.trim();
if (!value) {
return;
}
const cardType = Stripe.card.cardType(value);
const lastChar = value[e.target.value.length - 1];
const num = value.replace(/[^0-9]*/g, '');
let newNum = '';
if (cardType === 'American Express') {
// 1111 222222 33333
const g1 = num.slice(0, 4);
const g2 = num.slice(4, 10);
const g3 = num.slice(10, 15);
newNum = g1;
newNum += g2 ? ` ${g2}` : '';
newNum += g3 ? ` ${g3}` : '';
} else if (cardType === 'Diners Club') {
// 1111 2222 3333 44
const g1 = num.slice(0, 4);
const g2 = num.slice(4, 8);
const g3 = num.slice(8, 12);
const g4 = num.slice(12, 14);
newNum = g1;
newNum += g2 ? ` ${g2}` : '';
newNum += g3 ? ` ${g3}` : '';
newNum += g4 ? ` ${g4}` : '';
} else {
// 1111 2222 3333 4444
const g1 = num.slice(0, 4);
const g2 = num.slice(4, 8);
const g3 = num.slice(8, 12);
const g4 = num.slice(12, 16);
newNum = g1;
newNum += g2 ? ` ${g2}` : '';
newNum += g3 ? ` ${g3}` : '';
newNum += g4 ? ` ${g4}` : '';
}
// Handle trailing dash so we can add and delete dashes properly
if (lastChar === ' ') {
newNum += ' ';
}
this.setState({cardType: cardType === 'Unknown' ? '' : cardType});
// Only update number if it changed from the user's original to prevent cursor jump
if (newNum !== value) {
e.target.value = newNum;
}
}
_handleSubmit (e) {
e.preventDefault();
this.setState({error: '', loading: true});
const params = {
name: this._nameInput.value,
number: this._cardNumberInput.value.replace(/ /g, ''),
cvc: this._cvcInput.value,
exp_month: parseInt(this._expiryMonthInput.value, 10),
exp_year: parseInt(this._expiryYearInput.value, 10),
};
Stripe.card.createToken(params, async (status, response) => {
if (status === 200) {
try {
await session.subscribe(response.id, this.state.selectedPlan);
this.setState({error: '', loading: false});
showModal(AlertModal, {
title: 'Payment Succeeded',
message: 'Thanks for supporting Insomnia! You may now continue using the app.'
});
this.hide();
} catch (e) {
this.setState({error: e.message, loading: false});
}
} else {
this.setState({error: response.error.message, loading: false});
}
});
}
render () {
const {selectedPlan, cardType, message, title} = this.state;
return (
<form onSubmit={this._handleSubmit.bind(this)}
key={session.getCurrentSessionId()}>
<Modal ref={m => this.modal = m} {...this.props}>
<ModalHeader>{title || "Enter Payment Method"}</ModalHeader>
<ModalBody className="pad changelog">
<div style={{maxWidth: '32rem', margin: 'auto'}}>
{message ? (
<Nl2Br className="notice info">{message}</Nl2Br>
) : null}
<div>
<div className="pad-left-sm">
<div className="inline-block text-center"
style={{width: '50%'}}>
<input
id="plan-plus-monthly-1"
type="radio"
name="payment-cycle"
checked={selectedPlan === 'plus-monthly-1'}
onChange={e => this._handlePlanChange('plus-monthly-1')}
/>
&nbsp;&nbsp;
<label htmlFor="plan-plus-monthly-1">
Per Month ($5/mo)
</label>
</div>
<div className="inline-block text-center"
style={{width: '50%'}}>
<input
id="plan-plus-yearly-1"
type="radio"
name="payment-cycle"
checked={selectedPlan === 'plus-yearly-1'}
onChange={e => this._handlePlanChange('plus-yearly-1')}
/>
&nbsp;&nbsp;
<label htmlFor="plan-plus-yearly-1">Annual ($50/yr)</label>
</div>
</div>
</div>
<div className="pad-top">
<label htmlFor="payment-name">Full name</label>
<div className="form-control form-control--outlined">
<input type="text"
required="required"
id="payment-name"
name="payment-name"
placeholder="Paula Jones"
defaultValue={session.getFullName()}
ref={n => this._nameInput = n}/>
</div>
</div>
<div>
<label htmlFor="payment-card-number">
Card Number {cardType ? `(${cardType})` : ''}
</label>
<div className="form-control form-control--outlined">
<input type="text"
required="required"
id="payment-card-number"
placeholder="4012 8888 8888 1881"
onChange={this._handleCreditCardNumberChange.bind(this)}
ref={n => this._cardNumberInput = n}/>
</div>
</div>
<div>
<div className="inline-block" style={{width: '66%'}}>
<label htmlFor="payment-expiry">
Expiration Date
</label>
<div className="pad-left-sm pad-top-sm">
<select name="payment-expiration-month"
id="payment-expiration-month"
ref={n => this._expiryMonthInput = n}>
{MONTHS.map(({name, value}) => (
<option key={value} value={value}>
{value} {name}</option>
))}
</select>
{" "}
<select name="payment-expiration-year"
id="payment-expiration-year"
ref={n => this._expiryYearInput = n}>
{YEARS.map(year => (
<option key={year} value={year}>{year}</option>
))}
</select>
</div>
</div>
<div className="inline-block" style={{width: '34%'}}>
<label htmlFor="payment-cvc">Security Code (CVC)</label>
<div className="form-control form-control--outlined">
<input type="text"
maxLength="4"
required="required"
id="payment-cvc"
name="payment-cvc"
placeholder="013"
onChange={this._handleCreditCardCvcChange.bind(this)}
ref={n => this._cvcInput = n}/>
</div>
</div>
</div>
{this.state.error ? (
<div className="danger pad-top">** {this.state.error}</div>
) : null}
</div>
</ModalBody>
<ModalFooter>
<div className="margin-left faint italic txt-sm">
* Payments secured and managed by
{" "}
<Link href="https://stripe.com">Stripe</Link>
</div>
{this.state.loading ? (
<button type="submit" className="btn">
<i className="fa fa-spin fa-refresh margin-right-sm"></i>
{" "}
Processing
</button>
) : (
<button type="submit" className="btn">
Submit Details
</button>
)}
</ModalFooter>
</Modal>
</form>
)
}
}
PaymentModal.propTypes = {};
export default PaymentModal;

View File

@ -1,10 +1,9 @@
import React, {Component} from 'react';
import Link from '../base/Link';
import Modal from '../base/Modal';
import ModalBody from '../base/ModalBody';
import ModalHeader from '../base/ModalHeader';
import ModalFooter from '../base/ModalFooter';
import PaymentModal from './PaymentModal';
import {showModal} from './index';
import * as session from '../../../sync/session';
import {trackEvent} from '../../../analytics';
@ -16,12 +15,6 @@ class PaymentNotificationModal extends Component {
this.hide();
};
_handleProceedToPayment = () => {
this.hide();
showModal(PaymentModal);
trackEvent('Billing', 'Trial Ended', 'Proceed')
};
show () {
// Don't trigger automatically if user has dismissed it already
if (hidePaymentNotificationUntilNextLaunch) {
@ -52,10 +45,9 @@ class PaymentNotificationModal extends Component {
</p>
<br/>
<p className="pad-top">
<button className="btn btn--compact btn--outlined"
onClick={this._handleProceedToPayment}>
Proceed to Billing
</button>
<Link button={true} href="https://insomnia.rest/app/subscribe/" className="btn btn--compact btn--outlined">
Enter Billing Info
</Link>
</p>
</div>
</ModalBody>
@ -63,7 +55,7 @@ class PaymentNotificationModal extends Component {
<button className="btn" onClick={this._handleHide}>
Maybe Later
</button>
<div></div>
&nbsp;
</ModalFooter>
</Modal>
)

View File

@ -87,7 +87,7 @@ class RequestCreateModal extends Component {
<Modal ref={m => this.modal = m}>
<ModalHeader>New Request</ModalHeader>
<ModalBody noScroll={true}>
<form onSubmit={this._handleSubmit} className="pad no-pad-top">
<form onSubmit={this._handleSubmit} className="pad">
<div className="row-fill">
<div className="form-control form-control--outlined">
<label>Name
@ -95,14 +95,13 @@ class RequestCreateModal extends Component {
</label>
</div>
<div className="form-control" style={{width: 'auto'}}>
<label htmlFor="nothing">&nbsp;
<MethodDropdown
className="btn btn--clicky no-wrap"
right={true}
method={selectedMethod}
onChange={this._handleChangeSelectedMethod}
/>
</label>
<label>&nbsp;</label>
<MethodDropdown
className="btn btn--clicky no-wrap"
right={true}
method={selectedMethod}
onChange={this._handleChangeSelectedMethod}
/>
</div>
{!this._shouldNotHaveBody() ? (
<div className="form-control" style={{width: 'auto'}}>

View File

@ -4,7 +4,6 @@ import {shell} from 'electron';
import Modal from '../base/Modal';
import ModalBody from '../base/ModalBody';
import ModalHeader from '../base/ModalHeader';
import ModalFooter from '../base/ModalFooter';
import SettingsShortcuts from '../settings/SettingsShortcuts';
import SettingsAbout from '../settings/SettingsAbout';
import SettingsGeneral from '../settings/SettingsGeneral';
@ -14,7 +13,6 @@ import * as models from '../../../models';
import {getAppVersion, getAppName} from '../../../common/constants';
import * as session from '../../../sync/session';
import {showModal} from './index';
import SignupModal from './SignupModal';
import * as sync from '../../../sync';
import {trackEvent} from '../../../analytics/index';
@ -82,7 +80,8 @@ class SettingsModal extends Component {
<button onClick={e => trackEvent('Setting', 'Tab General')}>General</button>
</Tab>
<Tab selected={this._currentTabIndex === 1}>
<button onClick={e => trackEvent('Setting', 'Tab Import/Export')}>Import/Export</button>
<button onClick={e => trackEvent('Setting', 'Tab Import/Export')}>Import/Export
</button>
</Tab>
<Tab selected={this._currentTabIndex === 3}>
<button onClick={e => trackEvent('Setting', 'Tab Shortcuts')}>Shortcuts</button>
@ -126,9 +125,8 @@ class SettingsModal extends Component {
<SettingsSync
loggedIn={session.isLoggedIn()}
firstName={session.getFirstName() || ''}
handleExit={() => this.modal.hide()}
handleUpdateSetting={(key, value) => models.settings.update(settings, {[key]: value})}
handleShowSignup={() => showModal(SignupModal)}
email={session.getEmail() || ''}
handleExit={this._handleClose}
handleCancelAccount={sync.cancelAccount}
handleReset={() => this._handleSyncReset()}
handleLogout={sync.logout}
@ -139,11 +137,6 @@ class SettingsModal extends Component {
</TabPanel>
</Tabs>
</ModalBody>
{/*<ModalFooter>*/}
{/*<button className="btn" onClick={this._handleClose}>*/}
{/*Close*/}
{/*</button>*/}
{/*</ModalFooter>*/}
</Modal>
);
}

View File

@ -1,195 +0,0 @@
import React, {Component} from 'react';
import classnames from 'classnames';
import Link from '../base/Link';
import Modal from '../base/Modal';
import ModalBody from '../base/ModalBody';
import ModalHeader from '../base/ModalHeader';
import ModalFooter from '../base/ModalFooter';
import * as session from '../../../sync/session';
import {showModal} from './index';
import LoginModal from './LoginModal';
import * as sync from '../../../sync';
import {trackEvent} from '../../../analytics';
const STEP_BASIC_INFO = 'basic';
const STEP_CONFIRM_PASSWORD = 'confirm';
const STEP_LOGIN_INFO = 'done';
class SignupModal extends Component {
state = {
step: STEP_BASIC_INFO,
error: '',
loading: false
};
async _handleSignup (e) {
e.preventDefault();
if (this.state.step === STEP_BASIC_INFO) {
this.setState({step: STEP_CONFIRM_PASSWORD});
return;
}
this.setState({error: '', loading: true});
const email = this._emailInput.value;
const password = this._passwordInput.value;
const firstName = this._nameFirstInput.value;
const lastName = this._nameLastInput.value;
try {
await session.signup(firstName, lastName, email, password);
this.setState({step: STEP_LOGIN_INFO, loading: false});
sync.init();
} catch (e) {
this.setState({error: e.message, loading: false});
}
}
_handleLogin (e) {
e.preventDefault();
this.modal.hide();
showModal(LoginModal, {});
trackEvent('Signup', 'Switch to Login');
}
_checkPasswordsMatch () {
if (this._passwordInput.value !== this._passwordConfirmInput.value) {
this._passwordConfirmInput.setCustomValidity('Password didn\'t match')
} else {
this._passwordConfirmInput.setCustomValidity('')
}
}
show () {
this.setState({step: STEP_BASIC_INFO, loading: false, error: ''});
this.modal.show();
setTimeout(() => this._nameFirstInput.focus(), 200);
}
render () {
const {step} = this.state;
let inner = null;
if (step === STEP_BASIC_INFO || step === STEP_CONFIRM_PASSWORD) {
inner = [
<ModalHeader key="header">Sign Up For a New Account</ModalHeader>,
<ModalBody key="body" className="pad">
<div className={classnames({hide: step !== STEP_BASIC_INFO})}>
<div className="form-control form-control--outlined no-pad-top">
<label>First Name
<input type="text"
required="required"
name="signup-name-first"
placeholder="Jane"
ref={n => this._nameFirstInput = n}/>
</label>
</div>
<div className="form-control form-control--outlined">
<label>Last Name
<input type="text"
name="signup-name-last"
placeholder="Doe"
ref={n => this._nameLastInput = n}/>
</label>
</div>
<div className="form-control form-control--outlined">
<label>Email Address
<input type="email"
required="required"
name="signup-email"
placeholder="me@mydomain.com"
ref={n => this._emailInput = n}/>
</label>
</div>
<div className="form-control form-control--outlined">
<label>Password <span className="faint">(minimum 6 characters)</span>
<input type="password"
required="required"
pattern=".{6,}"
name="signup-password"
placeholder="•••••••••••••"
ref={n => this._passwordInput = n}/>
</label>
</div>
</div>
{step === STEP_CONFIRM_PASSWORD ? (
<div className="text-center pad">
<p className="notice info text-center txt-lg">
Keep your password safe because it cannot be recovered
<br/>
<span className="txt-sm italic">
<Link href="https://insomnia.rest/documentation/plus/">Read More</Link>
{" "}
about how your password is used to encrypt your data
</span>
</p>
<div className="text-left">
<div className="form-control form-control--outlined">
<label className="pad-left-sm">Confirm your Password
<input type="password"
required="required"
pattern=".{6,}"
id="signup-password-confirm"
name="signup-password-confirm"
placeholder="•••••••••••••"
autoFocus="autoFocus"
onChange={this._checkPasswordsMatch.bind(this)}
ref={n => this._passwordConfirmInput = n}/>
</label>
</div>
<button type="button"
className="pad-sm faint"
onClick={e => this.setState({step: STEP_BASIC_INFO})}>
<i className="fa fa-caret-left"/>
Go back
</button>
</div>
</div>
) : null}
{this.state.error ? (
<div className="danger pad-top">** {this.state.error}</div>
) : null}
</ModalBody>,
<ModalFooter key="footer">
<div className="margin-left">
Already have an account?
{" "}
<a href="#" onClick={this._handleLogin.bind(this)}>Login</a>
</div>
<button type="submit" className="btn">
{this.state.loading ? <i className="fa fa-spin fa-refresh margin-right-sm"/> : null}
Create Account
</button>
</ModalFooter>
]
} else {
inner = [
<ModalHeader key="header">Account Created</ModalHeader>,
<ModalBody key="body" className="pad no-pad-top">
<h1>Thanks for signing up!</h1>
<p>A verification email has been sent to your email address. You may now log in.</p>
</ModalBody>,
<ModalFooter key="footer">
<button type="submit"
className="btn"
onClick={e => this._handleLogin(e)}>
Proceed to Login
</button>
</ModalFooter>
]
}
return (
<form onSubmit={this._handleSignup.bind(this)}>
<Modal ref={m => this.modal = m} {...this.props}>
{inner}
</Modal>
</form>
)
}
}
SignupModal.propTypes = {};
export default SignupModal;

View File

@ -250,8 +250,8 @@ class WorkspaceSettingsModal extends Component {
/>
</label>
</div>
<div className="row-fill">
<div className="form-control">
<div className="form-row">
<div className="form-control width-auto">
<label>PFX <span className="faint">(or PKCS12)</span>
<FileInputButton
className="btn btn--clicky"
@ -261,11 +261,11 @@ class WorkspaceSettingsModal extends Component {
/>
</label>
</div>
<div className="width-auto">
<div className="text-center">
<br/><br/>
&nbsp;&nbsp;Or&nbsp;&nbsp;
</div>
<div className="no-wrap row-fill">
<div className="row-fill">
<div className="form-control">
<label>CRT File
<FileInputButton

View File

@ -0,0 +1,95 @@
import React, {Component, PropTypes} from 'react';
import {Dropdown, DropdownButton, DropdownItem, DropdownDivider} from '../base/dropdown';
import Link from '../base/Link';
import Modal from '../base/Modal';
import ModalBody from '../base/ModalBody';
import ModalHeader from '../base/ModalHeader';
import ModalFooter from '../base/ModalFooter';
import * as session from '../../../sync/session';
import LoginModal from './LoginModal';
import {showModal} from './index';
class WorkspaceShareSettingsModal extends Component {
state = {
teams: []
};
_handleShareWithTeam = team => {
console.log('Share with ', team);
};
async _load () {
if (!session.isLoggedIn()) {
showModal(LoginModal);
}
const teams = await session.listTeams();
this.setState({teams});
}
_handleSubmit = e => {
e.preventDefault();
};
_handleClose = () => this.hide();
_setModalRef = m => {
this.modal = m;
};
show () {
this.modal.show();
this._load();
}
render () {
const {teams} = this.state;
return (
<form onSubmit={this._handleSubmit}>
<Modal ref={this._setModalRef}>
<ModalHeader key="header">Share Workspace</ModalHeader>
<ModalBody key="body" className="pad" noScroll={true}>
<p>
Enabling sync will automatically share your workspace with your entire team
</p>
<div className="text-center form-control pad">
<Dropdown outline={true}>
<DropdownDivider>Teams</DropdownDivider>
<DropdownButton type="button" className="btn btn--clicky">
<i className="fa fa-lock"/> Private <i className="fa fa-caret-down"/>
</DropdownButton>
{teams.map(team => (
<DropdownItem key={team._id} onClick={e => this._handleShareWithTeam(team)}>
<i className="fa fa-users"/> Share with <strong>{team.name}</strong>
</DropdownItem>
))}
<DropdownDivider>Other</DropdownDivider>
<DropdownItem>
<i className="fa fa-lock"/> Private
</DropdownItem>
</Dropdown>
&nbsp;&nbsp;
<Link button={true}
className="btn btn--super-compact inline-block"
href="https://insomnia.rest/app/teams/">
Manage Teams
</Link>
</div>
</ModalBody>
<ModalFooter key="footer">
<button type="button" className="btn" onClick={this._handleClose}>
Done
</button>
</ModalFooter>
</Modal>
</form>
);
}
}
WorkspaceShareSettingsModal.propTypes = {
workspace: PropTypes.object.isRequired,
};
export default WorkspaceShareSettingsModal;

View File

@ -2,130 +2,88 @@ import React, {PropTypes} from 'react';
const SettingsGeneral = ({settings, updateSetting}) => (
<div>
<div>
<input
id="setting-follow-redirects"
type="checkbox"
checked={settings.followRedirects}
onChange={e => updateSetting('followRedirects', e.target.checked)}
/>
&nbsp;&nbsp;
<label htmlFor="setting-follow-redirects">
Follow redirects automatically
<div className="form-control form-control--thin">
<label>Follow redirects automatically
<input type="checkbox"
checked={settings.followRedirects}
onChange={e => updateSetting('followRedirects', e.target.checked)}/>
</label>
</div>
<div>
<input
id="setting-validate-ssl"
type="checkbox"
checked={settings.validateSSL}
onChange={e => updateSetting('validateSSL', e.target.checked)}
/>
&nbsp;&nbsp;
<label htmlFor="setting-validate-ssl">
Validate SSL Certificates
<div className="form-control form-control--thin">
<label>Validate SSL Certificates
<input type="checkbox"
checked={settings.validateSSL}
onChange={e => updateSetting('validateSSL', e.target.checked)}/>
</label>
</div>
<div>
<input
id="setting-show-passwords"
type="checkbox"
checked={settings.showPasswords}
onChange={e => updateSetting('showPasswords', e.target.checked)}
/>
&nbsp;&nbsp;
<label htmlFor="setting-show-passwords">
Show passwords in plain-text
<div className="form-control form-control--thin">
<label>Show passwords in plain-text
<input type="checkbox"
checked={settings.showPasswords}
onChange={e => updateSetting('showPasswords', e.target.checked)}/>
</label>
</div>
<div>
<input
id="setting-bulk-header-editor"
type="checkbox"
checked={settings.useBulkHeaderEditor}
onChange={e => updateSetting('useBulkHeaderEditor', e.target.checked)}
/>
&nbsp;&nbsp;
<label htmlFor="setting-bulk-header-editor">
Use bulk header editor by default
<div className="form-control form-control--thin">
<label>Use bulk header editor by default
<input type="checkbox"
checked={settings.useBulkHeaderEditor}
onChange={e => updateSetting('useBulkHeaderEditor', e.target.checked)}/>
</label>
</div>
<div>
<input
id="setting-stacked-layout"
type="checkbox"
checked={settings.forceVerticalLayout}
onChange={e => updateSetting('forceVerticalLayout', e.target.checked)}
/>
&nbsp;&nbsp;
<label htmlFor="setting-stacked-layout">
Always use vertical layout
<div className="form-control form-control--thin">
<label>Always use vertical layout
<input type="checkbox"
checked={settings.forceVerticalLayout}
onChange={e => updateSetting('forceVerticalLayout', e.target.checked)}/>
</label>
</div>
<div>
<input
id="setting-editor-line-wrapping"
type="checkbox"
checked={settings.editorLineWrapping}
onChange={e => updateSetting('editorLineWrapping', e.target.checked)}
/>
&nbsp;&nbsp;
<label htmlFor="setting-editor-line-wrapping">
Wrap Long Lines
<div className="form-control form-control--thin">
<label>Wrap Long Lines
<input type="checkbox"
checked={settings.editorLineWrapping}
onChange={e => updateSetting('editorLineWrapping', e.target.checked)}/>
</label>
</div>
<div className="form-control form-control--outlined">
<div className="form-control form-control--outlined pad-top-sm">
<label>Request/Response Font Size (px)
<input
type="number"
min={8}
max={20}
value={settings.editorFontSize}
onChange={e => updateSetting('editorFontSize', parseInt(e.target.value, 10))}
/>
<input type="number"
min={8}
max={20}
value={settings.editorFontSize}
onChange={e => updateSetting('editorFontSize', parseInt(e.target.value, 10))}/>
</label>
</div>
<div className="form-control form-control--outlined">
<label>Request Timeout (ms) (0 for no timeout)
<input
type="number"
min={0}
value={settings.timeout}
onChange={e => updateSetting('timeout', parseInt(e.target.value, 10))}
/>
<input type="number"
min={0}
value={settings.timeout}
onChange={e => updateSetting('timeout', parseInt(e.target.value, 10))}/>
</label>
<div className="inline-block" style={{width: '50%'}}>
<div className="form-control form-control--outlined">
<label>HTTP Proxy
<input
type="text"
placeholder="localhost:8005"
defaultValue={settings.httpProxy}
onChange={e => updateSetting('httpProxy', e.target.value)}
/>
</label>
</div>
</div>
<div className="form-row">
<div className="form-control form-control--outlined">
<label>HTTP Proxy
<input type="text"
placeholder="localhost:8005"
defaultValue={settings.httpProxy}
onChange={e => updateSetting('httpProxy', e.target.value)}/>
</label>
</div>
<div className="inline-block" style={{width: '50%'}}>
<div className="pad-left-sm">
<div className="form-control form-control--outlined">
<label>HTTPS Proxy
<input
placeholder="localhost:8005"
type="text"
defaultValue={settings.httpsProxy}
onChange={e => updateSetting('httpsProxy', e.target.value)}
/>
</label>
</div>
</div>
<div className="form-control form-control--outlined">
<label>HTTPS Proxy
<input placeholder="localhost:8005"
type="text"
defaultValue={settings.httpsProxy}
onChange={e => updateSetting('httpsProxy', e.target.value)}/>
</label>
</div>
</div>
<br/>

View File

@ -22,7 +22,7 @@ const SettingsImportExport = ({
<DropdownButton className="btn btn--clicky">
Export Data <i className="fa fa-caret-down"></i>
</DropdownButton>
<DropdownDivider name="Choose Export Type"/>
<DropdownDivider>Choose Export Type</DropdownDivider>
<DropdownItem onClick={handleExportWorkspace}>
<i className="fa fa-home"></i>
Current Workspace

View File

@ -1,99 +1,82 @@
import React, {PropTypes} from 'react';
import React, {PropTypes, Component} from 'react';
import Link from '../base/Link';
import PromptButton from '../base/PromptButton';
import {trackEvent} from '../../../analytics/index';
import {showModal} from '../modals/index';
import PaymentModal from '../modals/PaymentModal';
import LoginModal from '../modals/LoginModal';
const SettingsSync = ({
loggedIn,
firstName,
handleExit,
handleUpdateSetting,
handleShowSignup,
handleCancelAccount,
handleLogout,
handleReset,
}) => (
<div>
<h1 className="no-margin-top">Cloud Sync and Backup</h1>
class SettingsSync extends Component {
_handleClickLogout = async () => {
this.props.handleExit();
await this.props.handleLogout();
};
{loggedIn ? [
<p key="1">
Hi {firstName}! Thanks for signing up for Insomnia
Plus.
</p>,
<p key="2" className="pad-top">
<PromptButton
className="btn btn--clicky danger"
onClick={() => {
handleExit();
handleCancelAccount();
}}>
Cancel Subscription
</PromptButton>
{" "}
<PromptButton className="btn btn--clicky warning"
onClick={handleReset}
confirmMessage="Delete all sync-related data?">
Reset Remote Data
</PromptButton>
{" "}
<button className="btn btn--clicky"
onClick={async () => {
handleExit();
showModal(PaymentModal);
}}>
Update Billing
</button>
{" "}
<PromptButton className="btn btn--clicky"
onClick={async () => {
handleExit();
await handleLogout();
}}>
Log Out
</PromptButton>
</p>
] : [
<p key="0">
<Link href="https://insomnia.rest/plus/">Insomnia Plus</Link> helps you <i>rest</i> easy by
keeping your workspaces securely backed up and synced across all of your devices.
</p>,
<p key="1">
Upgrade today to start enjoying
{" "}
<Link href="https://insomnia.rest/pricing/">all of the benefits</Link>, while also helping
support my continuous effort of making Insomnia awesome! <i
className="fa fa-smile-o txt-xl"/>
</p>,
<p key="2" className="pad-top text-center">
<button className="btn txt-lg btn--outlined"
onClick={() => {
handleExit();
trackEvent('Settings Sync', 'Click Upgrade');
handleShowSignup()
}}>
Upgrade to Plus
</button>
</p>,
<p key="3" className="text-center italic">
$5 per month or $50 per year
<br/>
<span className="txt-sm faint pad-top-sm">
14-day trial (cancel at any time)
</span>
</p>
]}
</div>
);
_handleClickLogin = async () => {
this.props.handleExit();
showModal(LoginModal);
};
render () {
const {
loggedIn,
firstName,
email,
} = this.props;
return (
<div>
<h1 className="no-margin-top">Cloud Sync and Backup</h1>
{loggedIn ? [
<p key="1">
Hi {firstName}! Thanks for signing up for Insomnia
Plus. You are currently signed in with {email}
</p>,
<p key="2" className="pad-top">
<Link button={true} className="btn btn--clicky" href="https://insomnia.rest/app/">
Manage Account
</Link>
{" "}
<PromptButton className="btn btn--clicky" onClick={this._handleClickLogout}>
Log Out
</PromptButton>
</p>
] : [
<p key="0">
<Link href="https://insomnia.rest/plus/">Insomnia Plus</Link> helps you <i>rest</i> easy
by
keeping your workspaces securely backed up and synced across all of your devices.
</p>,
<p key="1">
Upgrade today to start enjoying
{" "}
<Link href="https://insomnia.rest/pricing/">all of the benefits</Link>, while also
helping
support my continuous effort of making Insomnia awesome! <i
className="fa fa-smile-o txt-xl"/>
</p>,
<p key="2" className="pad-top text-center">
<Link button={true}
href="https://insomnia.rest/pricing"
className="btn txt-lg btn--outlined">
Create an Account
</Link>
{" "}
<button className="btn" onClick={this._handleClickLogin}>Log In</button>
</p>,
<p key="3" className="text-center italic">
<span className="txt-sm faint pad-top-sm">all plans include a free trial</span>
</p>
]}
</div>
)
}
}
SettingsSync.propTypes = {
loggedIn: PropTypes.bool.isRequired,
firstName: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
handleExit: PropTypes.func.isRequired,
handleUpdateSetting: PropTypes.func.isRequired,
handleShowSignup: PropTypes.func.isRequired,
handleCancelAccount: PropTypes.func.isRequired,
handleLogout: PropTypes.func.isRequired,
handleReset: PropTypes.func.isRequired,

View File

@ -10,8 +10,19 @@ import {trackEvent} from '../../../analytics/index';
class SidebarRequestRow extends PureComponent {
state = {
dragDirection: 0,
isEditing: false,
};
state = {dragDirection: 0};
_handleEditStart = () => {
trackEvent('Request', 'Rename', 'In Place');
this.setState({isEditing: true});
};
_handleRequestUpdateName = name => {
models.request.update(this.props.request, {name})
};
_handleRequestCreateFromEmpty = () => {
const parentId = this.props.requestGroup._id;
@ -63,8 +74,7 @@ class SidebarRequestRow extends PureComponent {
node = (
<li className={classes}>
<div className="sidebar__item" tabIndex={0}>
<button className="sidebar__clickable"
onClick={this._handleRequestCreateFromEmpty}>
<button className="sidebar__clickable" onClick={this._handleRequestCreateFromEmpty}>
<em>click to add first request...</em>
</button>
</div>
@ -77,14 +87,11 @@ class SidebarRequestRow extends PureComponent {
<button className="wide" onClick={this._handleRequestActivate}>
<div className="sidebar__clickable">
<MethodTag method={request.method}/>
<Editable
value={request.name}
onEditStart={() => trackEvent('Request', 'Rename', 'In Place')}
onSubmit={name => models.request.update(request, {name})}
/>
<Editable value={request.name}
onEditStart={this._handleEditStart}
onSubmit={this._handleRequestUpdateName}/>
</div>
</button>
<div className="sidebar__actions">
<RequestActionsDropdown
handleDuplicateRequest={handleDuplicateRequest}
@ -94,13 +101,16 @@ class SidebarRequestRow extends PureComponent {
requestGroup={requestGroup}
/>
</div>
</div>
</li>
)
}
return connectDragSource(connectDropTarget(node));
if (!this.state.isEditing) {
return connectDragSource(connectDropTarget(node));
} else {
return connectDropTarget(node);
}
}
}

View File

@ -19,11 +19,6 @@
}
}
.cookie-editor__filter {
margin: @padding-md @padding-md @padding-sm;
padding-bottom: @padding-md;
}
.cookie-editor__editor {
padding: 0 @padding-md @padding-md @padding-md;
overflow-y: auto;

View File

@ -4,24 +4,33 @@
.form-control {
outline: none;
border: 0;
padding-top: @padding-sm;
margin-bottom: @padding-sm;
width: 100%;
box-sizing: border-box;
button,
input,
textarea,
input[type="number"],
input[type="password"],
input[type="email"],
input[type="color"],
input[type="date"],
input[type="text"] {
button {
width: 100%;
height: 100%;
display: block;
margin-top: @padding-xs;
}
button,
input[type="radio"],
input[type="checkbox"] {
width: auto;
}
input[type="radio"],
input[type="checkbox"] {
height: 1rem;
float: left;
margin-top: @padding-xxs;
margin-right: @padding-xs;
}
& > button {
width: auto;
}
@ -36,6 +45,14 @@
display: block;
}
&.form-control--thin {
label {
font-weight: normal;
}
margin-bottom: @padding-xxs;
}
&.form-control--padded,
&.form-control--outlined,
&.form-control--underlined {
@ -77,6 +94,32 @@
margin-right: 0 !important;
}
.form-row {
display: flex;
align-items: center;
justify-content: center;
align-content: center;
flex-direction: row;
& > * {
width: 100%;
margin-left: 0.5rem;
margin-right: 0.5rem;
}
& > p {
margin: 0;
}
& > *:first-child {
margin-left: 0;
}
& > *:last-child {
margin-right: 0;
}
}
.btn {
text-align: center;
padding: 0 (@padding-md * 1.5);

View File

@ -245,6 +245,7 @@
.editable {
line-height: @line-height-xs;
height: 100%;
}
}
@ -279,6 +280,7 @@
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
height: 100%;
& > *:last-child {
white-space: nowrap;

View File

@ -71,6 +71,10 @@
color: @surprise !important;
}
.boring {
color: @font-super-light-bg !important;
}
.bg-success,
.bg-http-method-POST {
background: @success !important;

View File

@ -1,7 +1,7 @@
/* Padding */
@padding-md: 1.26rem;
@padding-sm: 0.63rem;
@padding-xs: 0.44rem;
@padding-xs: 0.42rem;
@padding-xxs: 0.24rem;
@padding-lg: 2.5rem;
@padding-xl: 5rem;

View File

@ -63,6 +63,10 @@ p {
line-height: 1.7em;
}
small {
font-size: 0.85em;
}
label.label--small,
table th {
color: @hl;
@ -110,18 +114,19 @@ code, pre, .monospace {
.notice {
text-align: center;
color: @font-light-bg !important;
padding: @padding-sm;
padding: @padding-sm * 1.5;
border: 1px solid;
border-radius: @radius-md;
}
.notice.warning {
background: fadeout(@warning, 85);
border: 1px solid @warning;
background: fadeout(@warning, 90);
border-color: @warning;
}
.notice.info {
background: fadeout(@info, 85);
border: 1px solid @info;
background: fadeout(@info, 90);
border-color: @info;
}
.text-center {

View File

@ -108,7 +108,7 @@
"mkdirp": "^0.5.1",
"mousetrap": "^1.6.0",
"nedb": "^1.8.0",
"node-forge": "^0.6.43",
"node-forge": "^0.6.46",
"nunjucks": "^3.0.0",
"raven": "^0.12.1",
"react": "^15.3.1",
@ -122,7 +122,6 @@
"redux-thunk": "^2.0.1",
"request": "^2.74.0",
"reselect": "^2.5.4",
"sjcl": "^1.0.6",
"srp": "git@github.com:getinsomnia/srp-js.git#6ebd8c3acfbcf69645e4c6e6f8f6b55138ff22c3",
"tough-cookie": "^2.3.1",
"traverse": "^0.6.6",