feat(self-hosting): Add onboarding flow for self-hosted sync engine

Summary:
Adds a fun new UI for adding accounts to the sync engine. After creating your sync engine instance, all you have to do is auth your accounts on the command line and then enter the URL/port number in this flow. That pulls all of your accounts from the `/accounts` endpoint, mocks an identity token, and edits your `config.json` properly.

TODO: Update the docs in the repo and revert the PR with the temporary fix.

Test Plan: Tested locally.

Reviewers: bengotow, halla, juan

Reviewed By: halla, juan

Differential Revision: https://phab.nylas.com/D3114
This commit is contained in:
Jackie Luo 2016-07-21 13:59:43 -07:00
parent e40239f6b9
commit 98e351ebeb
12 changed files with 303 additions and 107 deletions

View file

@ -1,103 +1,11 @@
# Configuration
This document outlines configuration options which aren't exposed via N1's
preferences interface, but may be useful.
preferences interface but may be useful.
## Running Against Open Source Sync Engine
N1 needs to fetch mail from a running instance of the [Nylas Sync
Engine](https://github.com/nylas/sync-engine). The Sync Engine is what
abstracts away IMAP, POP, and SMTP to serve your email on any provider
through a modern, RESTful API.
By default the N1 source points to our hosted version of the sync-engine;
however, the Sync Engine is open source and you can run it yourself.
1. Install the Nylas Sync Engine in a Vagrant virtual machine by following the
[installation and setup](https://github.com/nylas/sync-engine#installation-and-setup)
instructions.
2. Once you've installed the sync engine, add accounts by running the inbox-auth
script. For Gmail accounts, the syntax is simple: `bin/inbox-auth you@gmail.com`
3. Start the sync engine by running `bin/inbox-start` and the API via `bin/inbox-api`.
4. After you've linked accounts to the Sync Engine, open or create a file at
`~/.nylas/config.json`. This is the config file that N1 reads at launch.
Replace `env: "production"` with `env: "local"` at the top level of the config.
This tells N1 to look at `localhost:5555` for the sync engine. If you've deployed
the sync engine elsewhere, add the following block beneath `env: "local"`:
```javascript
"syncEngine": {
"APIRoot": "http://mysite.com:5555"
},
```
NOTE: If you are using a custom network layout and your sync engine is not on
`localhost:5555`, use `env: custom` instead along with your alternate IP for the
API Root, for example `192.168.1.00:5555`
```javascript
{
"env": "custom",
"syncEngine": {
"APIRoot": "http://192.168.1.100:5555"
},
```
Copy the JSON array of accounts returned from the Sync Engine's `/accounts`
endpoint (ex. `http://localhost:5555/accounts`) into the config file at the
path `*.nylas.accounts`.
N1 will look for access tokens for these accounts under `*.nylas.accountTokens`,
but the open source version of the sync engine does not provide access tokens.
When you make requests to the open source API, you provide an account
ID in the HTTP Basic Auth username field instead of an account token.
For each account you've created, add an entry to `*.nylas.accountTokens`
with the account ID as both the key and value.
The final `config.json` file should look something like this:
```javascript
{
"*": {
"env": "local",
"nylas": {
"accounts": [
{
"server_id": "{ACCOUNT_ID_1}",
"object": "account",
"account_id": "{ACCOUNT_ID_1}",
"name": "{YOUR NAME}",
"provider": "{PROVIDER_NAME}",
"email_address": "{YOUR_EMAIL_ADDRESS}",
"organization_unit": "{folder or label}",
"id": "{ACCOUNT_ID_1}"
},
{
"server_id": "{ACCOUNT_ID_2}",
"object": "account",
"account_id": "{ACCOUNT_ID_2}",
"name": "{YOUR_NAME}",
"provider": "{PROVIDER_NAME}",
"email_address": "{YOUR_EMAIL_ADDRESS}",
"organization_unit": "{folder or label}",
"id": "{ACCOUNT_ID_2}"
}
],
"accountTokens": {
"{ACCOUNT_ID_1}": "{ACCOUNT_ID_1}",
"{ACCOUNT_ID_2}": "{ACCOUNT_ID_2}"
}
}
}
}
```
Note: `{ACCOUNT_ID_1}` refers to the database ID of the `Account` object
you create when setting up the Sync Engine. The JSON above should match
fairly closely with the Sync Engine `Account` object.
If you want to point N1 to your self-hosted sync engine, select "Hosting your own sync engine?" under the "Get Started" button on the welcome screen. There, follow the instructions for creating your own instance of the sync engine and enter the URL and port number where you have it running.
## Other Config Options

View file

@ -68,8 +68,8 @@ Great starting points for creating your own plugins!
- [Website Launcher](https://github.com/adriangrantdotorg/nylas-n1-background-webpage)—Opens a URL in separate window
- In Development: [Cypher](https://github.com/mbilker/cypher) (PGP Encryption)
# Running Locally
By default, the N1 source points to our hosted version of the Nylas Sync Engine—however, the Sync Engine is open source, and you can [run it yourself](https://github.com/nylas/N1/blob/master/CONFIGURATION.md).
# Configuration
You can configure N1 in a few ways—for instance, pointing it to your self-hosted instance of the sync engine or changing the interface zoom level. [Learn more about how.](https://github.com/nylas/N1/blob/master/CONFIGURATION.md).
# Feature Requests / Plugin Ideas

View file

@ -7,6 +7,7 @@ const OnboardingActions = Reflux.createActions([
"moveToPage",
"authenticationJSONReceived",
"accountJSONReceived",
"accountsAddedLocally",
]);
for (const key of Object.keys(OnboardingActions)) {

View file

@ -5,6 +5,8 @@ import PageTopBar from './page-top-bar';
import WelcomePage from './page-welcome';
import TutorialPage from './page-tutorial';
import SelfHostingSetupPage from './page-self-hosting-setup';
import SelfHostingConfigPage from './page-self-hosting-config';
import AuthenticatePage from './page-authenticate';
import AccountChoosePage from './page-account-choose';
import AccountSettingsPage from './page-account-settings';
@ -16,6 +18,8 @@ import InitialPreferencesPage from './page-initial-preferences';
const PageComponents = {
"welcome": WelcomePage,
"tutorial": TutorialPage,
"self-hosting-setup": SelfHostingSetupPage,
"self-hosting-config": SelfHostingConfigPage,
"authenticate": AuthenticatePage,
"account-choose": AccountChoosePage,
"account-settings": AccountSettingsPage,

View file

@ -24,6 +24,7 @@ class OnboardingStore extends NylasStore {
this.listenTo(OnboardingActions.moveToPreviousPage, this._onMoveToPreviousPage)
this.listenTo(OnboardingActions.moveToPage, this._onMoveToPage)
this.listenTo(OnboardingActions.accountJSONReceived, this._onAccountJSONReceived)
this.listenTo(OnboardingActions.accountsAddedLocally, this._onAccountsAddedLocally)
this.listenTo(OnboardingActions.authenticationJSONReceived, this._onAuthenticationJSONReceived)
this.listenTo(OnboardingActions.setAccountInfo, this._onSetAccountInfo);
this.listenTo(OnboardingActions.setAccountType, this._onSetAccountType);
@ -177,6 +178,29 @@ class OnboardingStore extends NylasStore {
}
}
_onAccountsAddedLocally = (accounts) => {
try {
const isFirstAccount = AccountStore.accounts().length === 0
for (const account of accounts) {
account.auth_token = account.id
AccountStore.addAccountFromJSON(account)
}
ipcRenderer.send('new-account-added')
NylasEnv.displayWindow()
if (isFirstAccount) {
this._onMoveToPage('initial-preferences')
} else {
this._onOnboardingComplete();
}
} catch (e) {
NylasEnv.reportError(e)
NylasEnv.showErrorDialog("Unable to Connect Accounts", "Sorry, something went wrong on your instance of the sync engine. Please try again.")
}
}
page() {
return this._pageStack[this._pageStack.length - 1];
}

View file

@ -2,6 +2,7 @@ import React from 'react';
import {RetinaImg} from 'nylas-component-kit';
import OnboardingActions from './onboarding-actions';
import AccountTypes from './account-types';
import SelfHostingConfigPage from './page-self-hosting-config'
export default class AccountChoosePage extends React.Component {
static displayName = "AccountChoosePage";
@ -30,6 +31,11 @@ export default class AccountChoosePage extends React.Component {
}
render() {
if (NylasEnv.config.get('env', 'custom') ||
NylasEnv.config.get('env', 'local')) {
return (<SelfHostingConfigPage addAccount />)
}
return (
<div className="page account-choose">
<h2>

View file

@ -0,0 +1,159 @@
import React from 'react'
import {Actions} from 'nylas-exports'
import {Flexbox} from 'nylas-component-kit'
import OnboardingActions from './onboarding-actions'
class SelfHostingConfigPage extends React.Component {
static displayName = 'SelfHostingConfigPage'
static propTypes = {
addAccount: React.PropTypes.bool,
}
constructor(props) {
super(props)
this.state = {
url: "",
port: "",
error: null,
}
}
_onChangeUrl = (event) => {
this.setState({
url: event.target.value,
})
}
_onChangePort = (event) => {
this.setState({
port: event.target.value,
})
}
_addAccountJSON = () => {
// Connect to local sync engine's /accounts endpoint and add accounts to N1
const xmlHttp = new XMLHttpRequest()
xmlHttp.onreadystatechange = () => {
if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
const accounts = JSON.parse(xmlHttp.responseText)
if (accounts.length === 0) {
this.setState({error: "There are no accounts added to this instance of the sync engine. Make sure you've authed an account."})
}
OnboardingActions.accountsAddedLocally(accounts)
}
}
xmlHttp.onerror = () => {
this.setState({error: `The request to ${NylasEnv.config.get('syncEngine.APIRoot')}/accounts failed.`})
}
xmlHttp.open("GET", `${NylasEnv.config.get('syncEngine.APIRoot')}/accounts`)
xmlHttp.send(null)
}
_onSubmit = () => {
if (this.state.url.length === 0 || this.state.port.length === 0) {
this.setState({error: "Please include both a URL and port number."})
return
}
NylasEnv.config.set('env', 'custom')
NylasEnv.config.set('syncEngine.APIRoot', `http://${this.state.url}:${this.state.port}`)
Actions.setNylasIdentity({
token: "SELFHOSTEDSYNCENGINE",
identity: {
firstname: "",
lastname: "",
valid_until: null,
free_until: Number.INT_MAX,
email: "",
id: 1,
seen_welcome_page: true,
},
})
this._addAccountJSON()
}
_onKeyDown = (event) => {
if (['Enter', 'Return'].includes(event.key)) {
this._onSubmit();
}
}
_renderInitalConfig() {
return (
<div>
<h2>Configure your self-hosted sync engine</h2>
<div className="message empty">
Once you have created your instance of the sync engine, connect it to N1.
</div>
</div>
)
}
_renderAdditionalConfig() {
return (
<div>
<h2>Connect more email accounts</h2>
<div className="message empty">
To add new accounts, use the <a href="https://github.com/nylas/sync-engine#installation-and-setup">instructions</a> for the Sync Engine. For example:<br />
<code>bin/inbox-auth you@gmail.com</code>
</div>
</div>
)
}
_renderErrorMessage() {
return (
<div className="message error">
{this.state.error}
</div>
)
}
render() {
return (
<div className="page self-hosting">
{!this.props.addAccount ? this._renderInitalConfig() : this._renderAdditionalConfig()}
{this.state.error ? this._renderErrorMessage() : null}
<div className="self-hosting-container">
<Flexbox direction="horizontal">
<div className="api-root">
<h4>{`http://`}</h4>
</div>
<div>
<label>Sync Engine URL:</label>
<input
title="Sync Engine URL"
type="text"
value={this.state.url}
onChange={this._onChangeUrl}
onKeyDown={this._onKeyDown}
/>
</div>
<div className="api-root">
<h4>{`:`}</h4>
</div>
<div>
<label>Sync Engine Port:</label>
<input
title="Sync Engine Port"
type="text"
value={this.state.port}
onChange={this._onChangePort}
onKeyDown={this._onKeyDown}
/>
</div>
</Flexbox>
</div>
<button
className="btn btn-large btn-gradient"
onClick={this._onSubmit}
>
Connect Accounts
</button>
</div>
)
}
}
export default SelfHostingConfigPage

View file

@ -0,0 +1,42 @@
import React from 'react'
import OnboardingActions from './onboarding-actions'
class SelfHostingSetupPage extends React.Component {
static displayName = 'SelfHostingSetupPage'
_onContinue = () => {
OnboardingActions.moveToPage("self-hosting-config");
}
render() {
return (
<div className="page self-hosting">
<h2>Create your sync engine instance</h2>
<div className="self-hosting-container">
<div className="message empty">
N1 needs to fetch mail from a running instance of the <a href="https://github.com/nylas/sync-engine">Nylas Sync Engine</a>. By default, N1 points to our hosted version, but the code is open source so that you can run your own instance. Note that Exchange accounts are not supported and some plugins that rely on our back-end (snoozing, open/link tracking, etc.) will not work.
</div>
<div className="section">
1. Install the Nylas Sync Engine in a Vagrant virtual machine by following the <a href="https://github.com/nylas/sync-engine#installation-and-setup">installation and setup</a> instructions.
</div>
<div className="section">
2. Add accounts by running the <code>inbox-auth</code> script. For example: <code>bin/inbox-auth you@gmail.com</code>.
</div>
<div className="section">
3. Start the sync engine by running <code>bin/inbox-start</code> and the API via <code>bin/inbox-api</code>.
</div>
</div>
<button
key="next"
className="btn btn-large btn-gradient"
onClick={this._onContinue}
>
Done
</button>
</div>
)
}
}
export default SelfHostingSetupPage

View file

@ -19,6 +19,10 @@ export default class WelcomePage extends React.Component {
OnboardingActions.moveToPage("tutorial");
}
_onSelfHosting = () => {
OnboardingActions.moveToPage("self-hosting-setup")
}
_renderContent(isFirstAccount) {
if (isFirstAccount) {
return (
@ -32,15 +36,14 @@ export default class WelcomePage extends React.Component {
return (
<div>
<p className="hero-text" style={{fontSize: 46, marginTop: 187}}>Welcome back!</p>
<p className="hero-text" style={{fontSize: 20, maxWidth: 550, margin: 'auto', lineHeight: 1.7, marginTop: 30}}>This month we're <a href="https://nylas.com/blog/nylas-pro/">launching Nylas Pro</a>. As an existing user, you'll receive a coupon for your first year free. Create a Nylas ID to continue using N1, and look out for a coupon email!</p>
<p className="hero-text" style={{fontSize: 20, maxWidth: 550, margin: 'auto', lineHeight: 1.7, marginTop: 30}}>Since you've been gone, we've <a href="https://nylas.com/blog/nylas-pro/">launched Nylas Pro</a>, which now requires a paid subscription. Create a Nylas ID to start your trial and continue using N1!</p>
<RetinaImg className="icons" url="nylas://onboarding/assets/icons-bg@2x.png" mode={RetinaImg.Mode.ContentPreserve} />
</div>
)
}
render() {
const isFirstAccount = (AccountStore.accounts().length === 0);
const isFirstAccount = (AccountStore.accounts().length === 0)
return (
<div className={`page welcome is-first-account-${isFirstAccount}`}>
<div className="steps-container">
@ -48,6 +51,7 @@ export default class WelcomePage extends React.Component {
</div>
<div className="footer">
<button key="next" className="btn btn-large btn-continue" onClick={this._onContinue}>Get Started</button>
<div className="btn-self-hosting" onClick={this._onSelfHosting}>Hosting your own sync engine?</div>
</div>
</div>
);

View file

@ -503,6 +503,13 @@
left: 0;
pointer-events: none;
}
.btn-self-hosting {
cursor: pointer;
font-weight: 300;
margin-bottom: 20px;
color: white;
}
}
.page.welcome.is-first-account-false {
@ -536,6 +543,52 @@
}
}
.page.self-hosting {
text-align: center;
cursor: default;
h2 {
margin-top: 70px;
}
input {
display: inline-block;
width: 100%;
padding: 7px;
margin-bottom: 10px;
background: #FFF;
color: #333;
text-align: left;
border: 1px solid #AAA;
&:focus {
border: 1px solid @accent-primary;
}
&.error {
border: 1px solid #A33;
}
}
.message {
margin-top: 20px;
}
.self-hosting-container {
width: 400px;
display: block;
margin: 40px auto;
.section {
margin: 20px 0;
}
.api-root {
margin: 20px 5px 0 5px;
}
}
}
body.platform-win32 {
.page-frame {
.alpha-fade-enter {

View file

@ -193,11 +193,8 @@ export default class Application extends EventEmitter {
const accounts = this.config.get('nylas.accounts');
const hasAccount = accounts && accounts.length > 0;
const hasN1ID = this.config.get('nylas.identity.id');
const env = this.config.get('env');
const isLocalOrCustom = (env === 'local' || env === 'custom');
if (isLocalOrCustom || (hasAccount && hasN1ID)) {
if (hasAccount && hasN1ID) {
this.windowManager.ensureWindow(WindowManager.MAIN_WINDOW);
this.windowManager.ensureWindow(WindowManager.WORK_WINDOW);
} else {

View file

@ -85,12 +85,10 @@ class NylasAPI
@AppID = 'c5dis00do2vki9ib6hngrjs18'
@APIRoot = 'https://api-staging-experimental.nylas.com'
@pluginsSupported = true
else if env in ['local']
@AppID = NylasEnv.config.get('syncEngine.AppID') or 'n/a'
@APIRoot = 'http://localhost:5555'
else if env in ['custom']
else if env in ['custom', 'local']
@AppID = NylasEnv.config.get('syncEngine.AppID') or 'n/a'
@APIRoot = NylasEnv.config.get('syncEngine.APIRoot') or 'http://localhost:5555'
@pluginsSupported = false
current = {@AppID, @APIRoot, @APITokens}