Przeglądaj źródła

Merge branch 'invite'

Conflicts:
	public/css/less/gfbox.less
	public/emails/reset_password.html
	public/emails/welcome_on_signup.html
Torkel Ödegaard 10 lat temu
rodzic
commit
234d1291f9
75 zmienionych plików z 3549 dodań i 506 usunięć
  1. 12 0
      .editorconfig
  2. 1 0
      .gitignore
  3. 8 0
      emails/README.md
  4. 688 0
      emails/assets/css/ink.css
  5. 67 0
      emails/assets/css/style.css
  6. 8 0
      emails/grunt/aliases.yaml
  7. 16 0
      emails/grunt/assemble.js
  8. 5 0
      emails/grunt/clean.js
  9. 14 0
      emails/grunt/premailer.js
  10. 10 0
      emails/grunt/processhtml.js
  11. 13 0
      emails/grunt/replace.js
  12. 9 0
      emails/grunt/uncss.js
  13. 16 0
      emails/grunt/watch.js
  14. 6 0
      emails/gruntfile.js
  15. 24 0
      emails/package.json
  16. 38 0
      emails/templates/invited_to_org.html
  17. 79 0
      emails/templates/layouts/default.html
  18. 44 0
      emails/templates/new_user_invite.html
  19. 42 0
      emails/templates/reset_password.html
  20. 37 0
      emails/templates/welcome_on_signup.html
  21. 10 0
      pkg/api/api.go
  22. 26 0
      pkg/api/dtos/invite.go
  23. 204 0
      pkg/api/org_invite.go
  24. 1 0
      pkg/metrics/metrics.go
  25. 4 3
      pkg/models/org_user.go
  26. 91 0
      pkg/models/temp_user.go
  27. 1 0
      pkg/services/sqlstore/migrations/migrations.go
  28. 38 0
      pkg/services/sqlstore/migrations/temp_user.go
  29. 6 0
      pkg/services/sqlstore/org_users.go
  30. 1 1
      pkg/services/sqlstore/sqlstore.goconvey
  31. 108 0
      pkg/services/sqlstore/temp_user.go
  32. 51 0
      pkg/services/sqlstore/temp_user_test.go
  33. 18 0
      pkg/util/strings.go
  34. 3 1
      public/app/app.js
  35. 2 0
      public/app/components/require.config.js
  36. 1 0
      public/app/controllers/all.js
  37. 41 0
      public/app/controllers/invitedCtrl.js
  38. 11 11
      public/app/features/admin/partials/edit_user.html
  39. 4 11
      public/app/features/dashboard/unsavedChangesSrv.js
  40. 1 0
      public/app/features/org/all.js
  41. 27 3
      public/app/features/org/orgUsersCtrl.js
  42. 70 0
      public/app/features/org/partials/invite.html
  43. 58 44
      public/app/features/org/partials/orgUsers.html
  44. 48 0
      public/app/features/org/userInviteCtrl.js
  45. 2 2
      public/app/features/profile/partials/profile.html
  46. 3 0
      public/app/partials/bootstrap/tab.html
  47. 10 0
      public/app/partials/bootstrap/tabset.html
  48. 1 1
      public/app/partials/confirm_modal.html
  49. 2 0
      public/app/partials/login.html
  50. 95 0
      public/app/partials/signup_invited.html
  51. 4 0
      public/app/routes/all.js
  52. 1 1
      public/app/services/alertSrv.js
  53. 1 0
      public/app/services/utilSrv.js
  54. 1 2
      public/css/less/bootswatch.dark.less
  55. 1 13
      public/css/less/bootswatch.light.less
  56. 0 6
      public/css/less/gfbox.less
  57. 37 7
      public/css/less/grafana.less
  58. 42 0
      public/css/less/login.less
  59. 7 19
      public/css/less/tables_lists.less
  60. 33 0
      public/css/less/tabs.less
  61. 1 1
      public/css/less/tightform.less
  62. 247 0
      public/css/less/type.less
  63. 4 2
      public/css/less/variables.dark.less
  64. 9 10
      public/css/less/variables.light.less
  65. 2 0
      public/emails/README.md
  66. 198 0
      public/emails/invited_to_org.html
  67. 204 0
      public/emails/new_user_invite.html
  68. 193 61
      public/emails/reset_password.html
  69. 188 60
      public/emails/welcome_on_signup.html
  70. BIN
      public/img/background_tease.jpg
  71. 2 0
      public/test/test-main.js
  72. 6 0
      public/vendor/angular-ui/angular-bootstrap.js
  73. 293 0
      public/vendor/angular-ui/tabs.js
  74. 0 1
      public/vendor/bootstrap/less/bootstrap.less
  75. 0 246
      public/vendor/bootstrap/less/type.less

+ 12 - 0
.editorconfig

@@ -0,0 +1,12 @@
+# http://editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false

+ 1 - 0
.gitignore

@@ -3,6 +3,7 @@ coverage/
 .aws-config.json
 awsconfig
 /dist
+/emails/dist
 /tmp
 
 docs/AWS_S3_BUCKET

+ 8 - 0
emails/README.md

@@ -0,0 +1,8 @@
+
+- npm install
+- grunt   (default task will build new inlines email templates)
+- grunt watch  (will build on source html or css change)
+
+assembled email templates will be in dist/ and final
+inlined templates will be in ../public/emails/
+

+ 688 - 0
emails/assets/css/ink.css

@@ -0,0 +1,688 @@
+/**********************************************
+* Ink v1.0.5 - Copyright 2013 ZURB Inc        *
+**********************************************/
+
+/* Client-specific Styles & Reset */
+
+#outlook a {
+  padding:0;
+}
+
+body{
+  width:100% !important;
+  min-width: 100%;
+  -webkit-text-size-adjust:100%;
+  -ms-text-size-adjust:100%;
+  margin:0;
+  padding:0;
+}
+
+.ExternalClass {
+  width:100%;
+}
+
+.ExternalClass,
+.ExternalClass p,
+.ExternalClass span,
+.ExternalClass font,
+.ExternalClass td,
+.ExternalClass div {
+  line-height: 100%;
+}
+
+#backgroundTable {
+  margin:0;
+  padding:0;
+  width:100% !important;
+  line-height: 100% !important;
+}
+
+img {
+  outline:none;
+  text-decoration:none;
+  -ms-interpolation-mode: bicubic;
+  width: auto;
+  max-width: 100%;
+  float: left;
+  clear: both;
+  display: block;
+}
+
+center {
+  width: 100%;
+  min-width: 580px;
+}
+
+a img {
+  border: none;
+}
+
+p {
+  margin: 0 0 0 10px;
+}
+
+table {
+  border-spacing: 0;
+  border-collapse: collapse;
+}
+
+td {
+  word-break: break-word;
+  -webkit-hyphens: auto;
+  -moz-hyphens: auto;
+  hyphens: auto;
+  border-collapse: collapse !important;
+}
+
+table, tr, td {
+  padding: 0;
+  vertical-align: top;
+  text-align: left;
+}
+
+hr {
+  color: #d9d9d9;
+  background-color: #d9d9d9;
+  height: 1px;
+  border: none;
+}
+
+/* Responsive Grid */
+
+table.body {
+  height: 100%;
+  width: 100%;
+}
+
+table.container {
+  width: 580px;
+  margin: 0 auto;
+  text-align: inherit;
+}
+
+table.row {
+  padding: 0px;
+  width: 100%;
+  position: relative;
+}
+
+table.container table.row {
+  display: block;
+}
+
+td.wrapper {
+  padding: 10px 20px 0px 0px;
+  position: relative;
+}
+
+table.columns,
+table.column {
+  margin: 0 auto;
+}
+
+table.columns td,
+table.column td {
+  padding: 0px 0px 10px;
+}
+
+table.columns td.sub-columns,
+table.column td.sub-columns,
+table.columns td.sub-column,
+table.column td.sub-column {
+  padding-right: 10px;
+}
+
+td.sub-column, td.sub-columns {
+  min-width: 0px;
+}
+
+table.row td.last,
+table.container td.last {
+  padding-right: 0px;
+}
+
+table.one { width: 30px; }
+table.two { width: 80px; }
+table.three { width: 130px; }
+table.four { width: 180px; }
+table.five { width: 230px; }
+table.six { width: 280px; }
+table.seven { width: 330px; }
+table.eight { width: 380px; }
+table.nine { width: 430px; }
+table.ten { width: 480px; }
+table.eleven { width: 530px; }
+table.twelve { width: 580px; }
+
+table.one center { min-width: 30px; }
+table.two center { min-width: 80px; }
+table.three center { min-width: 130px; }
+table.four center { min-width: 180px; }
+table.five center { min-width: 230px; }
+table.six center { min-width: 280px; }
+table.seven center { min-width: 330px; }
+table.eight center { min-width: 380px; }
+table.nine center { min-width: 430px; }
+table.ten center { min-width: 480px; }
+table.eleven center { min-width: 530px; }
+table.twelve center { min-width: 580px; }
+
+table.one .panel center { min-width: 10px; }
+table.two .panel center { min-width: 60px; }
+table.three .panel center { min-width: 110px; }
+table.four .panel center { min-width: 160px; }
+table.five .panel center { min-width: 210px; }
+table.six .panel center { min-width: 260px; }
+table.seven .panel center { min-width: 310px; }
+table.eight .panel center { min-width: 360px; }
+table.nine .panel center { min-width: 410px; }
+table.ten .panel center { min-width: 460px; }
+table.eleven .panel center { min-width: 510px; }
+table.twelve .panel center { min-width: 560px; }
+
+.body .columns td.one,
+.body .column td.one { width: 8.333333%; }
+.body .columns td.two,
+.body .column td.two { width: 16.666666%; }
+.body .columns td.three,
+.body .column td.three { width: 25%; }
+.body .columns td.four,
+.body .column td.four { width: 33.333333%; }
+.body .columns td.five,
+.body .column td.five { width: 41.666666%; }
+.body .columns td.six,
+.body .column td.six { width: 50%; }
+.body .columns td.seven,
+.body .column td.seven { width: 58.333333%; }
+.body .columns td.eight,
+.body .column td.eight { width: 66.666666%; }
+.body .columns td.nine,
+.body .column td.nine { width: 75%; }
+.body .columns td.ten,
+.body .column td.ten { width: 83.333333%; }
+.body .columns td.eleven,
+.body .column td.eleven { width: 91.666666%; }
+.body .columns td.twelve,
+.body .column td.twelve { width: 100%; }
+
+td.offset-by-one { padding-left: 50px; }
+td.offset-by-two { padding-left: 100px; }
+td.offset-by-three { padding-left: 150px; }
+td.offset-by-four { padding-left: 200px; }
+td.offset-by-five { padding-left: 250px; }
+td.offset-by-six { padding-left: 300px; }
+td.offset-by-seven { padding-left: 350px; }
+td.offset-by-eight { padding-left: 400px; }
+td.offset-by-nine { padding-left: 450px; }
+td.offset-by-ten { padding-left: 500px; }
+td.offset-by-eleven { padding-left: 550px; }
+
+td.expander {
+  visibility: hidden;
+  width: 0px;
+  padding: 0 !important;
+}
+
+table.columns .text-pad,
+table.column .text-pad {
+  padding-left: 10px;
+  padding-right: 10px;
+}
+
+table.columns .left-text-pad,
+table.columns .text-pad-left,
+table.column .left-text-pad,
+table.column .text-pad-left {
+  padding-left: 10px;
+}
+
+table.columns .right-text-pad,
+table.columns .text-pad-right,
+table.column .right-text-pad,
+table.column .text-pad-right {
+  padding-right: 10px;
+}
+
+/* Block Grid */
+
+.block-grid {
+  width: 100%;
+  max-width: 580px;
+}
+
+.block-grid td {
+  display: inline-block;
+  padding:10px;
+}
+
+.two-up td {
+  width:270px;
+}
+
+.three-up td {
+  width:173px;
+}
+
+.four-up td {
+  width:125px;
+}
+
+.five-up td {
+  width:96px;
+}
+
+.six-up td {
+  width:76px;
+}
+
+.seven-up td {
+  width:62px;
+}
+
+.eight-up td {
+  width:52px;
+}
+
+/* Alignment & Visibility Classes */
+
+table.center, td.center {
+  text-align: center;
+}
+
+h1.center,
+h2.center,
+h3.center,
+h4.center,
+h5.center,
+h6.center {
+  text-align: center;
+}
+
+span.center {
+  display: block;
+  width: 100%;
+  text-align: center;
+}
+
+img.center {
+  margin: 0 auto;
+  float: none;
+}
+
+.show-for-small,
+.hide-for-desktop {
+  display: none;
+}
+
+/* Typography */
+
+body, table.body, h1, h2, h3, h4, h5, h6, p, td {
+  color: #222222;
+  font-family: "Helvetica", "Arial", sans-serif;
+  font-weight: normal;
+  padding:0;
+  margin: 0;
+  text-align: left;
+  line-height: 1.3;
+}
+
+h1, h2, h3, h4, h5, h6 {
+  word-break: normal;
+}
+
+h1 {font-size: 40px;}
+h2 {font-size: 36px;}
+h3 {font-size: 32px;}
+h4 {font-size: 28px;}
+h5 {font-size: 24px;}
+h6 {font-size: 20px;}
+body, table.body, p, td {font-size: 14px;line-height:19px;}
+
+p.lead, p.lede, p.leed {
+  font-size: 18px;
+  line-height:21px;
+}
+
+p {
+  margin-bottom: 10px;
+}
+
+small {
+  font-size: 10px;
+}
+
+a {
+  color: #2ba6cb;
+  text-decoration: none;
+}
+
+a:hover {
+  color: #2795b6 !important;
+}
+
+a:active {
+  color: #2795b6 !important;
+}
+
+a:visited {
+  color: #2ba6cb !important;
+}
+
+h1 a,
+h2 a,
+h3 a,
+h4 a,
+h5 a,
+h6 a {
+  color: #2ba6cb;
+}
+
+h1 a:active,
+h2 a:active,
+h3 a:active,
+h4 a:active,
+h5 a:active,
+h6 a:active {
+  color: #2ba6cb !important;
+}
+
+h1 a:visited,
+h2 a:visited,
+h3 a:visited,
+h4 a:visited,
+h5 a:visited,
+h6 a:visited {
+  color: #2ba6cb !important;
+}
+
+/* Panels */
+
+.panel {
+  background: #f2f2f2;
+  border: 1px solid #d9d9d9;
+  padding: 10px !important;
+}
+
+.sub-grid table {
+  width: 100%;
+}
+
+.sub-grid td.sub-columns {
+  padding-bottom: 0;
+}
+
+/* Buttons */
+
+table.button,
+table.tiny-button,
+table.small-button,
+table.medium-button,
+table.large-button {
+  width: 100%;
+  overflow: hidden;
+}
+
+table.button td,
+table.tiny-button td,
+table.small-button td,
+table.medium-button td,
+table.large-button td {
+  display: block;
+  width: auto !important;
+  text-align: center;
+  background: #2ba6cb;
+  border: 1px solid #2284a1;
+  color: #ffffff;
+  padding: 8px 0;
+}
+
+table.tiny-button td {
+  padding: 5px 0 4px;
+}
+
+table.small-button td {
+  padding: 8px 0 7px;
+}
+
+table.medium-button td {
+  padding: 12px 0 10px;
+}
+
+table.large-button td {
+  padding: 21px 0 18px;
+}
+
+table.button td a,
+table.tiny-button td a,
+table.small-button td a,
+table.medium-button td a,
+table.large-button td a {
+  font-weight: bold;
+  text-decoration: none;
+  font-family: Helvetica, Arial, sans-serif;
+  color: #ffffff;
+  font-size: 16px;
+}
+
+table.tiny-button td a {
+  font-size: 12px;
+  font-weight: normal;
+}
+
+table.small-button td a {
+  font-size: 16px;
+}
+
+table.medium-button td a {
+  font-size: 20px;
+}
+
+table.large-button td a {
+  font-size: 24px;
+}
+
+table.button:hover td,
+table.button:visited td,
+table.button:active td {
+  background: #2795b6 !important;
+}
+
+table.button:hover td a,
+table.button:visited td a,
+table.button:active td a {
+  color: #fff !important;
+}
+
+table.button:hover td,
+table.tiny-button:hover td,
+table.small-button:hover td,
+table.medium-button:hover td,
+table.large-button:hover td {
+  background: #2795b6 !important;
+}
+
+table.button:hover td a,
+table.button:active td a,
+table.button td a:visited,
+table.tiny-button:hover td a,
+table.tiny-button:active td a,
+table.tiny-button td a:visited,
+table.small-button:hover td a,
+table.small-button:active td a,
+table.small-button td a:visited,
+table.medium-button:hover td a,
+table.medium-button:active td a,
+table.medium-button td a:visited,
+table.large-button:hover td a,
+table.large-button:active td a,
+table.large-button td a:visited {
+  color: #ffffff !important;
+}
+
+table.secondary td {
+  background: #e9e9e9;
+  border-color: #d0d0d0;
+  color: #555;
+}
+
+table.secondary td a {
+  color: #555;
+}
+
+table.secondary:hover td {
+  background: #d0d0d0 !important;
+  color: #555;
+}
+
+table.secondary:hover td a,
+table.secondary td a:visited,
+table.secondary:active td a {
+  color: #555 !important;
+}
+
+table.success td {
+  background: #5da423;
+  border-color: #457a1a;
+}
+
+table.success:hover td {
+  background: #457a1a !important;
+}
+
+table.alert td {
+  background: #c60f13;
+  border-color: #970b0e;
+}
+
+table.alert:hover td {
+  background: #970b0e !important;
+}
+
+table.radius td {
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+}
+
+table.round td {
+  -webkit-border-radius: 500px;
+  -moz-border-radius: 500px;
+  border-radius: 500px;
+}
+
+/* Outlook First */
+
+body.outlook p {
+  display: inline !important;
+}
+
+/*  Media Queries */
+
+@media only screen and (max-width: 600px) {
+
+  table[class="body"] img {
+    width: auto !important;
+    height: auto !important;
+  }
+
+  table[class="body"] center {
+    min-width: 0 !important;
+  }
+
+  table[class="body"] .container {
+    width: 95% !important;
+  }
+
+  table[class="body"] .row {
+    width: 100% !important;
+    display: block !important;
+  }
+
+  table[class="body"] .wrapper {
+    display: block !important;
+    padding-right: 0 !important;
+  }
+
+  table[class="body"] .columns,
+  table[class="body"] .column {
+    table-layout: fixed !important;
+    float: none !important;
+    width: 100% !important;
+    padding-right: 0px !important;
+    padding-left: 0px !important;
+    display: block !important;
+  }
+
+  table[class="body"] .wrapper.first .columns,
+  table[class="body"] .wrapper.first .column {
+    display: table !important;
+  }
+
+  table[class="body"] table.columns td,
+  table[class="body"] table.column td {
+    width: 100% !important;
+  }
+
+  table[class="body"] .columns td.one,
+  table[class="body"] .column td.one { width: 8.333333% !important; }
+  table[class="body"] .columns td.two,
+  table[class="body"] .column td.two { width: 16.666666% !important; }
+  table[class="body"] .columns td.three,
+  table[class="body"] .column td.three { width: 25% !important; }
+  table[class="body"] .columns td.four,
+  table[class="body"] .column td.four { width: 33.333333% !important; }
+  table[class="body"] .columns td.five,
+  table[class="body"] .column td.five { width: 41.666666% !important; }
+  table[class="body"] .columns td.six,
+  table[class="body"] .column td.six { width: 50% !important; }
+  table[class="body"] .columns td.seven,
+  table[class="body"] .column td.seven { width: 58.333333% !important; }
+  table[class="body"] .columns td.eight,
+  table[class="body"] .column td.eight { width: 66.666666% !important; }
+  table[class="body"] .columns td.nine,
+  table[class="body"] .column td.nine { width: 75% !important; }
+  table[class="body"] .columns td.ten,
+  table[class="body"] .column td.ten { width: 83.333333% !important; }
+  table[class="body"] .columns td.eleven,
+  table[class="body"] .column td.eleven { width: 91.666666% !important; }
+  table[class="body"] .columns td.twelve,
+  table[class="body"] .column td.twelve { width: 100% !important; }
+
+  table[class="body"] td.offset-by-one,
+  table[class="body"] td.offset-by-two,
+  table[class="body"] td.offset-by-three,
+  table[class="body"] td.offset-by-four,
+  table[class="body"] td.offset-by-five,
+  table[class="body"] td.offset-by-six,
+  table[class="body"] td.offset-by-seven,
+  table[class="body"] td.offset-by-eight,
+  table[class="body"] td.offset-by-nine,
+  table[class="body"] td.offset-by-ten,
+  table[class="body"] td.offset-by-eleven {
+    padding-left: 0 !important;
+  }
+
+  table[class="body"] table.columns td.expander {
+    width: 1px !important;
+  }
+
+  table[class="body"] .right-text-pad,
+  table[class="body"] .text-pad-right {
+    padding-left: 10px !important;
+  }
+
+  table[class="body"] .left-text-pad,
+  table[class="body"] .text-pad-left {
+    padding-right: 10px !important;
+  }
+
+  table[class="body"] .hide-for-small,
+  table[class="body"] .show-for-desktop {
+    display: none !important;
+  }
+
+  table[class="body"] .show-for-small,
+  table[class="body"] .hide-for-desktop {
+    display: inherit !important;
+  }
+}

+ 67 - 0
emails/assets/css/style.css

@@ -0,0 +1,67 @@
+
+body, table.body, h1, h2, h3, h4, h5, h6, p, td {
+  font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -webkit-text-size-adjust: none;
+}
+
+table.facebook td {
+  background: #3b5998;
+  border-color: #2d4473;
+}
+
+table.facebook:hover td {
+  background: #2d4473 !important;
+}
+
+table.twitter td {
+  background: #00acee;
+  border-color: #0087bb;
+}
+
+table.twitter:hover td {
+  background: #0087bb !important;
+}
+
+table.google-plus td {
+  background-color: #DB4A39;
+  border-color: #CC0000;
+}
+
+table.google-plus:hover td {
+  background: #CC0000 !important;
+}
+
+.template-label {
+  color: #ffffff;
+  font-weight: bold;
+  font-size: 11px;
+}
+
+.callout .wrapper {
+  padding-bottom: 20px;
+}
+
+.callout .panel {
+  background: #ECF8FF;
+  border-color: #b9e5ff;
+}
+
+.header {
+  background: #333;
+}
+
+.footer {
+  margin-top: 20px;
+}
+
+@media only screen and (max-width: 600px) {
+  table[class="body"] .right-text-pad {
+    padding-left: 10px !important;
+  }
+
+  table[class="body"] .left-text-pad {
+    padding-right: 10px !important;
+  }
+}
+

+ 8 - 0
emails/grunt/aliases.yaml

@@ -0,0 +1,8 @@
+
+default:
+    - 'clean'
+    - 'assemble'
+    - 'replace'
+    - 'uncss'
+    - 'processhtml'
+    - 'premailer'

+ 16 - 0
emails/grunt/assemble.js

@@ -0,0 +1,16 @@
+module.exports = function() {
+  'use strict';
+  return  {
+    options: {
+      layout: 'templates/layouts/default.html',
+      partials: ['templates/partials/*.hbs'],
+      helpers: ['templates/helpers/**/*.js'],
+      data: [],
+      flatten: true
+    },
+    pages: {
+      src: ['templates/*.html'],
+      dest: 'dist/'
+    }
+  };
+};

+ 5 - 0
emails/grunt/clean.js

@@ -0,0 +1,5 @@
+module.exports = function(config) {
+  return {
+    dist: ['dist'],
+  };
+};

+ 14 - 0
emails/grunt/premailer.js

@@ -0,0 +1,14 @@
+module.exports = {
+	main: {
+    	options: {
+      		verbose: true,
+      		removeComments: true
+    	},
+      files: [{
+        expand: true,     // Enable dynamic expansion.
+        cwd: 'dist',      // Src matches are relative to this path.
+        src: ['*.html'], // Actual pattern(s) to match.
+        dest: '../public/emails/',   // Destination path prefix.
+      }],
+  	}
+};

+ 10 - 0
emails/grunt/processhtml.js

@@ -0,0 +1,10 @@
+module.exports = {
+	dist: {
+    files: [{
+      expand: true,     // Enable dynamic expansion.
+      cwd: 'dist',      // Src matches are relative to this path.
+      src: ['*.html'], // Actual pattern(s) to match.
+      dest: 'dist/',   // Destination path prefix.
+    }],
+  }
+};

+ 13 - 0
emails/grunt/replace.js

@@ -0,0 +1,13 @@
+module.exports = {
+	dist: {
+    overwrite: true,
+    src: ['dist/*.html'],
+    replacements: [{
+      from: '[[',
+      to: '{{'
+    }, {
+      from: ']]',
+      to:  '}}'
+    }]
+  }
+};

+ 9 - 0
emails/grunt/uncss.js

@@ -0,0 +1,9 @@
+module.exports = {
+    dist: {
+        src: ['dist/*.html'],
+        dest: 'dist/css/tidy.css',
+        options: {
+            report: 'min' // optional: include to report savings
+        }
+    }
+};

+ 16 - 0
emails/grunt/watch.js

@@ -0,0 +1,16 @@
+module.exports = {
+    src: {
+        files: [
+            //what are the files that we want to watch
+            'assets/css/*.css',
+            'templates/**/*.html',
+            'grunt/*.js',
+        ],
+        tasks: ['default'],
+        options: {
+            nospawn: true,
+            livereload: true,
+        }
+    }
+
+};

+ 6 - 0
emails/gruntfile.js

@@ -0,0 +1,6 @@
+module.exports = function(grunt) {
+
+  // load grunt config
+  require('load-grunt-config')(grunt);
+
+};

+ 24 - 0
emails/package.json

@@ -0,0 +1,24 @@
+{
+  "name": "Grafana-Email-Campaign",
+  "version": "1.0.0",
+  "description": "Grafana Email templates based on Zurb Ink",
+  "repository": "dnnsldr/",
+  "author": {
+    "name": "dnnsldr",
+    "email": "delder@riester.com",
+    "url": "https://github.com/dnnsldr"
+  },
+  "devDependencies": {
+    "grunt": "^0.4.5",
+    "grunt-premailer": "^0.2.10",
+    "grunt-processhtml": "^0.3.3",
+    "grunt-uncss": "^0.3.7",
+    "load-grunt-config": "^0.14.0",
+    "grunt-contrib-watch": "^0.6.1",
+    "grunt-text-replace": "^0.3.12"
+  },
+  "dependencies": {
+    "grunt-assemble": "^0.4.0",
+    "grunt-contrib-clean": "^0.6.0"
+  }
+}

+ 38 - 0
emails/templates/invited_to_org.html

@@ -0,0 +1,38 @@
+<!-- This email is sent when an existing user is added to an organization -->
+
+[[Subject .Subject "[[.InvitedBy]] has added you to the Grafana organization [[.OrgName]]"]]
+
+<table class="row">
+	<tr>
+		<td class="wrapper last">
+
+			<table class="twelve columns">
+				<tr>
+					<td>
+						<h3>You have been added to the Grafana organization [[.OrgName]]</h3>
+					</td>
+					<td class="expander"></td>
+				</tr>
+			</table>
+
+		</td>
+	</tr>
+</table>
+
+<table class="row">
+	<tr>
+		<td class="wrapper last">
+			<table class="twelve columns">
+				<tr>
+					<td class="center">
+						<p>You can switch organization in the left side menu, in the dropdown below your username.</p>
+					</td>
+					<td class="expander"></td>
+				</tr>
+			</table>
+
+		</td>
+	</tr>
+</table>
+
+

+ 79 - 0
emails/templates/layouts/default.html

@@ -0,0 +1,79 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+	<meta name="viewport" content="width=device-width"/>
+   <!-- build:css css/tidy.css -->
+  <link inline rel="stylesheet" href="../assets/css/ink.css">
+  <link inline rel="stylesheet" href="../assets/css/style.css">
+  <!-- /build -->
+</head>
+<body>
+	<table class="body">
+		<tr>
+			<td class="center" align="center" valign="top">
+        <center>
+
+          <table class="row header">
+            <tr>
+              <td class="center" align="center">
+                <center>
+
+                  <table class="container">
+                    <tr>
+                      <td class="wrapper last">
+
+                        <table class="twelve columns">
+                          <tr>
+                            <td class="six sub-columns center">
+															<img src="http://docs.grafana.org/img/logo_transparent_200x75.png" style="width: 150px; float: none; display: inline">
+                            </td>
+														<td class="expander"></td>
+                          </tr>
+                        </table>
+
+                      </td>
+                    </tr>
+                  </table>
+
+                </center>
+              </td>
+            </tr>
+          </table>
+
+					<table class="container">
+						<tr>
+							<td>
+								{{> body }}
+								<!-- footer -->
+								<table class="row footer">
+									<tr>
+										<td class="wrapper last">
+											<table class="twelve columns">
+												<tr>
+													<td align="center">
+														<center>
+															<p style="text-align:center;">
+																Sent by <a href="[[.AppUrl]]">Grafana v[[.BuildVersion]]</a>
+															</p>
+														</center>
+													</td>
+													<td class="expander"></td>
+												</tr>
+											</table>
+										</td>
+									</tr>
+								</table>
+
+								<!-- container end below -->
+							</td>
+						</tr>
+
+					</table>
+				</center>
+			</td>
+		</tr>
+
+	</table>
+</body>
+</html>

+ 44 - 0
emails/templates/new_user_invite.html

@@ -0,0 +1,44 @@
+<!-- This email is sent when user who does not already exist in Grafana is added to an organization -->
+
+[[Subject .Subject "[[.InvitedBy]] has invited you to join Grafana"]]
+
+<table class="row">
+	<tr>
+		<td class="wrapper last">
+
+			<table class="twelve columns">
+				<tr>
+					<td>
+						<h3>You're invited to sign up to Grafana and join organization [[.OrgName]]</h3>
+					</td>
+					<td class="expander"></td>
+				</tr>
+			</table>
+
+		</td>
+	</tr>
+</table>
+
+<table class="row">
+	<tr>
+		<td class="wrapper last">
+			<table class="twelve columns">
+				<tr>
+					<td class="center">
+						<table class="button radius">
+							<tr>
+								<td>
+									<a href="[[.LinkUrl]]">Complete Sign Up</a>
+								</td>
+							</tr>
+						</table>
+					</td>
+					<td class="expander"></td>
+				</tr>
+			</table>
+
+		</td>
+	</tr>
+</table>
+
+

+ 42 - 0
emails/templates/reset_password.html

@@ -0,0 +1,42 @@
+[[Subject .Subject "Reset your Grafana password - [[.Name]]"]]
+
+<table class="row">
+	<tr>
+		<td class="wrapper last">
+
+			<table class="twelve columns">
+				<tr>
+					<td>
+						<h3>Hi [[.Name]]</h3>
+					</td>
+					<td class="expander"></td>
+				</tr>
+			</table>
+
+		</td>
+	</tr>
+</table>
+
+<table class="row">
+	<tr>
+		<td class="wrapper last">
+			<table class="twelve columns">
+				<tr>
+					<td class="center">
+						<p>
+							Please click the following link to reset your password within <strong>[[.EmailCodeValidHours]] hours</strong>.
+						</p>
+						<p>
+							<a href="[[.AppUrl]]user/password/reset?code=[[.Code]]">[[.AppUrl]]user/password/reset?code=[[.Code]]</a>
+						</p>
+						<p>Not working? Try copying and pasting it to your browser.</p>
+					</td>
+					<td class="expander"></td>
+				</tr>
+			</table>
+
+		</td>
+	</tr>
+</table>
+
+

+ 37 - 0
emails/templates/welcome_on_signup.html

@@ -0,0 +1,37 @@
+[[Subject .Subject "Welcome to Grafana"]]
+
+<table class="row">
+	<tr>
+		<td class="wrapper last">
+
+			<table class="twelve columns">
+				<tr>
+					<td>
+						<h3>Hi [[.Name]]</h3>
+					</td>
+					<td class="expander"></td>
+				</tr>
+			</table>
+
+		</td>
+	</tr>
+</table>
+
+<table class="row">
+	<tr>
+		<td class="wrapper last">
+			<table class="twelve columns">
+				<tr>
+					<td class="center">
+						<p>
+							If you are new to Grafana please read the <a href="http://docs.grafana.org/guides/gettingstarted/">Getting Started</a> guide.
+						</p>
+					</td>
+					<td class="expander"></td>
+				</tr>
+			</table>
+		</td>
+	</tr>
+</table>
+
+

+ 10 - 0
pkg/api/api.go

@@ -22,6 +22,7 @@ func Register(r *macaron.Macaron) {
 	r.Post("/login", bind(dtos.LoginCommand{}), wrap(LoginPost))
 	r.Get("/login/:name", OAuthLogin)
 	r.Get("/login", LoginView)
+	r.Get("/invite/:code", Index)
 
 	// authed views
 	r.Get("/profile/", reqSignedIn, Index)
@@ -42,6 +43,10 @@ func Register(r *macaron.Macaron) {
 	r.Get("/signup", Index)
 	r.Post("/api/user/signup", bind(m.CreateUserCommand{}), wrap(SignUp))
 
+	// invited
+	r.Get("/api/user/invite/:code", wrap(GetInviteInfoByCode))
+	r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), wrap(CompleteInvite))
+
 	// reset password
 	r.Get("/user/password/send-reset-email", Index)
 	r.Get("/user/password/reset", Index)
@@ -89,6 +94,11 @@ func Register(r *macaron.Macaron) {
 			r.Get("/users", wrap(GetOrgUsersForCurrentOrg))
 			r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg))
 			r.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg))
+
+			// invites
+			r.Get("/invites", wrap(GetPendingOrgInvites))
+			r.Post("/invites", bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
+			r.Patch("/invites/:code/revoke", wrap(RevokeInvite))
 		}, regOrgAdmin)
 
 		// create new org

+ 26 - 0
pkg/api/dtos/invite.go

@@ -0,0 +1,26 @@
+package dtos
+
+import m "github.com/grafana/grafana/pkg/models"
+
+type AddInviteForm struct {
+	LoginOrEmail string     `json:"loginOrEmail" binding:"Required"`
+	Name         string     `json:"name"`
+	Role         m.RoleType `json:"role" binding:"Required"`
+	SkipEmails   bool       `json:"skipEmails"`
+}
+
+type InviteInfo struct {
+	Email     string `json:"email"`
+	Name      string `json:"name"`
+	Username  string `json:"username"`
+	InvitedBy string `json:"invitedBy"`
+}
+
+type CompleteInviteForm struct {
+	InviteCode      string `json:"inviteCode"`
+	Email           string `json:"email" binding:"Required"`
+	Name            string `json:"name"`
+	Username        string `json:"username"`
+	Password        string `json:"password"`
+	ConfirmPassword string `json:"confirmPassword"`
+}

+ 204 - 0
pkg/api/org_invite.go

@@ -0,0 +1,204 @@
+package api
+
+import (
+	"fmt"
+
+	"github.com/grafana/grafana/pkg/api/dtos"
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/events"
+	"github.com/grafana/grafana/pkg/metrics"
+	"github.com/grafana/grafana/pkg/middleware"
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/setting"
+	"github.com/grafana/grafana/pkg/util"
+)
+
+func GetPendingOrgInvites(c *middleware.Context) Response {
+	query := m.GetTempUsersForOrgQuery{OrgId: c.OrgId, Status: m.TmpUserInvitePending}
+
+	if err := bus.Dispatch(&query); err != nil {
+		return ApiError(500, "Failed to get invites from db", err)
+	}
+
+	for _, invite := range query.Result {
+		invite.Url = setting.ToAbsUrl("invite/" + invite.Code)
+	}
+
+	return Json(200, query.Result)
+}
+
+func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response {
+	if !inviteDto.Role.IsValid() {
+		return ApiError(400, "Invalid role specified", nil)
+	}
+
+	// first try get existing user
+	userQuery := m.GetUserByLoginQuery{LoginOrEmail: inviteDto.LoginOrEmail}
+	if err := bus.Dispatch(&userQuery); err != nil {
+		if err != m.ErrUserNotFound {
+			return ApiError(500, "Failed to query db for existing user check", err)
+		}
+	} else {
+		return inviteExistingUserToOrg(c, userQuery.Result, &inviteDto)
+	}
+
+	cmd := m.CreateTempUserCommand{}
+	cmd.OrgId = c.OrgId
+	cmd.Email = inviteDto.LoginOrEmail
+	cmd.Name = inviteDto.Name
+	cmd.Status = m.TmpUserInvitePending
+	cmd.InvitedByUserId = c.UserId
+	cmd.Code = util.GetRandomString(30)
+	cmd.Role = inviteDto.Role
+	cmd.RemoteAddr = c.Req.RemoteAddr
+
+	if err := bus.Dispatch(&cmd); err != nil {
+		return ApiError(500, "Failed to save invite to database", err)
+	}
+
+	// send invite email
+	if !inviteDto.SkipEmails && util.IsEmail(inviteDto.LoginOrEmail) {
+		emailCmd := m.SendEmailCommand{
+			To:       []string{inviteDto.LoginOrEmail},
+			Template: "new_user_invite.html",
+			Data: map[string]interface{}{
+				"Name":      util.StringsFallback2(cmd.Name, cmd.Email),
+				"OrgName":   c.OrgName,
+				"Email":     c.Email,
+				"LinkUrl":   setting.ToAbsUrl("invite/" + cmd.Code),
+				"InvitedBy": util.StringsFallback3(c.Name, c.Email, c.Login),
+			},
+		}
+
+		if err := bus.Dispatch(&emailCmd); err != nil {
+			return ApiError(500, "Failed to send email invite", err)
+		}
+
+		return ApiSuccess(fmt.Sprintf("Sent invite to %s", inviteDto.LoginOrEmail))
+	}
+
+	return ApiSuccess(fmt.Sprintf("Created invite for %s", inviteDto.LoginOrEmail))
+}
+
+func inviteExistingUserToOrg(c *middleware.Context, user *m.User, inviteDto *dtos.AddInviteForm) Response {
+	// user exists, add org role
+	createOrgUserCmd := m.AddOrgUserCommand{OrgId: c.OrgId, UserId: user.Id, Role: inviteDto.Role}
+	if err := bus.Dispatch(&createOrgUserCmd); err != nil {
+		if err == m.ErrOrgUserAlreadyAdded {
+			return ApiError(412, fmt.Sprintf("User %s is already added to organization", inviteDto.LoginOrEmail), err)
+		}
+		return ApiError(500, "Error while trying to create org user", err)
+	} else {
+
+		if !inviteDto.SkipEmails && util.IsEmail(user.Email) {
+			emailCmd := m.SendEmailCommand{
+				To:       []string{user.Email},
+				Template: "invited_to_org.html",
+				Data: map[string]interface{}{
+					"Name":      user.NameOrFallback(),
+					"OrgName":   c.OrgName,
+					"InvitedBy": util.StringsFallback3(c.Name, c.Email, c.Login),
+				},
+			}
+
+			if err := bus.Dispatch(&emailCmd); err != nil {
+				return ApiError(500, "Failed to send email invited_to_org", err)
+			}
+		}
+
+		return ApiSuccess(fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.OrgName))
+	}
+}
+
+func RevokeInvite(c *middleware.Context) Response {
+	cmd := m.UpdateTempUserStatusCommand{
+		Code:   c.Params(":code"),
+		Status: m.TmpUserRevoked,
+	}
+
+	if err := bus.Dispatch(&cmd); err != nil {
+		return ApiError(500, "Failed to update invite status", err)
+	}
+
+	return ApiSuccess("Invite revoked")
+}
+
+func GetInviteInfoByCode(c *middleware.Context) Response {
+	query := m.GetTempUserByCodeQuery{Code: c.Params(":code")}
+
+	if err := bus.Dispatch(&query); err != nil {
+		if err == m.ErrTempUserNotFound {
+			return ApiError(404, "Invite not found", nil)
+		}
+		return ApiError(500, "Failed to get invite", err)
+	}
+
+	invite := query.Result
+
+	return Json(200, dtos.InviteInfo{
+		Email:     invite.Email,
+		Name:      invite.Name,
+		Username:  invite.Email,
+		InvitedBy: util.StringsFallback3(invite.InvitedByName, invite.InvitedByLogin, invite.InvitedByEmail),
+	})
+}
+
+func CompleteInvite(c *middleware.Context, completeInvite dtos.CompleteInviteForm) Response {
+	query := m.GetTempUserByCodeQuery{Code: completeInvite.InviteCode}
+
+	if err := bus.Dispatch(&query); err != nil {
+		if err == m.ErrTempUserNotFound {
+			return ApiError(404, "Invite not found", nil)
+		}
+		return ApiError(500, "Failed to get invite", err)
+	}
+
+	invite := query.Result
+	if invite.Status != m.TmpUserInvitePending {
+		return ApiError(412, fmt.Sprintf("Invite cannot be used in status %s", invite.Status), nil)
+	}
+
+	cmd := m.CreateUserCommand{
+		Email:    completeInvite.Email,
+		Name:     completeInvite.Name,
+		Login:    completeInvite.Username,
+		Password: completeInvite.Password,
+	}
+
+	if err := bus.Dispatch(&cmd); err != nil {
+		return ApiError(500, "failed to create user", err)
+	}
+
+	user := cmd.Result
+
+	bus.Publish(&events.UserSignedUp{
+		Id:    user.Id,
+		Name:  user.Name,
+		Email: user.Email,
+		Login: user.Login,
+	})
+
+	// add to org
+	addOrgUserCmd := m.AddOrgUserCommand{OrgId: invite.OrgId, UserId: user.Id, Role: invite.Role}
+	if err := bus.Dispatch(&addOrgUserCmd); err != nil {
+		return ApiError(500, "Error while trying to create org user", err)
+	}
+
+	// set org to active
+	if err := bus.Dispatch(&m.SetUsingOrgCommand{OrgId: invite.OrgId, UserId: user.Id}); err != nil {
+		return ApiError(500, "Failed to set org as active", err)
+	}
+
+	// update temp user status
+	updateTmpUserCmd := m.UpdateTempUserStatusCommand{Code: invite.Code, Status: m.TmpUserCompleted}
+	if err := bus.Dispatch(&updateTmpUserCmd); err != nil {
+		return ApiError(500, "Failed to update invite status", err)
+	}
+
+	loginUserWithUser(&user, c)
+
+	metrics.M_Api_User_SignUp.Inc(1)
+	metrics.M_Api_User_SignUpInvite.Inc(1)
+
+	return ApiSuccess("User created and logged in")
+}

+ 1 - 0
pkg/metrics/metrics.go

@@ -14,6 +14,7 @@ var (
 	M_Api_Status_404 = NewComboCounterRef("api.status.404")
 
 	M_Api_User_SignUp       = NewComboCounterRef("api.user.signup")
+	M_Api_User_SignUpInvite = NewComboCounterRef("api.user.signup_invite")
 	M_Api_Dashboard_Get     = NewComboCounterRef("api.dashboard.get")
 	M_Api_Dashboard_Post    = NewComboCounterRef("api.dashboard.post")
 	M_Api_Admin_User_Create = NewComboCounterRef("api.admin.user_create")

+ 4 - 3
pkg/models/org_user.go

@@ -7,9 +7,10 @@ import (
 
 // Typed errors
 var (
-	ErrInvalidRoleType = errors.New("Invalid role type")
-	ErrLastOrgAdmin    = errors.New("Cannot remove last organization admin")
-	ErrOrgUserNotFound = errors.New("Cannot find the organization user")
+	ErrInvalidRoleType     = errors.New("Invalid role type")
+	ErrLastOrgAdmin        = errors.New("Cannot remove last organization admin")
+	ErrOrgUserNotFound     = errors.New("Cannot find the organization user")
+	ErrOrgUserAlreadyAdded = errors.New("User is already added to organization")
 )
 
 type RoleType string

+ 91 - 0
pkg/models/temp_user.go

@@ -0,0 +1,91 @@
+package models
+
+import (
+	"errors"
+	"time"
+)
+
+// Typed errors
+var (
+	ErrTempUserNotFound = errors.New("User not found")
+)
+
+type TempUserStatus string
+
+const (
+	TmpUserInvitePending TempUserStatus = "InvitePending"
+	TmpUserCompleted     TempUserStatus = "Completed"
+	TmpUserEmailPending  TempUserStatus = "EmailPending"
+	TmpUserRevoked       TempUserStatus = "Revoked"
+)
+
+// TempUser holds data for org invites and unconfirmed sign ups
+type TempUser struct {
+	Id              int64
+	OrgId           int64
+	Version         int
+	Email           string
+	Name            string
+	Role            RoleType
+	InvitedByUserId int64
+	Status          TempUserStatus
+
+	EmailSent   bool
+	EmailSentOn time.Time
+	Code        string
+	RemoteAddr  string
+
+	Created time.Time
+	Updated time.Time
+}
+
+// ---------------------
+// COMMANDS
+
+type CreateTempUserCommand struct {
+	Email           string
+	Name            string
+	OrgId           int64
+	InvitedByUserId int64
+	Status          TempUserStatus
+	Code            string
+	Role            RoleType
+	RemoteAddr      string
+
+	Result *TempUser
+}
+
+type UpdateTempUserStatusCommand struct {
+	Code   string
+	Status TempUserStatus
+}
+
+type GetTempUsersForOrgQuery struct {
+	OrgId  int64
+	Status TempUserStatus
+
+	Result []*TempUserDTO
+}
+
+type GetTempUserByCodeQuery struct {
+	Code string
+
+	Result *TempUserDTO
+}
+
+type TempUserDTO struct {
+	Id             int64          `json:"id"`
+	OrgId          int64          `json:"orgId"`
+	Name           string         `json:"name"`
+	Email          string         `json:"email"`
+	Role           RoleType       `json:"role"`
+	InvitedByLogin string         `json:"invitedByLogin"`
+	InvitedByEmail string         `json:"invitedByEmail"`
+	InvitedByName  string         `json:"invitedByName"`
+	Code           string         `json:"code"`
+	Status         TempUserStatus `json:"status"`
+	Url            string         `json:"url"`
+	EmailSent      bool           `json:"emailSent"`
+	EmailSentOn    time.Time      `json:"emailSentOn"`
+	Created        time.Time      `json:"createdOn"`
+}

+ 1 - 0
pkg/services/sqlstore/migrations/migrations.go

@@ -10,6 +10,7 @@ import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
 func AddMigrations(mg *Migrator) {
 	addMigrationLogMigrations(mg)
 	addUserMigrations(mg)
+	addTempUserMigrations(mg)
 	addStarMigrations(mg)
 	addOrgMigrations(mg)
 	addDashboardMigration(mg)

+ 38 - 0
pkg/services/sqlstore/migrations/temp_user.go

@@ -0,0 +1,38 @@
+package migrations
+
+import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+
+func addTempUserMigrations(mg *Migrator) {
+	tempUserV1 := Table{
+		Name: "temp_user",
+		Columns: []*Column{
+			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
+			{Name: "org_id", Type: DB_BigInt, Nullable: false},
+			{Name: "version", Type: DB_Int, Nullable: false},
+			{Name: "email", Type: DB_NVarchar, Length: 255},
+			{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: true},
+			{Name: "role", Type: DB_NVarchar, Length: 20, Nullable: true},
+			{Name: "code", Type: DB_NVarchar, Length: 255},
+			{Name: "status", Type: DB_Varchar, Length: 20},
+			{Name: "invited_by_user_id", Type: DB_BigInt, Nullable: true},
+			{Name: "email_sent", Type: DB_Bool},
+			{Name: "email_sent_on", Type: DB_DateTime, Nullable: true},
+			{Name: "remote_addr", Type: DB_Varchar, Nullable: true},
+			{Name: "created", Type: DB_DateTime},
+			{Name: "updated", Type: DB_DateTime},
+		},
+		Indices: []*Index{
+			{Cols: []string{"email"}, Type: IndexType},
+			{Cols: []string{"org_id"}, Type: IndexType},
+			{Cols: []string{"code"}, Type: IndexType},
+			{Cols: []string{"status"}, Type: IndexType},
+		},
+	}
+
+	// addDropAllIndicesMigrations(mg, "v7", tempUserV1)
+	// mg.AddMigration("Drop old table tempUser v7", NewDropTableMigration("temp_user"))
+
+	// create table
+	mg.AddMigration("create temp user table v1-7", NewAddTableMigration(tempUserV1))
+	addTableIndicesMigrations(mg, "v1-7", tempUserV1)
+}

+ 6 - 0
pkg/services/sqlstore/org_users.go

@@ -19,6 +19,12 @@ func init() {
 
 func AddOrgUser(cmd *m.AddOrgUserCommand) error {
 	return inTransaction(func(sess *xorm.Session) error {
+		// check if user exists
+		if res, err := sess.Query("SELECT 1 from org_user WHERE org_id=? and user_id=?", cmd.OrgId, cmd.UserId); err != nil {
+			return err
+		} else if len(res) == 1 {
+			return m.ErrOrgUserAlreadyAdded
+		}
 
 		entity := m.OrgUser{
 			OrgId:   cmd.OrgId,

+ 1 - 1
pkg/services/sqlstore/sqlstore.goconvey

@@ -1 +1 @@
--timeout=10s
+-timeout=20s

+ 108 - 0
pkg/services/sqlstore/temp_user.go

@@ -0,0 +1,108 @@
+package sqlstore
+
+import (
+	"time"
+
+	"github.com/go-xorm/xorm"
+	"github.com/grafana/grafana/pkg/bus"
+	m "github.com/grafana/grafana/pkg/models"
+)
+
+func init() {
+	bus.AddHandler("sql", CreateTempUser)
+	bus.AddHandler("sql", GetTempUsersForOrg)
+	bus.AddHandler("sql", UpdateTempUserStatus)
+	bus.AddHandler("sql", GetTempUserByCode)
+}
+
+func UpdateTempUserStatus(cmd *m.UpdateTempUserStatusCommand) error {
+	return inTransaction(func(sess *xorm.Session) error {
+		var rawSql = "UPDATE temp_user SET status=? WHERE code=?"
+		_, err := sess.Exec(rawSql, string(cmd.Status), cmd.Code)
+		return err
+	})
+}
+
+func CreateTempUser(cmd *m.CreateTempUserCommand) error {
+	return inTransaction2(func(sess *session) error {
+
+		// create user
+		user := &m.TempUser{
+			Email:           cmd.Email,
+			Name:            cmd.Name,
+			OrgId:           cmd.OrgId,
+			Code:            cmd.Code,
+			Role:            cmd.Role,
+			Status:          cmd.Status,
+			RemoteAddr:      cmd.RemoteAddr,
+			InvitedByUserId: cmd.InvitedByUserId,
+			Created:         time.Now(),
+			Updated:         time.Now(),
+		}
+
+		if _, err := sess.Insert(user); err != nil {
+			return err
+		}
+
+		cmd.Result = user
+		return nil
+	})
+}
+
+func GetTempUsersForOrg(query *m.GetTempUsersForOrgQuery) error {
+	var rawSql = `SELECT
+	                tu.id             as id,
+	                tu.org_id         as org_id,
+	                tu.email          as email,
+									tu.name           as name,
+									tu.role           as role,
+									tu.code           as code,
+									tu.status         as status,
+									tu.email_sent     as email_sent,
+									tu.email_sent_on  as email_sent_on,
+									tu.created				as created,
+									u.login						as invited_by_login,
+									u.name						as invited_by_name,
+									u.email						as invited_by_email
+	                FROM ` + dialect.Quote("temp_user") + ` as tu
+									LEFT OUTER JOIN ` + dialect.Quote("user") + ` as u on u.id = tu.invited_by_user_id
+	                WHERE tu.org_id=? AND tu.status =? ORDER BY tu.created desc`
+
+	query.Result = make([]*m.TempUserDTO, 0)
+	sess := x.Sql(rawSql, query.OrgId, string(query.Status))
+	err := sess.Find(&query.Result)
+	return err
+}
+
+func GetTempUserByCode(query *m.GetTempUserByCodeQuery) error {
+	var rawSql = `SELECT
+	                tu.id             as id,
+	                tu.org_id         as org_id,
+	                tu.email          as email,
+									tu.name           as name,
+									tu.role           as role,
+									tu.code           as code,
+									tu.status         as status,
+									tu.email_sent     as email_sent,
+									tu.email_sent_on  as email_sent_on,
+									tu.created				as created,
+									u.login						as invited_by_login,
+									u.name						as invited_by_name,
+									u.email						as invited_by_email
+	                FROM ` + dialect.Quote("temp_user") + ` as tu
+									LEFT OUTER JOIN ` + dialect.Quote("user") + ` as u on u.id = tu.invited_by_user_id
+	                WHERE tu.code=?`
+
+	var tempUser m.TempUserDTO
+	sess := x.Sql(rawSql, query.Code)
+	has, err := sess.Get(&tempUser)
+
+	if err != nil {
+		return err
+	} else if has == false {
+		return m.ErrTempUserNotFound
+	}
+
+	query.Result = &tempUser
+	return err
+}

+ 51 - 0
pkg/services/sqlstore/temp_user_test.go

@@ -0,0 +1,51 @@
+package sqlstore
+
+import (
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+
+	m "github.com/grafana/grafana/pkg/models"
+)
+
+func TestTempUserCommandsAndQueries(t *testing.T) {
+
+	Convey("Testing Temp User commands & queries", t, func() {
+		InitTestDB(t)
+
+		Convey("Given saved api key", func() {
+			cmd := m.CreateTempUserCommand{
+				OrgId:  2256,
+				Name:   "hello",
+				Code:   "asd",
+				Email:  "e@as.co",
+				Status: m.TmpUserInvitePending,
+			}
+			err := CreateTempUser(&cmd)
+			So(err, ShouldBeNil)
+
+			Convey("Should be able to get temp users by org id", func() {
+				query := m.GetTempUsersForOrgQuery{OrgId: 2256, Status: m.TmpUserInvitePending}
+				err = GetTempUsersForOrg(&query)
+
+				So(err, ShouldBeNil)
+				So(len(query.Result), ShouldEqual, 1)
+			})
+
+			Convey("Should be able to get temp users by code", func() {
+				query := m.GetTempUserByCodeQuery{Code: "asd"}
+				err = GetTempUserByCode(&query)
+
+				So(err, ShouldBeNil)
+				So(query.Result.Name, ShouldEqual, "hello")
+			})
+
+			Convey("Should be able update status", func() {
+				cmd2 := m.UpdateTempUserStatusCommand{Code: "asd", Status: m.TmpUserRevoked}
+				err := UpdateTempUserStatus(&cmd2)
+				So(err, ShouldBeNil)
+			})
+
+		})
+	})
+}

+ 18 - 0
pkg/util/strings.go

@@ -0,0 +1,18 @@
+package util
+
+func StringsFallback2(val1 string, val2 string) string {
+	if val1 != "" {
+		return val1
+	}
+	return val2
+}
+
+func StringsFallback3(val1 string, val2 string, val3 string) string {
+	if val1 != "" {
+		return val1
+	}
+	if val2 != "" {
+		return val2
+	}
+	return val3
+}

+ 3 - 1
public/app/app.js

@@ -12,6 +12,7 @@ define([
   'angular-sanitize',
   'angular-strap',
   'angular-dragdrop',
+  'angular-ui',
   'extend-jquery',
   'bindonce',
 ],
@@ -64,7 +65,8 @@ function (angular, $, _, appLevelRequire) {
     '$strap.directives',
     'ang-drag-drop',
     'grafana',
-    'pasvaz.bindonce'
+    'pasvaz.bindonce',
+    'ui.bootstrap.tabs',
   ];
 
   var module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];

+ 2 - 0
public/app/components/require.config.js

@@ -17,6 +17,7 @@ require.config({
     'angular-sanitize':       '../vendor/angular-sanitize/angular-sanitize',
     'angular-dragdrop':       '../vendor/angular-native-dragdrop/draganddrop',
     'angular-strap':          '../vendor/angular-other/angular-strap',
+    'angular-ui':             '../vendor/angular-ui/angular-bootstrap',
     timepicker:               '../vendor/angular-other/timepicker',
     datepicker:               '../vendor/angular-other/datepicker',
     bindonce:                 '../vendor/angular-bindonce/bindonce',
@@ -90,6 +91,7 @@ require.config({
     'angular-dragdrop':     ['jquery', 'angular'],
     'angular-mocks':        ['angular'],
     'angular-sanitize':     ['angular'],
+    'angular-ui':           ['angular'],
     'angular-route':        ['angular'],
     'angular-strap':        ['angular', 'bootstrap','timepicker', 'datepicker'],
     'bindonce':             ['angular'],

+ 1 - 0
public/app/controllers/all.js

@@ -6,6 +6,7 @@ define([
   './inspectCtrl',
   './jsonEditorCtrl',
   './loginCtrl',
+  './invitedCtrl',
   './resetPasswordCtrl',
   './sidemenuCtrl',
   './errorCtrl',

+ 41 - 0
public/app/controllers/invitedCtrl.js

@@ -0,0 +1,41 @@
+define([
+  'angular',
+  'config',
+],
+function (angular, config) {
+  'use strict';
+
+  var module = angular.module('grafana.controllers');
+
+  module.controller('InvitedCtrl', function($scope, $routeParams, contextSrv, backendSrv) {
+
+    contextSrv.sidemenu = false;
+
+    $scope.formModel = {};
+
+    $scope.init = function() {
+      backendSrv.get('/api/user/invite/' + $routeParams.code).then(function(invite) {
+        $scope.formModel.name = invite.name;
+        $scope.formModel.email = invite.email;
+        $scope.formModel.username = invite.email;
+        $scope.formModel.inviteCode =  $routeParams.code;
+
+        $scope.greeting = invite.name || invite.email || invite.username;
+        $scope.invitedBy = invite.invitedBy;
+      });
+    };
+
+    $scope.submit = function() {
+      if (!$scope.inviteForm.$valid) {
+        return;
+      }
+
+      backendSrv.post('/api/user/invite/complete', $scope.formModel).then(function() {
+        window.location.href = config.appSubUrl + '/';
+      });
+    };
+
+    $scope.init();
+
+  });
+});

+ 11 - 11
public/app/features/admin/partials/edit_user.html

@@ -9,7 +9,7 @@
 <div class="page-container">
 	<div class="page">
 		<h2>
-			User details
+			Edit User
 		</h2>
 
 		<form name="userForm">
@@ -17,7 +17,7 @@
 				<div class="tight-form">
 					<ul class="tight-form-list">
 						<li class="tight-form-item" style="width: 100px">
-							<strong>Name</strong>
+							Name
 						</li>
 						<li>
 							<input type="text" required ng-model="user.name" class="input-xxlarge tight-form-input last" >
@@ -28,7 +28,7 @@
 				<div class="tight-form">
 					<ul class="tight-form-list">
 						<li class="tight-form-item" style="width: 100px">
-							<strong>Email</strong>
+							Email
 						</li>
 						<li>
 							<input type="email" ng-model="user.email" class="input-xxlarge tight-form-input last" >
@@ -39,7 +39,7 @@
 				<div class="tight-form">
 					<ul class="tight-form-list">
 						<li class="tight-form-item" style="width: 100px">
-							<strong>Username</strong>
+							Username
 						</li>
 						<li>
 							<input type="text" ng-model="user.login" class="input-xxlarge tight-form-input last" >
@@ -53,16 +53,16 @@
 			<button type="submit" class="pull-right btn btn-success" ng-click="update()" ng-show="!createMode">Update</button>
 		</form>
 
-		<h2>
+		<h3>
 			Change password
-		</h2>
+		</h3>
 
 		<form name="passwordForm">
 			<div>
 				<div class="tight-form">
 					<ul class="tight-form-list">
 						<li class="tight-form-item" style="width: 100px">
-							<strong>New password</strong>
+							New password
 						</li>
 						<li>
 							<input type="password" required ng-minlength="4" ng-model="password" class="input-xxlarge tight-form-input last">
@@ -76,9 +76,9 @@
 			<button type="submit" class="pull-right btn btn-success" ng-click="setPassword()">Update</button>
 		</form>
 
-		<h2>
+		<h3>
 			Permissions
-		</h2>
+		</h3>
 
 		<div>
 			<div class="tight-form last">
@@ -97,9 +97,9 @@
 			<br>
 		</div>
 
-		<h2>
+		<h3>
 			Organizations
-		</h2>
+		</h3>
 
 		<form name="addOrgForm">
 			<div class="tight-form">

+ 4 - 11
public/app/features/dashboard/unsavedChangesSrv.js

@@ -7,7 +7,7 @@ function(angular, _) {
 
   var module = angular.module('grafana.services');
 
-  module.service('unsavedChangesSrv', function($modal, $q, $location, $timeout, contextSrv, $window) {
+  module.service('unsavedChangesSrv', function($rootScope, $q, $location, $timeout, contextSrv, $window) {
 
     function Tracker(dashboard, scope) {
       var self = this;
@@ -142,17 +142,10 @@ function(angular, _) {
         tracker.scope.$emit('save-dashboard');
       };
 
-      var confirmModal = $modal({
-        template: './app/partials/unsaved-changes.html',
-        modalClass: 'confirm-modal',
-        persist: false,
-        show: false,
+      $rootScope.appEvent('show-modal', {
+        src: './app/partials/unsaved-changes.html',
+        modalClass: 'modal-no-header confirm-modal',
         scope: modalScope,
-        keyboard: false
-      });
-
-      $q.when(confirmModal).then(function(modalEl) {
-        modalEl.modal('show');
       });
     };
 

+ 1 - 0
public/app/features/org/all.js

@@ -3,6 +3,7 @@ define([
   './datasourceEditCtrl',
   './orgUsersCtrl',
   './newOrgCtrl',
+  './userInviteCtrl',
   './orgApiKeysCtrl',
   './orgDetailsCtrl',
 ], function () {});

+ 27 - 3
public/app/features/org/orgUsersCtrl.js

@@ -13,14 +13,21 @@ function (angular) {
       role: 'Viewer',
     };
 
+    $scope.users = [];
+    $scope.pendingInvites = [];
+
     $scope.init = function() {
       $scope.get();
+      $scope.editor = { index: 0 };
     };
 
     $scope.get = function() {
       backendSrv.get('/api/org/users').then(function(users) {
         $scope.users = users;
       });
+      backendSrv.get('/api/org/invites').then(function(pendingInvites) {
+        $scope.pendingInvites = pendingInvites;
+      });
     };
 
     $scope.updateOrgUser = function(user) {
@@ -31,9 +38,26 @@ function (angular) {
       backendSrv.delete('/api/org/users/' + user.userId).then($scope.get);
     };
 
-    $scope.addUser = function() {
-      if (!$scope.form.$valid) { return; }
-      backendSrv.post('/api/org/users', $scope.user).then($scope.get);
+    $scope.revokeInvite = function(invite, evt) {
+      evt.stopPropagation();
+      backendSrv.patch('/api/org/invites/' + invite.code + '/revoke').then($scope.get);
+    };
+
+    $scope.copyInviteToClipboard = function(evt) {
+      evt.stopPropagation();
+    };
+
+    $scope.openInviteModal = function() {
+      var modalScope = $scope.$new();
+      modalScope.invitesSent = function() {
+        $scope.get();
+      };
+
+      $scope.appEvent('show-modal', {
+        src: './app/features/org/partials/invite.html',
+        modalClass: 'modal-no-header invite-modal',
+        scope: modalScope
+      });
     };
 
     $scope.init();

+ 70 - 0
public/app/features/org/partials/invite.html

@@ -0,0 +1,70 @@
+<div class="modal-body" ng-controller="UserInviteCtrl" ng-init="init()">
+
+	<a class="modal-close" ng-click="dismiss();">
+		<i class="fa fa-remove"></i>
+	</a>
+
+	<h3>
+		Invite Users
+	</h3>
+
+	<div class="modal-tagline">
+		Send invite or add existing Grafana users to the organization
+		<span class="highlight-word">{{contextSrv.user.orgName}}</span>
+	</div>
+
+	<br>
+	<br>
+
+	<form name="inviteForm">
+		<div style="display: inline-block">
+			<div>
+				<div class="tight-form" ng-repeat="invite in invites">
+					<ul class="tight-form-list">
+						<li class="tight-form-item">
+							Email or Username
+						</li>
+						<li>
+							<input type="text" ng-model="invite.loginOrEmail" required class="input-large tight-form-input" placeholder="email@test.com">
+						</li>
+						<li class="tight-form-item">
+							Name
+						</li>
+						<li>
+							<input type="text" ng-model="invite.name" class="input-large tight-form-input" placeholder="name (optional)">
+						</li>
+
+						<li class="tight-form-item">
+							Role
+						</li>
+						<li>
+							<select ng-model="invite.role" class="input-small tight-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']">
+							</select>
+						</li>
+						<li class="tight-form-item last" ng-show="$index > 0">
+							<a class="pointer" tabindex="1" ng-click="removeInvite(invite)">
+								<i class="fa fa-remove"></i>
+							</a>
+						</li>
+						<div class="clearfix"></div>
+					</ul>
+				</div>
+			</div>
+
+			<div style="text-align: left; margin-top: 6px;">
+				<a ng-click="addInvite()">+ Invite another</a>
+				<div class="form-inline" style="margin-top: 20px">
+					<editor-checkbox text="Skip sending invite email" model="options.skipEmails" change="targetBlur()"></editor-checkbox>
+				</div>
+			</div>
+
+			<div class="" style="margin-top: 30px; margin-bottom: 20px;">
+				<button type="button" class="btn btn-inverse" ng-click="dismiss()">Cancel</button>
+				<button type="submit" class="btn btn-success" ng-click="sendInvites();">Invite Users</button>
+
+			</div>
+		</div>
+	</form>
+</div>
+
+

+ 58 - 44
public/app/features/org/partials/orgUsers.html

@@ -9,53 +9,67 @@
 
 		<h2>Organization users</h2>
 
-		<form name="form">
-			<div class="tight-form">
-				<ul class="tight-form-list">
-					<li class="tight-form-item" style="width: 127px">
-						<strong>Username or Email</strong>
-					</li>
-					<li>
-						<input type="text" ng-model="user.loginOrEmail" required class="input-xlarge tight-form-input" placeholder="user@email.com or username">
-					</li>
-					<li class="tight-form-item">
-						role
-					</li>
-					<li>
-						<select type="text" ng-model="user.role" class="input-medium tight-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']">
-						</select>
-					</li>
-					<li>
-						<button class="btn btn-success tight-form-btn" ng-click="addUser()">Add</button>
-					</li>
-					<div class="clearfix"></div>
-				</ul>
-			</div>
-		</form>
+		<button class="btn btn-success pull-right" ng-click="openInviteModal()">
+			<i class="fa fa-plus"></i>
+			Add or Invite
+		</button>
 
 		<br>
 
-		<table class="grafana-options-table form-inline">
-			<tr>
-				<th>Login</th>
-				<th>Email</th>
-				<th>Role</th>
-				<th></th>
-			</tr>
-			<tr ng-repeat="user in users">
-				<td>{{user.login}}</td>
-				<td>{{user.email}}</td>
-				<td>
-					<select type="text" ng-model="user.role" class="input-medium" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="updateOrgUser(user)">
-					</select>
-				</td>
-				<td style="width: 1%">
-					<a ng-click="removeUser(user)" class="btn btn-danger btn-mini">
-						<i class="fa fa-remove"></i>
-					</a>
-				</td>
-			</tr>
-		</table>
+		<tabset>
+			<tab heading="Users ({{users.length}})">
+				<table class="grafana-options-table form-inline">
+					<tr>
+						<th>Login</th>
+						<th>Email</th>
+						<th>Role</th>
+						<th></th>
+					</tr>
+					<tr ng-repeat="user in users">
+						<td>{{user.login}}</td>
+						<td>{{user.email}}</td>
+						<td>
+							<select type="text" ng-model="user.role" class="input-medium" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="updateOrgUser(user)">
+							</select>
+						</td>
+						<td style="width: 1%">
+							<a ng-click="removeUser(user)" class="btn btn-danger btn-mini">
+								<i class="fa fa-remove"></i>
+							</a>
+						</td>
+					</tr>
+				</table>
+			</tab>
+			<tab heading="Pending Invitations ({{pendingInvites.length}})">
+				<div class="grafana-list-item" ng-repeat="invite in pendingInvites" ng-click="invite.expanded = !invite.expanded">
+					{{invite.email}}
+					<span ng-show="invite.name" style="padding-left: 20px"> {{invite.name}}</span>
+					<span class="pull-right">
+						<button class="btn btn-inverse btn-mini " data-clipboard-text="{{invite.url}}" clipboard-button ng-click="copyInviteToClipboard($event)"
+							<i class="fa fa-clipboard"></i> Copy Invite
+						</button>
+						&nbsp;
+						<a class="pointer">
+							<i ng-show="!invite.expanded" class="fa fa-caret-right"></i>
+							<i ng-show="invite.expanded" class="fa fa-caret-down"></i>
+						</a>
+					</span>
+					<div ng-show="invite.expanded">
+						<a href="{{invite.url}}">{{invite.url}}</a><br>
+						<button class="btn btn-inverse btn-mini">
+							<i class="fa fa-envelope-o"></i> Resend invite
+						</button>
+						&nbsp;
+						<button class="btn btn-inverse btn-mini" ng-click="revokeInvite(invite, $event)">
+							<i class="fa fa-remove" style="color: red"></i> Revoke invite
+						</button>
+						<span style="padding-left: 15px">
+							Invited: <em> {{invite.createdOn | date: 'shortDate'}} by {{invite.invitedBy}} </em>
+						</span>
+					<div>
+				</div>
+			</tab>
+		</tabset>
 
 	</div>
 </div>

+ 48 - 0
public/app/features/org/userInviteCtrl.js

@@ -0,0 +1,48 @@
+define([
+  'angular',
+  'lodash',
+],
+function (angular, _) {
+  'use strict';
+
+  var module = angular.module('grafana.controllers');
+
+  module.controller('UserInviteCtrl', function($scope, backendSrv) {
+
+    $scope.invites = [
+      {name: '', email: '', role: 'Editor'},
+    ];
+
+    $scope.options = {skipEmails: false};
+    $scope.init = function() { };
+
+    $scope.addInvite = function() {
+      $scope.invites.push({name: '', email: '', role: 'Editor'});
+    };
+
+    $scope.removeInvite = function(invite) {
+      $scope.invites = _.without($scope.invites, invite);
+    };
+
+    $scope.sendInvites = function() {
+      if (!$scope.inviteForm.$valid) { return; }
+      $scope.sendSingleInvite(0);
+    };
+
+    $scope.sendSingleInvite = function(index) {
+      var invite = $scope.invites[index];
+      invite.skipEmails = $scope.options.skipEmails;
+
+      return backendSrv.post('/api/org/invites', invite).finally(function() {
+        index += 1;
+
+        if (index === $scope.invites.length) {
+          $scope.invitesSent();
+          $scope.dismiss();
+        } else {
+          $scope.sendSingleInvite(index);
+        }
+      });
+    };
+  });
+});

+ 2 - 2
public/app/features/profile/partials/profile.html

@@ -8,7 +8,7 @@
 <div class="page-container">
 	<div class="page">
 
-		<h2>Profile details</h2>
+		<h2>Profile</h2>
 
 		<form name="userForm">
 			<div>
@@ -64,7 +64,7 @@
 			<button type="submit" class="pull-right btn btn-success" ng-click="update()">Update</button>
 		</form>
 
-		<h2>Organizations</h2>
+		<h3>Organizations</h3>
 
 		<table class="grafana-options-table">
 			<tr ng-repeat="org in orgs">

+ 3 - 0
public/app/partials/bootstrap/tab.html

@@ -0,0 +1,3 @@
+<li ng-class="{active: active, disabled: disabled}">
+  <a href ng-click="select()" tab-heading-transclude>{{heading}}</a>
+</li>

+ 10 - 0
public/app/partials/bootstrap/tabset.html

@@ -0,0 +1,10 @@
+<div>
+  <ul class="nav nav-{{type || 'tabs'}} nav-tabs-alt" ng-class="{'nav-stacked': vertical, 'nav-justified': justified}" ng-transclude></ul>
+  <div class="tab-content">
+    <div class="tab-pane"
+         ng-repeat="tab in tabs"
+         ng-class="{active: tab.active}"
+         tab-content-transclude="tab">
+    </div>
+  </div>
+</div>

+ 1 - 1
public/app/partials/confirm_modal.html

@@ -12,7 +12,7 @@
 		{{title}}
 	</div>
 
-	<div class="confirm-modal-text">
+	<div class="modal-tagline">
 		{{text}}
 	</div>
 

+ 2 - 0
public/app/partials/login.html

@@ -1,4 +1,6 @@
 <div class="container">
+	<div class="login-page-background">
+	</div>
 
 	<div class="login-box">
 

+ 95 - 0
public/app/partials/signup_invited.html

@@ -0,0 +1,95 @@
+<div class="container">
+
+	<div class="login-page-background">
+	</div>
+
+	<div class="login-box">
+
+		<div class="login-box-logo">
+			<img src="img/logo_transparent_200x75.png">
+		</div>
+
+    <div class="invite-box">
+			<h3>
+				Hello {{greeting}}.
+			</h3>
+
+			<div class="modal-tagline">
+				<em>{{invitedBy}}</em> has invited you to join Grafana and the organization <span class="highlight-word">{{contextSrv.user.orgName}}</span></br>Please complete the following to accept your invitation and continue:
+			</div>
+
+      <form name="inviteForm" class="login-form">
+				<div class="tight-from-container">
+					<div class="tight-form">
+						<ul class="tight-form-list">
+							<li class="tight-form-item" style="width: 128px">
+								Email
+							</li>
+							<li>
+								<input type="email" name="email" class="tight-form-input last" required ng-model='formModel.email' placeholder="Email" style="width: 253px">
+							</li>
+						</ul>
+						<div class="clearfix"></div>
+					</div>
+					<div class="tight-form">
+						<ul class="tight-form-list">
+							<li class="tight-form-item" style="width: 128px">
+								Name
+							</li>
+							<li>
+								<input type="text" name="name" class="tight-form-input last" ng-model='formModel.name' placeholder="Name (optional)" style="width: 253px">
+							</li>
+						</ul>
+						<div class="clearfix"></div>
+					</div>
+					<div class="tight-form">
+						<ul class="tight-form-list">
+							<li class="tight-form-item" style="width: 128px">
+								Username
+							</li>
+							<li>
+								<input type="text" name="username" class="tight-form-input last" required ng-model='formModel.username' placeholder="Username" style="width: 253px">
+							</li>
+						</ul>
+						<div class="clearfix"></div>
+					</div>
+
+					<div class="tight-form">
+						<ul class="tight-form-list">
+							<li class="tight-form-item" style="width: 128px">
+								Password
+							</li>
+							<li>
+								<input type="password" name="password" class="tight-form-input last" required ng-model="formModel.password" id="inputPassword" style="width: 253px" placeholder="password">
+							</li>
+						</ul>
+						<div class="clearfix"></div>
+					</div>
+				</div>
+
+				<div style="margin-left: 147px; width: 254px;">
+					<password-strength password="formModel.password"></password-strength>
+				</div>
+
+				<div class="login-submit-button-row">
+					<button type="submit" class="btn" ng-click="submit();" ng-class="{'btn-inverse': !inviteForm.$valid, 'btn-primary': inviteForm.$valid}">
+						Continue
+					</button>
+				</div>
+			</form>
+
+			<div class="clearfix"></div>
+		</div>
+
+		<div class="row" style="margin-top: 50px">
+			<div class="version-footer text-center small">
+				Grafana version: {{buildInfo.version}}, commit: {{buildInfo.commit}},
+				build date: {{buildInfo.buildstamp | date: 'yyyy-MM-dd HH:mm:ss' }}
+			</div>
+		</div>
+
+	</div>
+
+</div>
+
+

+ 4 - 0
public/app/routes/all.js

@@ -97,6 +97,10 @@ define([
         templateUrl: 'app/partials/login.html',
         controller : 'LoginCtrl',
       })
+      .when('/invite/:code', {
+        templateUrl: 'app/partials/signup_invited.html',
+        controller : 'InvitedCtrl',
+      })
       .when('/user/password/send-reset-email', {
         templateUrl: 'app/partials/reset_password.html',
         controller : 'ResetPasswordCtrl',

+ 1 - 1
public/app/services/alertSrv.js

@@ -71,7 +71,7 @@ function (angular, _) {
       var confirmModal = $modal({
         template: './app/partials/confirm_modal.html',
         persist: false,
-        modalClass: 'confirm-modal',
+        modalClass: 'modal-no-header confirm-modal',
         show: false,
         scope: scope,
         keyboard: false

+ 1 - 0
public/app/services/utilSrv.js

@@ -14,6 +14,7 @@ function (angular) {
 
     this.showModal = function(e, options) {
       var modal = $modal({
+        modalClass: options.modalClass,
         template: options.src,
         persist: false,
         show: false,

+ 1 - 2
public/css/less/bootswatch.dark.less

@@ -220,6 +220,7 @@ div.subnav {
 
 	li > a:hover,
 	li.active > a,
+	li.active > a:focus,
 	li.active > a:hover {
 	  border-color: transparent;
 	  background-color: transparent;
@@ -547,8 +548,6 @@ a:hover {
 }
 
 .modal {
- 	.border-radius(1px);
-	border-top: solid 1px lighten(@grayDark, 5%);
 	background-color: @grafanaPanelBackground;
 }
 

+ 1 - 13
public/css/less/bootswatch.light.less

@@ -159,6 +159,7 @@ div.subnav {
 
 	li > a:hover,
 	li.active > a,
+	li.active > a:focus,
 	li.active > a:hover {
 		border-color: transparent;
 	  background-color: transparent;
@@ -531,19 +532,6 @@ a.thumbnail {
 	.border-radius(0);
 }
 
-.modal {
-	.border-radius(0);
-	background-color: @bodyBackground;
-	&-header {
-		border-bottom: none;
-	}
-
-	&-footer {
-		border-top: none;
-		background-color: transparent;
-	}
-}
-
 .popover {
 	.border-radius(0);
 

+ 0 - 6
public/css/less/gfbox.less

@@ -67,12 +67,6 @@
   position: relative;
   border: @grafanaPanelBorder;
   padding: 20px 20px 60px 49px;
-
-  h2 {
-    color: @textColor;
-    font-weight: normal;
-    font-size: 22px;
-  }
 }
 
 .page {

+ 37 - 7
public/css/less/grafana.less

@@ -1,3 +1,4 @@
+@import "type.less";
 @import "login.less";
 @import "submenu.less";
 @import "graph.less";
@@ -15,6 +16,7 @@
 @import "admin.less";
 @import "validation.less";
 @import "fonts.less";
+@import "tabs.less";
 
 .row-control-inner {
   padding:0px;
@@ -257,18 +259,28 @@
   td:first-child { text-align: right; }
 }
 
-.confirm-modal {
+.modal-no-header {
   border: 1px solid @grafanaTargetFuncBackground;
-  max-width: 500px;
-  background-color: @grafanaPanelBackground;
   text-align: center;
 
+  h3 {
+    margin-top: 30px;
+  }
+
   .modal-close {
     float: right;
     font-size: 140%;
     padding: 10px;
   }
 
+  .modal-tagline {
+    font-size: 16px;
+  }
+}
+
+.confirm-modal {
+  max-width: 500px;
+
   .confirm-modal-icon {
     padding-top: 41px;
     font-size: 280%;
@@ -282,10 +294,6 @@
     margin-bottom: 15px;
   }
 
-  .confirm-modal-text {
-    font-size: 16px;
-  }
-
   .confirm-modal-buttons {
     margin-top: 35px;
     margin-bottom: 35px;
@@ -354,5 +362,27 @@
   color: @orange;
 }
 
+.highlight-word {
+  color: @orange;
+}
 
+.body-copy-emphasis {
+  color: @headingsColor;
+}
 
+.signup-page-container {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  height: 100%;
+  width: 100%;
+  background-image: url(/img/background_tease.jpg);
+
+  .signup-logo-container {
+    width: 150px;
+    margin: 0 auto;
+    padding: 80px 0;
+  }
+}

+ 42 - 0
public/css/less/login.less

@@ -93,4 +93,46 @@
   }
 }
 
+.login-page-background {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  height: 100%;
+  width: 100%;
+  background-image: url(/img/background_tease.jpg);
+  opacity: 0.05;
+  z-index: -1;
+}
+
+.invite-box {
+  text-align: center;
+  border: 1px solid @grafanaTargetFuncBackground;
+	background-color: @grafanaPanelBackground;
+	position: fixed;
+  max-width: 800px;
+  left: 0;
+  right: 0;
+  margin-left: auto;
+  margin-right: auto;
+  top: 20%;
 
+  .tight-form {
+    text-align: left;
+  }
+
+  h3 {
+    margin-top: 30px;
+  }
+
+  .modal-close {
+    float: right;
+    font-size: 140%;
+    padding: 10px;
+  }
+
+  .modal-tagline {
+    font-size: 16px;
+  }
+}

+ 7 - 19
public/css/less/tables_lists.less

@@ -33,23 +33,11 @@
   white-space: nowrap;
 }
 
-.grafana-options-list {
-  list-style: none;
-  margin: 0;
-  max-width: 450px;
-
-  li:nth-child(odd) {
-    background-color: @grafanaListAccent;
-  }
-
-  li {
-    float: left;
-    margin: 2px;
-    padding: 5px 10px;
-    border: 1px solid @grafanaListBorderBottom;
-    border: 1px solid @grafanaListBorderBottom;
-  }
-  li:first-child {
-    border: 1px solid @grafanaListBorderBottom;
-  }
+.grafana-list-item {
+  display: block;
+  padding: 1px 10px;
+  line-height: 34px;
+  background-color: @grafanaTargetBackground;
+  margin-bottom: 4px;
+  cursor: pointer;
 }

+ 33 - 0
public/css/less/tabs.less

@@ -0,0 +1,33 @@
+
+.nav-tabs-alt {
+  border-bottom: @grafanaTriggerBorder;
+  padding-left: 10px;
+
+	& > li > a {
+	  color: darken(@linkColor, 20%);
+	}
+
+	li > a:hover {
+    border-bottom: none;
+	}
+
+	li.active > a,
+	li.active > a:focus,
+	li.active > a:hover {
+    .border-radius(3px);
+		border: @grafanaTriggerBorder;
+	  background-color: transparent;
+	  border-bottom: 1px solid @grafanaPanelBackground;
+	  color: @linkColor;
+	}
+
+	li.disabled > a {
+		color: @textColor;
+	}
+
+	.open .dropdown-toggle {
+		background-color: #060606;
+		border-color: transparent;
+	}
+}
+

+ 1 - 1
public/css/less/tightform.less

@@ -165,7 +165,7 @@ select.tight-form-input {
   margin: 0px;
   border-radius: 0;
   height: 36px;
-  padding: 8px 3px;
+  padding: 9px 3px;
   &.last {
     border-right: none;
   }

+ 247 - 0
public/css/less/type.less

@@ -0,0 +1,247 @@
+//
+// Typography
+// --------------------------------------------------
+
+
+// Body text
+// -------------------------
+
+p {
+  margin: 0 0 @baseLineHeight / 2;
+}
+.lead {
+  margin-bottom: @baseLineHeight;
+  font-size: @baseFontSize * 1.5;
+  font-weight: 200;
+  line-height: @baseLineHeight * 1.5;
+}
+
+
+// Emphasis & misc
+// -------------------------
+
+// Ex: 14px base font * 85% = about 12px
+small   { font-size: 85%; }
+
+strong  { font-weight: 500; }
+em      { font-style: italic; color: @headingsColor; }
+cite    { font-style: normal; }
+
+// Utility classes
+.muted               { color: @grayLight; }
+a.muted:hover,
+a.muted:focus        { color: darken(@grayLight, 10%); }
+
+.text-warning        { color: @warningText; }
+a.text-warning:hover,
+a.text-warning:focus { color: darken(@warningText, 10%); }
+
+.text-error          { color: @errorText; }
+a.text-error:hover,
+a.text-error:focus   { color: darken(@errorText, 10%); }
+
+.text-info           { color: @infoText; }
+a.text-info:hover,
+a.text-info:focus    { color: darken(@infoText, 10%); }
+
+.text-success        { color: @successText; }
+a.text-success:hover,
+a.text-success:focus { color: darken(@successText, 10%); }
+
+.text-left           { text-align: left; }
+.text-right          { text-align: right; }
+.text-center         { text-align: center; }
+
+
+// Headings
+// -------------------------
+
+h1, h2, h3, h4, h5, h6 {
+  margin: (@baseLineHeight / 2) 0;
+  font-family: @headingsFontFamily;
+  font-weight: @headingsFontWeight;
+  line-height: @baseLineHeight;
+  color: @headingsColor;
+  text-rendering: optimizelegibility; // Fix the character spacing for headings
+  small {
+    font-weight: normal;
+    line-height: 1;
+    color: @grayLight;
+  }
+}
+
+h1,
+h2,
+h3 { line-height: @baseLineHeight * 2; }
+
+h1 { font-size: @baseFontSize * 2.00; } // ~38px
+h2 { font-size: @baseFontSize * 1.75; } // ~32px
+h3 { font-size: @baseFontSize * 1.50; } // ~24px
+h4 { font-size: @baseFontSize * 1.25; } // ~18px
+h5 { font-size: @baseFontSize; }
+h6 { font-size: @baseFontSize * 0.85; } // ~12px
+
+h1 small { font-size: @baseFontSize * 1.75; } // ~24px
+h2 small { font-size: @baseFontSize * 1.25; } // ~18px
+h3 small { font-size: @baseFontSize; }
+h4 small { font-size: @baseFontSize; }
+
+
+// Page header
+// -------------------------
+
+.page-header {
+  padding-bottom: (@baseLineHeight / 2) - 1;
+  margin: @baseLineHeight 0 (@baseLineHeight * 1.5);
+  border-bottom: 1px solid @grayLighter;
+}
+
+
+
+// Lists
+// --------------------------------------------------
+
+// Unordered and Ordered lists
+ul, ol {
+  padding: 0;
+  margin: 0 0 @baseLineHeight / 2 25px;
+}
+ul ul,
+ul ol,
+ol ol,
+ol ul {
+  margin-bottom: 0;
+}
+li {
+  line-height: @baseLineHeight;
+}
+
+// Remove default list styles
+ul.unstyled,
+ol.unstyled {
+  margin-left: 0;
+  list-style: none;
+}
+
+// Single-line list items
+ul.inline,
+ol.inline {
+  margin-left: 0;
+  list-style: none;
+  > li {
+    display: inline-block;
+    .ie7-inline-block();
+    padding-left: 5px;
+    padding-right: 5px;
+  }
+}
+
+// Description Lists
+dl {
+  margin-bottom: @baseLineHeight;
+}
+dt,
+dd {
+  line-height: @baseLineHeight;
+}
+dt {
+  font-weight: bold;
+}
+dd {
+  margin-left: @baseLineHeight / 2;
+}
+// Horizontal layout (like forms)
+.dl-horizontal {
+  .clearfix(); // Ensure dl clears floats if empty dd elements present
+  dt {
+    float: left;
+    width: @horizontalComponentOffset - 20;
+    clear: left;
+    text-align: right;
+    .text-overflow();
+  }
+  dd {
+    margin-left: @horizontalComponentOffset;
+  }
+}
+
+// MISC
+// ----
+
+// Horizontal rules
+hr {
+  margin: @baseLineHeight 0;
+  border: 0;
+  border-top: 1px solid @hrBorder;
+  border-bottom: 1px solid @white;
+}
+
+// Abbreviations and acronyms
+abbr[title],
+// Added data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257
+abbr[data-original-title] {
+  cursor: help;
+  border-bottom: 1px dotted @grayLight;
+}
+abbr.initialism {
+  font-size: 90%;
+  text-transform: uppercase;
+}
+
+// Blockquotes
+blockquote {
+  padding: 0 0 0 15px;
+  margin: 0 0 @baseLineHeight;
+  border-left: 5px solid @grayLighter;
+  p {
+    margin-bottom: 0;
+    font-size: @baseFontSize * 1.25;
+    font-weight: 300;
+    line-height: 1.25;
+  }
+  small {
+    display: block;
+    line-height: @baseLineHeight;
+    color: @grayLight;
+    &:before {
+      content: '\2014 \00A0';
+    }
+  }
+
+  // Float right with text-align: right
+  &.pull-right {
+    float: right;
+    padding-right: 15px;
+    padding-left: 0;
+    border-right: 5px solid @grayLighter;
+    border-left: 0;
+    p,
+    small {
+      text-align: right;
+    }
+    small {
+      &:before {
+        content: '';
+      }
+      &:after {
+        content: '\00A0 \2014';
+      }
+    }
+  }
+}
+
+// Quotes
+q:before,
+q:after,
+blockquote:before,
+blockquote:after {
+  content: "";
+}
+
+// Addresses
+address {
+  display: block;
+  margin-bottom: @baseLineHeight;
+  font-style: normal;
+  line-height: @baseLineHeight;
+}

+ 4 - 2
public/css/less/variables.dark.less

@@ -69,8 +69,8 @@
 @altFontFamily:         @serifFontFamily;
 
 @headingsFontFamily:    inherit; // empty to use BS default, @baseFontFamily
-@headingsFontWeight:    bold;    // instead of browser default, bold
-@headingsColor:         @textColor; // empty to use BS default, @textColor
+@headingsFontWeight:    normal;    // instead of browser default, bold
+@headingsColor:         darken(@white,11%); // empty to use BS default, @textColor
 @inputText:             @black;
 
 
@@ -98,6 +98,8 @@
 @grafanaListHighlight:      #333;
 @grafanaListMainLinkColor: @textColor;
 
+@pageContainerBorderColor: @grayDark;
+
 // Scrollbars
 @scrollbarBackground: #3a3a3a;
 @scrollbarBackground2: #3a3a3a;

+ 9 - 10
public/css/less/variables.light.less

@@ -33,9 +33,9 @@
 
 // grafana Variables
 // -------------------------
-@grafanaPanelBackground: 	@white;
-@grafanaPanelBorder: 		  solid 1px #ddd;
-@grafanaTriggerBorder:		solid 1px @grayLighter;
+@grafanaPanelBackground: 	@grayLighter;
+@grafanaPanelBorder: 	  	solid 1px #ddd;
+@grafanaTriggerBorder:		solid 1px @grayLight;
 
 // Submenu
 @submenuBackground:     rgb(218, 217, 217);
@@ -58,16 +58,14 @@
 
 // Scaffolding
 // -------------------------
-@bodyBackground:        #EAEAEA;
+@bodyBackground:        #EFEFEF;
 @textColor:             @gray;
 
-
 // Links
 // -------------------------
 @linkColor:             @gray;
 @linkColorDisabled:     lighten(@linkColor,30%);
-@linkColorHover:        @grayDarker;
-
+@linkColorHover:        darken(@linkColor, 20%);
 
 // Typography
 // -------------------------
@@ -76,14 +74,14 @@
 @monoFontFamily:        Menlo, Monaco, Consolas, "Courier New", monospace;
 
 @baseFontSize:          14px;
-@baseFontWeight:		400;
+@baseFontWeight:		    400;
 @baseFontFamily:        @sansFontFamily;
 @baseLineHeight:        20px;
 @altFontFamily:         @serifFontFamily;
 
 @headingsFontFamily:    inherit; // empty to use BS default, @baseFontFamily
-@headingsFontWeight:    bold;    // instead of browser default, bold
-@headingsColor:         @grayDarker; // empty to use BS default, @textColor
+@headingsFontWeight:    normal;    // instead of browser default, bold
+@headingsColor:         @textColor; // empty to use BS default, @textColor
 
 
 // Component sizing
@@ -111,6 +109,7 @@
 @grafanaListHighlightContrast: #ddd;
 @grafanaListMainLinkColor:     @textColor;
 
+@pageContainerBorderColor: darken(@grafanaTargetBackground, 5%);
 
 
 // Tables

+ 2 - 0
public/emails/README.md

@@ -0,0 +1,2 @@
+
+html files in this folder are generated from templates and build system in repo_root/emails

+ 198 - 0
public/emails/invited_to_org.html

@@ -0,0 +1,198 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml">
+<head>
+	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+	<meta name="viewport" content="width=device-width" />
+   
+</head>
+<body style="-ms-text-size-adjust: 100%; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; line-height: 19px; margin: 0; min-width: 100%; padding: 0; text-align: left; width: 100% !important"><style type="text/css">
+body {
+width: 100% !important; min-width: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0;
+}
+img {
+outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block;
+}
+body {
+color: #222222; font-family: "Helvetica", "Arial", sans-serif; font-weight: normal; padding: 0; margin: 0; text-align: left; line-height: 1.3;
+}
+body {
+font-size: 14px; line-height: 19px;
+}
+a:hover {
+color: #2795b6 !important;
+}
+a:active {
+color: #2795b6 !important;
+}
+a:visited {
+color: #2ba6cb !important;
+}
+table.button:hover td {
+background: #2795b6 !important;
+}
+table.button:visited td {
+background: #2795b6 !important;
+}
+table.button:active td {
+background: #2795b6 !important;
+}
+table.button:hover td a {
+color: #fff !important;
+}
+table.button:visited td a {
+color: #fff !important;
+}
+table.button:active td a {
+color: #fff !important;
+}
+table.button:hover td {
+background: #2795b6 !important;
+}
+table.button:hover td a {
+color: #ffffff !important;
+}
+table.button:active td a {
+color: #ffffff !important;
+}
+table.button td a:visited {
+color: #ffffff !important;
+}
+body {
+font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;
+}
+@media only screen and (max-width: 600px) {
+  table[class="body"] img {
+    width: auto !important; height: auto !important;
+  }
+  table[class="body"] center {
+    min-width: 0 !important;
+  }
+  table[class="body"] .container {
+    width: 95% !important;
+  }
+  table[class="body"] .row {
+    width: 100% !important; display: block !important;
+  }
+  table[class="body"] .wrapper {
+    display: block !important; padding-right: 0 !important;
+  }
+  table[class="body"] .columns {
+    table-layout: fixed !important; float: none !important; width: 100% !important; padding-right: 0px !important; padding-left: 0px !important; display: block !important;
+  }
+  table[class="body"] table.columns td {
+    width: 100% !important;
+  }
+  table[class="body"] .columns td.six {
+    width: 50% !important;
+  }
+  table[class="body"] table.columns td.expander {
+    width: 1px !important;
+  }
+}
+</style>
+	<table class="body" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border-collapse: collapse; border-spacing: 0; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; height: 100%; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; width: 100%">
+		<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+			<td class="center" align="center" valign="top" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: center; vertical-align: top; word-break: break-word">
+        <center style="min-width: 580px; width: 100%">
+
+          <table class="row header" style="background: #333; border-collapse: collapse; border-spacing: 0; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%" bgcolor="#333">
+            <tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+              <td class="center" align="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: center; vertical-align: top; word-break: break-word" valign="top">
+                <center style="min-width: 580px; width: 100%">
+
+                  <table class="container" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: inherit; vertical-align: top; width: 580px">
+                    <tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+                      <td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+
+                        <table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+                          <tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+                            <td class="six sub-columns center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; min-width: 0px; padding: 0px 10px 10px 0px; text-align: center; vertical-align: top; width: 50%; word-break: break-word" align="center" valign="top">
+															<img src="http://docs.grafana.org/img/logo_transparent_200x75.png" style="-ms-interpolation-mode: bicubic; clear: both; display: inline; float: none; max-width: 100%; outline: none; text-decoration: none; width: 150px" align="none" />
+                            </td>
+														<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+                          </tr>
+                        </table>
+
+                      </td>
+                    </tr>
+                  </table>
+
+                </center>
+              </td>
+            </tr>
+          </table>
+
+					<table class="container" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: inherit; vertical-align: top; width: 580px">
+						<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+							<td style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+								
+
+{{Subject .Subject "{{.InvitedBy}} has added you to the Grafana organization {{.OrgName}}"}}
+
+<table class="row" style="border-collapse: collapse; border-spacing: 0; display: block; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
+	<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+		<td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+
+			<table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+				<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+					<td style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+						<h3 style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 32px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left; word-break: normal" align="left">You have been added to the Grafana organization {{.OrgName}}</h3>
+					</td>
+					<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+				</tr>
+			</table>
+
+		</td>
+	</tr>
+</table>
+
+<table class="row" style="border-collapse: collapse; border-spacing: 0; display: block; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
+	<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+		<td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+			<table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+				<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+					<td class="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: center; vertical-align: top; word-break: break-word" align="center" valign="top">
+						<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; line-height: 19px; margin: 0 0 10px; padding: 0; text-align: left" align="left">You can switch organization in the left side menu, in the dropdown below your username.</p>
+					</td>
+					<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+				</tr>
+			</table>
+
+		</td>
+	</tr>
+</table>
+
+
+
+								
+								<table class="row footer" style="border-collapse: collapse; border-spacing: 0; display: block; margin-top: 20px; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
+									<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+										<td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+											<table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+												<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+													<td align="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: left; vertical-align: top; word-break: break-word" valign="top">
+														<center style="min-width: 580px; width: 100%">
+															<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; line-height: 19px; margin: 0 0 10px; padding: 0; text-align: center" align="center">
+																Sent by <a href="{{.AppUrl}}" style="color: #2ba6cb; text-decoration: none">Grafana v{{.BuildVersion}}</a>
+															</p>
+														</center>
+													</td>
+													<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+												</tr>
+											</table>
+										</td>
+									</tr>
+								</table>
+
+								
+							</td>
+						</tr>
+
+					</table>
+				</center>
+			</td>
+		</tr>
+
+	</table>
+</body>
+</html>

+ 204 - 0
public/emails/new_user_invite.html

@@ -0,0 +1,204 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml">
+<head>
+	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+	<meta name="viewport" content="width=device-width" />
+   
+</head>
+<body style="-ms-text-size-adjust: 100%; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; line-height: 19px; margin: 0; min-width: 100%; padding: 0; text-align: left; width: 100% !important"><style type="text/css">
+body {
+width: 100% !important; min-width: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0;
+}
+img {
+outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block;
+}
+body {
+color: #222222; font-family: "Helvetica", "Arial", sans-serif; font-weight: normal; padding: 0; margin: 0; text-align: left; line-height: 1.3;
+}
+body {
+font-size: 14px; line-height: 19px;
+}
+a:hover {
+color: #2795b6 !important;
+}
+a:active {
+color: #2795b6 !important;
+}
+a:visited {
+color: #2ba6cb !important;
+}
+table.button:hover td {
+background: #2795b6 !important;
+}
+table.button:visited td {
+background: #2795b6 !important;
+}
+table.button:active td {
+background: #2795b6 !important;
+}
+table.button:hover td a {
+color: #fff !important;
+}
+table.button:visited td a {
+color: #fff !important;
+}
+table.button:active td a {
+color: #fff !important;
+}
+table.button:hover td {
+background: #2795b6 !important;
+}
+table.button:hover td a {
+color: #ffffff !important;
+}
+table.button:active td a {
+color: #ffffff !important;
+}
+table.button td a:visited {
+color: #ffffff !important;
+}
+body {
+font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;
+}
+@media only screen and (max-width: 600px) {
+  table[class="body"] img {
+    width: auto !important; height: auto !important;
+  }
+  table[class="body"] center {
+    min-width: 0 !important;
+  }
+  table[class="body"] .container {
+    width: 95% !important;
+  }
+  table[class="body"] .row {
+    width: 100% !important; display: block !important;
+  }
+  table[class="body"] .wrapper {
+    display: block !important; padding-right: 0 !important;
+  }
+  table[class="body"] .columns {
+    table-layout: fixed !important; float: none !important; width: 100% !important; padding-right: 0px !important; padding-left: 0px !important; display: block !important;
+  }
+  table[class="body"] table.columns td {
+    width: 100% !important;
+  }
+  table[class="body"] .columns td.six {
+    width: 50% !important;
+  }
+  table[class="body"] table.columns td.expander {
+    width: 1px !important;
+  }
+}
+</style>
+	<table class="body" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border-collapse: collapse; border-spacing: 0; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; height: 100%; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; width: 100%">
+		<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+			<td class="center" align="center" valign="top" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: center; vertical-align: top; word-break: break-word">
+        <center style="min-width: 580px; width: 100%">
+
+          <table class="row header" style="background: #333; border-collapse: collapse; border-spacing: 0; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%" bgcolor="#333">
+            <tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+              <td class="center" align="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: center; vertical-align: top; word-break: break-word" valign="top">
+                <center style="min-width: 580px; width: 100%">
+
+                  <table class="container" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: inherit; vertical-align: top; width: 580px">
+                    <tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+                      <td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+
+                        <table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+                          <tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+                            <td class="six sub-columns center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; min-width: 0px; padding: 0px 10px 10px 0px; text-align: center; vertical-align: top; width: 50%; word-break: break-word" align="center" valign="top">
+															<img src="http://docs.grafana.org/img/logo_transparent_200x75.png" style="-ms-interpolation-mode: bicubic; clear: both; display: inline; float: none; max-width: 100%; outline: none; text-decoration: none; width: 150px" align="none" />
+                            </td>
+														<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+                          </tr>
+                        </table>
+
+                      </td>
+                    </tr>
+                  </table>
+
+                </center>
+              </td>
+            </tr>
+          </table>
+
+					<table class="container" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: inherit; vertical-align: top; width: 580px">
+						<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+							<td style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+								
+
+{{Subject .Subject "{{.InvitedBy}} has invited you to join Grafana"}}
+
+<table class="row" style="border-collapse: collapse; border-spacing: 0; display: block; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
+	<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+		<td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+
+			<table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+				<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+					<td style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+						<h3 style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 32px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left; word-break: normal" align="left">You're invited to sign up to Grafana and join organization {{.OrgName}}</h3>
+					</td>
+					<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+				</tr>
+			</table>
+
+		</td>
+	</tr>
+</table>
+
+<table class="row" style="border-collapse: collapse; border-spacing: 0; display: block; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
+	<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+		<td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+			<table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+				<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+					<td class="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: center; vertical-align: top; word-break: break-word" align="center" valign="top">
+						<table class="button radius" style="border-collapse: collapse; border-spacing: 0; overflow: hidden; padding: 0; text-align: left; vertical-align: top; width: 100%">
+							<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+								<td style="-moz-border-radius: 3px; -moz-hyphens: auto; -webkit-border-radius: 3px; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; background: #2ba6cb; border-collapse: collapse !important; border-radius: 3px; border: 1px solid #2284a1; color: #ffffff; display: block; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 8px 0; text-align: center; vertical-align: top; width: auto !important; word-break: break-word" align="center" bgcolor="#2ba6cb" valign="top">
+									<a href="{{.LinkUrl}}" style="color: #ffffff; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: bold; text-decoration: none">Complete Sign Up</a>
+								</td>
+							</tr>
+						</table>
+					</td>
+					<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+				</tr>
+			</table>
+
+		</td>
+	</tr>
+</table>
+
+
+
+								
+								<table class="row footer" style="border-collapse: collapse; border-spacing: 0; display: block; margin-top: 20px; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
+									<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+										<td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+											<table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+												<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+													<td align="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: left; vertical-align: top; word-break: break-word" valign="top">
+														<center style="min-width: 580px; width: 100%">
+															<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; line-height: 19px; margin: 0 0 10px; padding: 0; text-align: center" align="center">
+																Sent by <a href="{{.AppUrl}}" style="color: #2ba6cb; text-decoration: none">Grafana v{{.BuildVersion}}</a>
+															</p>
+														</center>
+													</td>
+													<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+												</tr>
+											</table>
+										</td>
+									</tr>
+								</table>
+
+								
+							</td>
+						</tr>
+
+					</table>
+				</center>
+			</td>
+		</tr>
+
+	</table>
+</body>
+</html>

+ 193 - 61
public/emails/reset_password.html

@@ -1,71 +1,203 @@
-{{Subject .Subject "Reset your Grafana password - {{.Name}}"}}
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;">
-  <head>
-<!-- If you delete this meta tag, Half Life 3 will never be released. -->
-    <meta name="viewport" content="width=device-width" />
-    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
-    <title></title>
-  </head>
-  <body bgcolor="#FFFFFF" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; margin: 0; padding: 0;"><style type="text/css">
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml">
+<head>
+	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+	<meta name="viewport" content="width=device-width" />
+
+</head>
+<body style="-ms-text-size-adjust: 100%; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; line-height: 19px; margin: 0; min-width: 100%; padding: 0; text-align: left; width: 100% !important"><style type="text/css">
+body {
+width: 100% !important; min-width: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0;
+}
+img {
+outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block;
+}
+body {
+color: #222222; font-family: "Helvetica", "Arial", sans-serif; font-weight: normal; padding: 0; margin: 0; text-align: left; line-height: 1.3;
+}
+body {
+font-size: 14px; line-height: 19px;
+}
+a:hover {
+color: #2795b6 !important;
+}
+a:active {
+color: #2795b6 !important;
+}
+a:visited {
+color: #2ba6cb !important;
+}
+table.button:hover td {
+background: #2795b6 !important;
+}
+table.button:visited td {
+background: #2795b6 !important;
+}
+table.button:active td {
+background: #2795b6 !important;
+}
+table.button:hover td a {
+color: #fff !important;
+}
+table.button:visited td a {
+color: #fff !important;
+}
+table.button:active td a {
+color: #fff !important;
+}
+table.button:hover td {
+background: #2795b6 !important;
+}
+table.button:hover td a {
+color: #ffffff !important;
+}
+table.button:active td a {
+color: #ffffff !important;
+}
+table.button td a:visited {
+color: #ffffff !important;
+}
+body {
+font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;
+}
 @media only screen and (max-width: 600px) {
-  a[class="btn"] {
-    display: block !important; margin-bottom: 10px !important; background-image: none !important; margin-right: 0 !important;
+  table[class="body"] img {
+    width: auto !important; height: auto !important;
+  }
+  table[class="body"] center {
+    min-width: 0 !important;
+  }
+  table[class="body"] .container {
+    width: 95% !important;
+  }
+  table[class="body"] .row {
+    width: 100% !important; display: block !important;
   }
-  div[class="column"] {
-    width: auto !important; float: none !important;
+  table[class="body"] .wrapper {
+    display: block !important; padding-right: 0 !important;
   }
-  table.social div[class="column"] {
-    width: auto !important;
+  table[class="body"] .columns {
+    table-layout: fixed !important; float: none !important; width: 100% !important; padding-right: 0px !important; padding-left: 0px !important; display: block !important;
+  }
+  table[class="body"] table.columns td {
+    width: 100% !important;
+  }
+  table[class="body"] .columns td.six {
+    width: 50% !important;
+  }
+  table[class="body"] table.columns td.expander {
+    width: 1px !important;
   }
 }
 </style>
+	<table class="body" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border-collapse: collapse; border-spacing: 0; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; height: 100%; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; width: 100%">
+		<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+			<td class="center" align="center" valign="top" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: center; vertical-align: top; word-break: break-word">
+        <center style="min-width: 580px; width: 100%">
+
+          <table class="row header" style="background: #333; border-collapse: collapse; border-spacing: 0; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%" bgcolor="#333">
+            <tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+              <td class="center" align="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: center; vertical-align: top; word-break: break-word" valign="top">
+                <center style="min-width: 580px; width: 100%">
+
+                  <table class="container" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: inherit; vertical-align: top; width: 580px">
+                    <tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+                      <td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+
+                        <table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+                          <tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+                            <td class="six sub-columns center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; min-width: 0px; padding: 0px 10px 10px 0px; text-align: center; vertical-align: top; width: 50%; word-break: break-word" align="center" valign="top">
+															<img src="http://docs.grafana.org/img/logo_transparent_200x75.png" style="-ms-interpolation-mode: bicubic; clear: both; display: inline; float: none; max-width: 100%; outline: none; text-decoration: none; width: 150px" align="none" />
+                            </td>
+														<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+                          </tr>
+                        </table>
+
+                      </td>
+                    </tr>
+                  </table>
+
+                </center>
+              </td>
+            </tr>
+          </table>
+
+					<table class="container" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: inherit; vertical-align: top; width: 580px">
+						<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+							<td style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+								{{Subject .Subject "Reset your Grafana password - {{.Name}}"}}
+
+<table class="row" style="border-collapse: collapse; border-spacing: 0; display: block; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
+	<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+		<td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+
+			<table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+				<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+					<td style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+						<h3 style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 32px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left; word-break: normal" align="left">Hi {{.Name}}</h3>
+					</td>
+					<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+				</tr>
+			</table>
+
+		</td>
+	</tr>
+</table>
+
+<table class="row" style="border-collapse: collapse; border-spacing: 0; display: block; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
+	<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+		<td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+			<table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+				<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+					<td class="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: center; vertical-align: top; word-break: break-word" align="center" valign="top">
+						<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; line-height: 19px; margin: 0 0 10px; padding: 0; text-align: left" align="left">
+							Please click the following link to reset your password within <strong>{{.EmailCodeValidHours}} hours</strong>.
+						</p>
+						<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; line-height: 19px; margin: 0 0 10px; padding: 0; text-align: left" align="left">
+							<a href="{{.AppUrl}}user/password/reset?code={{.Code}}" style="color: #2ba6cb; text-decoration: none">{{.AppUrl}}user/password/reset?code={{.Code}}</a>
+						</p>
+						<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; line-height: 19px; margin: 0 0 10px; padding: 0; text-align: left" align="left">Not working? Try copying and pasting it to your browser.</p>
+					</td>
+					<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+				</tr>
+			</table>
+
+		</td>
+	</tr>
+</table>
+
+
+
+
+								<table class="row footer" style="border-collapse: collapse; border-spacing: 0; display: block; margin-top: 20px; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
+									<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+										<td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+											<table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+												<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+													<td align="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: left; vertical-align: top; word-break: break-word" valign="top">
+														<center style="min-width: 580px; width: 100%">
+															<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; line-height: 19px; margin: 0 0 10px; padding: 0; text-align: center" align="center">
+																Sent by <a href="{{.AppUrl}}" style="color: #2ba6cb; text-decoration: none">Grafana v{{.BuildVersion}}</a>
+															</p>
+														</center>
+													</td>
+													<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+												</tr>
+											</table>
+										</td>
+									</tr>
+								</table>
+
+
+							</td>
+						</tr>
+
+					</table>
+				</center>
+			</td>
+		</tr>
 
-<!-- HEADER -->
-<table class="head-wrap" bgcolor="#666666" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; width: 100%; margin: 0; padding: 0;"><tr style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><td style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"></td>
-        <td class="header container" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto; padding: 0;">
-
-                <div class="content" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; max-width: 600px; display: block; margin: 0 auto; padding: 15px;">
-                <table style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; width: 100%; margin: 0; padding: 0;"><tr style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><td align="center" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><img src="http://grafana.org/assets/img/logo_transparent_200x75.png" alt="Grafnaa" style="width: 200px; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; max-width: 100%; margin: 0; padding: 0;" /></td>
-                    </tr></table></div>
-
-        </td>
-        <td style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"></td>
-    </tr></table><!-- /HEADER --><!-- BODY --><table class="body-wrap" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; width: 100%; margin: 0; padding: 0;"><tr style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><td style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"></td>
-        <td class="container" bgcolor="#FFFFFF" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto; padding: 0;">
-
-            <div class="content" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; max-width: 600px; display: block; margin: 0 auto; padding: 15px;">
-            <table style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; width: 100%; margin: 0; padding: 0;"><tr style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;">
-                <td align="left" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;">
-                        <p style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; font-size: 14px; line-height: 1.6; margin: 25px 0 15px; font-weight:600;">Hi {{.Name}},</p>
-
-                        <p style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; font-size: 14px; line-height: 1.6; margin: 0 0 15px;">Please click the following link to reset your password within <b>{{.EmailCodeValidHours}} hours</b>.</p>
-                        <p style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; font-size: 14px; line-height: 1.6; margin: 0 0 15px;">
-                            <a href="{{.AppUrl}}/user/password/reset?code={{.Code}}">{{.AppUrl}}user/password/reset?code={{.Code}}</a>
-                        </p>
-                        <p style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; font-size: 14px; line-height: 1.6; margin: 0 0 15px;"></p>
-
-
-                </td>
-                </tr>
-                <tr style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><td align="center" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 15px;">
-                        <p class="callout" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; color: #999; font-weight: normal; font-size: 14px; line-height: 1.6; margin: 0 0 15px;">
-                            Not working? Try copying and pasting it to your browser. </br>Otherwise, dont hesitate to email us at <a href="mailto:contact@grafana.org">contact@grafana.org</a>
-                        </p><!-- /Callout Panel -->
-                    </td>
-                </tr>
-                </table></div><!-- /content -->
-
-        </td>
-        <td style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"></td>
-    </tr></table><!-- /BODY -->
-    <table style="width: 100%">
-        <tr>
-            <td align="center">
-                <p style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; color: #999; font-weight: normal; font-size: 14px; line-height: 1.6; margin: 0 0 15px; padding: 15px;">© 2015 <a style="color:#13b2d4;text-decoration:none;" target="_blank" href="https://grafana.org">Grafana</a></p>
-            </td>
-        </tr>
-    </table><!-- /FOOTER -->
+	</table>
 </body>
 </html>
 

+ 188 - 60
public/emails/welcome_on_signup.html

@@ -1,69 +1,197 @@
-{{Subject .Subject "Welcome to Grafana"}}
-
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;">
-  <head>
-<!-- If you delete this meta tag, Half Life 3 will never be released. -->
-    <meta name="viewport" content="width=device-width" />
-    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
-    <title>{{.CheckType}} {{ .State }} for {{.Endpoint}}"}}</title>
-  </head>
-  <body bgcolor="#FFFFFF" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; margin: 0; padding: 0;"><style type="text/css">
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml">
+<head>
+	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+	<meta name="viewport" content="width=device-width" />
+
+</head>
+<body style="-ms-text-size-adjust: 100%; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; line-height: 19px; margin: 0; min-width: 100%; padding: 0; text-align: left; width: 100% !important"><style type="text/css">
+body {
+width: 100% !important; min-width: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0;
+}
+img {
+outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block;
+}
+body {
+color: #222222; font-family: "Helvetica", "Arial", sans-serif; font-weight: normal; padding: 0; margin: 0; text-align: left; line-height: 1.3;
+}
+body {
+font-size: 14px; line-height: 19px;
+}
+a:hover {
+color: #2795b6 !important;
+}
+a:active {
+color: #2795b6 !important;
+}
+a:visited {
+color: #2ba6cb !important;
+}
+table.button:hover td {
+background: #2795b6 !important;
+}
+table.button:visited td {
+background: #2795b6 !important;
+}
+table.button:active td {
+background: #2795b6 !important;
+}
+table.button:hover td a {
+color: #fff !important;
+}
+table.button:visited td a {
+color: #fff !important;
+}
+table.button:active td a {
+color: #fff !important;
+}
+table.button:hover td {
+background: #2795b6 !important;
+}
+table.button:hover td a {
+color: #ffffff !important;
+}
+table.button:active td a {
+color: #ffffff !important;
+}
+table.button td a:visited {
+color: #ffffff !important;
+}
+body {
+font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;
+}
 @media only screen and (max-width: 600px) {
-  a[class="btn"] {
-    display: block !important; margin-bottom: 10px !important; background-image: none !important; margin-right: 0 !important;
+  table[class="body"] img {
+    width: auto !important; height: auto !important;
+  }
+  table[class="body"] center {
+    min-width: 0 !important;
+  }
+  table[class="body"] .container {
+    width: 95% !important;
   }
-  div[class="column"] {
-    width: auto !important; float: none !important;
+  table[class="body"] .row {
+    width: 100% !important; display: block !important;
   }
-  table.social div[class="column"] {
-    width: auto !important;
+  table[class="body"] .wrapper {
+    display: block !important; padding-right: 0 !important;
+  }
+  table[class="body"] .columns {
+    table-layout: fixed !important; float: none !important; width: 100% !important; padding-right: 0px !important; padding-left: 0px !important; display: block !important;
+  }
+  table[class="body"] table.columns td {
+    width: 100% !important;
+  }
+  table[class="body"] .columns td.six {
+    width: 50% !important;
+  }
+  table[class="body"] table.columns td.expander {
+    width: 1px !important;
   }
 }
 </style>
+	<table class="body" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border-collapse: collapse; border-spacing: 0; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; height: 100%; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; width: 100%">
+		<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+			<td class="center" align="center" valign="top" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: center; vertical-align: top; word-break: break-word">
+        <center style="min-width: 580px; width: 100%">
+
+          <table class="row header" style="background: #333; border-collapse: collapse; border-spacing: 0; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%" bgcolor="#333">
+            <tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+              <td class="center" align="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: center; vertical-align: top; word-break: break-word" valign="top">
+                <center style="min-width: 580px; width: 100%">
+
+                  <table class="container" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: inherit; vertical-align: top; width: 580px">
+                    <tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+                      <td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+
+                        <table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+                          <tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+                            <td class="six sub-columns center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; min-width: 0px; padding: 0px 10px 10px 0px; text-align: center; vertical-align: top; width: 50%; word-break: break-word" align="center" valign="top">
+															<img src="http://docs.grafana.org/img/logo_transparent_200x75.png" style="-ms-interpolation-mode: bicubic; clear: both; display: inline; float: none; max-width: 100%; outline: none; text-decoration: none; width: 150px" align="none" />
+                            </td>
+														<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+                          </tr>
+                        </table>
+
+                      </td>
+                    </tr>
+                  </table>
+
+                </center>
+              </td>
+            </tr>
+          </table>
+
+					<table class="container" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: inherit; vertical-align: top; width: 580px">
+						<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+							<td style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+								{{Subject .Subject "Welcome to Grafana"}}
+
+<table class="row" style="border-collapse: collapse; border-spacing: 0; display: block; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
+	<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+		<td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+
+			<table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+				<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+					<td style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+						<h3 style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 32px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left; word-break: normal" align="left">Hi {{.Name}}</h3>
+					</td>
+					<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+				</tr>
+			</table>
+
+		</td>
+	</tr>
+</table>
+
+<table class="row" style="border-collapse: collapse; border-spacing: 0; display: block; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
+	<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+		<td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+			<table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+				<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+					<td class="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: center; vertical-align: top; word-break: break-word" align="center" valign="top">
+						<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; line-height: 19px; margin: 0 0 10px; padding: 0; text-align: left" align="left">
+							If you are new to Grafana please read the <a href="http://docs.grafana.org/guides/gettingstarted/" style="color: #2ba6cb; text-decoration: none">Getting Started</a> guide.
+						</p>
+					</td>
+					<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+				</tr>
+			</table>
+		</td>
+	</tr>
+</table>
+
+
+
+
+								<table class="row footer" style="border-collapse: collapse; border-spacing: 0; display: block; margin-top: 20px; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
+									<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+										<td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+											<table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+												<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+													<td align="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: left; vertical-align: top; word-break: break-word" valign="top">
+														<center style="min-width: 580px; width: 100%">
+															<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; line-height: 19px; margin: 0 0 10px; padding: 0; text-align: center" align="center">
+																Sent by <a href="{{.AppUrl}}" style="color: #2ba6cb; text-decoration: none">Grafana v{{.BuildVersion}}</a>
+															</p>
+														</center>
+													</td>
+													<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+												</tr>
+											</table>
+										</td>
+									</tr>
+								</table>
+
+
+							</td>
+						</tr>
+
+					</table>
+				</center>
+			</td>
+		</tr>
 
-<!-- HEADER -->
-<table class="head-wrap" bgcolor="#666666" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; width: 100%; margin: 0; padding: 0;"><tr style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><td style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"></td>
-        <td class="header container" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto; padding: 0;">
-
-                <div class="content" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; max-width: 600px; display: block; margin: 0 auto; padding: 15px;">
-                <table style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; width: 100%; margin: 0; padding: 0;"><tr style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><td align="center" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><img src="http://grafana.org/assets/img/logo_transparent_200x75.png" alt="Grafana" style="width: 200px; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; max-width: 100%; margin: 0; padding: 0;" /></td>
-                    </tr></table></div>
-
-        </td>
-        <td style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"></td>
-    </tr></table><!-- /HEADER --><!-- BODY --><table class="body-wrap" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; width: 100%; margin: 0; padding: 0;"><tr style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><td style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"></td>
-        <td class="container" bgcolor="#FFFFFF" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto; padding: 0;">
-
-            <div class="content" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; max-width: 600px; display: block; margin: 0 auto; padding: 15px;">
-            <table style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; width: 100%; margin: 0; padding: 0;"><tr style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;">
-                <td align="left" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;">
-                        <p style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; font-size: 14px; line-height: 1.6; margin: 25px 0 15px; font-weight:600;">Hi {{.Name}},</p>
-
-                        <p class="callout" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; font-size: 14px; line-height: 1.6; margin: 0 0 15px;">
-                        Thanks for signing up, we're happy to have you.
-                        </br></br>
-                </td>
-                </tr><tr style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><td align="center" style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 25 0;">
-                      <table border="0" cellspacing="0" cellpadding="0">
-                        <tr>
-													<td align="center" style="-webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px;" bgcolor="#13b2d4"><a href="{{.AppUrl}}" target="_blank" style="font-size: 16px; font-family: 'Open Sans', Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; padding: 12px 25px; border: 1px solid #13b2d4; display: inline-block; text-shadow: none;">Log into your Grafana server</a></td>
-                        </tr>
-                      </table>
-                    </td>
-                        <!-- Callout Panel -->
-                </tr>
-                </table></div><!-- /content -->
-
-        </td>
-        <td style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"></td>
-    </tr></table><!-- /BODY -->
-    <table style="width: 100%">
-        <tr>
-            <td align="center">
-                <p style="font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; color: #999; font-weight: normal; font-size: 14px; line-height: 1.6; margin: 0 0 15px; padding: 15px;">© 2015 <a style="color:#13b2d4;text-decoration:none;" target="_blank" href="https://grafana.org">Grafana</a></p>
-            </td>
-        </tr>
-    </table><!-- /FOOTER -->
+	</table>
 </body>
 </html>

BIN
public/img/background_tease.jpg


+ 2 - 0
public/test/test-main.js

@@ -22,6 +22,7 @@ require.config({
     'angular-sanitize':       '../vendor/angular-sanitize/angular-sanitize',
     angularMocks:             '../vendor/angular-mocks/angular-mocks',
     'angular-dragdrop':       '../vendor/angular-native-dragdrop/draganddrop',
+    'angular-ui':             '../vendor/angular-ui/angular-bootstrap',
     'angular-strap':          '../vendor/angular-other/angular-strap',
     timepicker:               '../vendor/angular-other/timepicker',
     datepicker:               '../vendor/angular-other/datepicker',
@@ -83,6 +84,7 @@ require.config({
 
     'angular-route':        ['angular'],
     'angular-sanitize':     ['angular'],
+    'angular-ui':           ['angular'],
     'angular-dragdrop':     ['jquery', 'angular'],
     'angular-mocks':        ['angular'],
     'angular-strap':        ['angular', 'bootstrap','timepicker', 'datepicker'],

+ 6 - 0
public/vendor/angular-ui/angular-bootstrap.js

@@ -0,0 +1,6 @@
+define([
+  'angular',
+  '../vendor/angular-ui/tabs',
+], function() {
+});
+

+ 293 - 0
public/vendor/angular-ui/tabs.js

@@ -0,0 +1,293 @@
+
+/**
+ * @ngdoc overview
+ * @name ui.bootstrap.tabs
+ *
+ * @description
+ * AngularJS version of the tabs directive.
+ */
+
+angular.module('ui.bootstrap.tabs', [])
+
+.controller('TabsetController', ['$scope', function TabsetCtrl($scope) {
+  var ctrl = this,
+      tabs = ctrl.tabs = $scope.tabs = [];
+
+  ctrl.select = function(selectedTab) {
+    angular.forEach(tabs, function(tab) {
+      if (tab.active && tab !== selectedTab) {
+        tab.active = false;
+        tab.onDeselect();
+      }
+    });
+    selectedTab.active = true;
+    selectedTab.onSelect();
+  };
+
+  ctrl.addTab = function addTab(tab) {
+    tabs.push(tab);
+    // we can't run the select function on the first tab
+    // since that would select it twice
+    if (tabs.length === 1 && tab.active !== false) {
+      tab.active = true;
+    } else if (tab.active) {
+      ctrl.select(tab);
+    }
+    else {
+      tab.active = false;
+    }
+  };
+
+  ctrl.removeTab = function removeTab(tab) {
+    var index = tabs.indexOf(tab);
+    //Select a new tab if the tab to be removed is selected and not destroyed
+    if (tab.active && tabs.length > 1 && !destroyed) {
+      //If this is the last tab, select the previous tab. else, the next tab.
+      var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1;
+      ctrl.select(tabs[newActiveIndex]);
+    }
+    tabs.splice(index, 1);
+  };
+
+  var destroyed;
+  $scope.$on('$destroy', function() {
+    destroyed = true;
+  });
+}])
+
+/**
+ * @ngdoc directive
+ * @name ui.bootstrap.tabs.directive:tabset
+ * @restrict EA
+ *
+ * @description
+ * Tabset is the outer container for the tabs directive
+ *
+ * @param {boolean=} vertical Whether or not to use vertical styling for the tabs.
+ * @param {boolean=} justified Whether or not to use justified styling for the tabs.
+ *
+ * @example
+<example module="ui.bootstrap">
+  <file name="index.html">
+    <tabset>
+      <tab heading="Tab 1"><b>First</b> Content!</tab>
+      <tab heading="Tab 2"><i>Second</i> Content!</tab>
+    </tabset>
+    <hr />
+    <tabset vertical="true">
+      <tab heading="Vertical Tab 1"><b>First</b> Vertical Content!</tab>
+      <tab heading="Vertical Tab 2"><i>Second</i> Vertical Content!</tab>
+    </tabset>
+    <tabset justified="true">
+      <tab heading="Justified Tab 1"><b>First</b> Justified Content!</tab>
+      <tab heading="Justified Tab 2"><i>Second</i> Justified Content!</tab>
+    </tabset>
+  </file>
+</example>
+ */
+.directive('tabset', function() {
+  return {
+    restrict: 'EA',
+    transclude: true,
+    replace: true,
+    scope: {
+      type: '@'
+    },
+    controller: 'TabsetController',
+    templateUrl: 'app/partials/bootstrap/tabset.html',
+    link: function(scope, element, attrs) {
+      scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false;
+      scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false;
+    }
+  };
+})
+
+/**
+ * @ngdoc directive
+ * @name ui.bootstrap.tabs.directive:tab
+ * @restrict EA
+ *
+ * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}.
+ * @param {string=} select An expression to evaluate when the tab is selected.
+ * @param {boolean=} active A binding, telling whether or not this tab is selected.
+ * @param {boolean=} disabled A binding, telling whether or not this tab is disabled.
+ *
+ * @description
+ * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}.
+ *
+ * @example
+<example module="ui.bootstrap">
+  <file name="index.html">
+    <div ng-controller="TabsDemoCtrl">
+      <button class="btn btn-small" ng-click="items[0].active = true">
+        Select item 1, using active binding
+      </button>
+      <button class="btn btn-small" ng-click="items[1].disabled = !items[1].disabled">
+        Enable/disable item 2, using disabled binding
+      </button>
+      <br />
+      <tabset>
+        <tab heading="Tab 1">First Tab</tab>
+        <tab select="alertMe()">
+          <tab-heading><i class="icon-bell"></i> Alert me!</tab-heading>
+          Second Tab, with alert callback and html heading!
+        </tab>
+        <tab ng-repeat="item in items"
+          heading="{{item.title}}"
+          disabled="item.disabled"
+          active="item.active">
+          {{item.content}}
+        </tab>
+      </tabset>
+    </div>
+  </file>
+  <file name="script.js">
+    function TabsDemoCtrl($scope) {
+      $scope.items = [
+        { title:"Dynamic Title 1", content:"Dynamic Item 0" },
+        { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true }
+      ];
+
+      $scope.alertMe = function() {
+        setTimeout(function() {
+          alert("You've selected the alert tab!");
+        });
+      };
+    };
+  </file>
+</example>
+ */
+
+/**
+ * @ngdoc directive
+ * @name ui.bootstrap.tabs.directive:tabHeading
+ * @restrict EA
+ *
+ * @description
+ * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element.
+ *
+ * @example
+<example module="ui.bootstrap">
+  <file name="index.html">
+    <tabset>
+      <tab>
+        <tab-heading><b>HTML</b> in my titles?!</tab-heading>
+        And some content, too!
+      </tab>
+      <tab>
+        <tab-heading><i class="icon-heart"></i> Icon heading?!?</tab-heading>
+        That's right.
+      </tab>
+    </tabset>
+  </file>
+</example>
+ */
+.directive('tab', ['$parse', '$log', function($parse, $log) {
+  return {
+    require: '^tabset',
+    restrict: 'EA',
+    replace: true,
+    templateUrl: 'app/partials/bootstrap/tab.html',
+    transclude: true,
+    scope: {
+      active: '=?',
+      heading: '@',
+      onSelect: '&select', //This callback is called in contentHeadingTransclude
+                          //once it inserts the tab's content into the dom
+      onDeselect: '&deselect'
+    },
+    controller: function() {
+      //Empty controller so other directives can require being 'under' a tab
+    },
+    compile: function(elm, attrs, transclude) {
+      return function postLink(scope, elm, attrs, tabsetCtrl) {
+        scope.$watch('active', function(active) {
+          if (active) {
+            tabsetCtrl.select(scope);
+          }
+        });
+
+        scope.disabled = false;
+        if ( attrs.disable ) {
+          scope.$parent.$watch($parse(attrs.disable), function(value) {
+            scope.disabled = !! value;
+          });
+        }
+
+        // Deprecation support of "disabled" parameter
+        // fix(tab): IE9 disabled attr renders grey text on enabled tab #2677
+        // This code is duplicated from the lines above to make it easy to remove once
+        // the feature has been completely deprecated
+        if ( attrs.disabled ) {
+          $log.warn('Use of "disabled" attribute has been deprecated, please use "disable"');
+          scope.$parent.$watch($parse(attrs.disabled), function(value) {
+            scope.disabled = !! value;
+          });
+        }
+
+        scope.select = function() {
+          if ( !scope.disabled ) {
+            scope.active = true;
+          }
+        };
+
+        tabsetCtrl.addTab(scope);
+        scope.$on('$destroy', function() {
+          tabsetCtrl.removeTab(scope);
+        });
+
+        //We need to transclude later, once the content container is ready.
+        //when this link happens, we're inside a tab heading.
+        scope.$transcludeFn = transclude;
+      };
+    }
+  };
+}])
+
+.directive('tabHeadingTransclude', [function() {
+  return {
+    restrict: 'A',
+    require: '^tab',
+    link: function(scope, elm, attrs, tabCtrl) {
+      scope.$watch('headingElement', function updateHeadingElement(heading) {
+        if (heading) {
+          elm.html('');
+          elm.append(heading);
+        }
+      });
+    }
+  };
+}])
+
+.directive('tabContentTransclude', function() {
+  return {
+    restrict: 'A',
+    require: '^tabset',
+    link: function(scope, elm, attrs) {
+      var tab = scope.$eval(attrs.tabContentTransclude);
+
+      //Now our tab is ready to be transcluded: both the tab heading area
+      //and the tab content area are loaded.  Transclude 'em both.
+      tab.$transcludeFn(tab.$parent, function(contents) {
+        angular.forEach(contents, function(node) {
+          if (isTabHeading(node)) {
+            //Let tabHeadingTransclude know.
+            tab.headingElement = node;
+          } else {
+            elm.append(node);
+          }
+        });
+      });
+    }
+  };
+  function isTabHeading(node) {
+    return node.tagName &&  (
+      node.hasAttribute('tab-heading') ||
+      node.hasAttribute('data-tab-heading') ||
+      node.tagName.toLowerCase() === 'tab-heading' ||
+      node.tagName.toLowerCase() === 'data-tab-heading'
+    );
+  }
+})
+
+;

+ 0 - 1
public/vendor/bootstrap/less/bootstrap.less

@@ -22,7 +22,6 @@
 
 // Base CSS
 @import "type.less";
-@import "code.less";
 @import "forms.less";
 @import "tables.less";
 

+ 0 - 246
public/vendor/bootstrap/less/type.less

@@ -1,247 +1 @@
-//
-// Typography
-// --------------------------------------------------
 
-
-// Body text
-// -------------------------
-
-p {
-  margin: 0 0 @baseLineHeight / 2;
-}
-.lead {
-  margin-bottom: @baseLineHeight;
-  font-size: @baseFontSize * 1.5;
-  font-weight: 200;
-  line-height: @baseLineHeight * 1.5;
-}
-
-
-// Emphasis & misc
-// -------------------------
-
-// Ex: 14px base font * 85% = about 12px
-small   { font-size: 85%; }
-
-strong  { font-weight: bold; }
-em      { font-style: italic; }
-cite    { font-style: normal; }
-
-// Utility classes
-.muted               { color: @grayLight; }
-a.muted:hover,
-a.muted:focus        { color: darken(@grayLight, 10%); }
-
-.text-warning        { color: @warningText; }
-a.text-warning:hover,
-a.text-warning:focus { color: darken(@warningText, 10%); }
-
-.text-error          { color: @errorText; }
-a.text-error:hover,
-a.text-error:focus   { color: darken(@errorText, 10%); }
-
-.text-info           { color: @infoText; }
-a.text-info:hover,
-a.text-info:focus    { color: darken(@infoText, 10%); }
-
-.text-success        { color: @successText; }
-a.text-success:hover,
-a.text-success:focus { color: darken(@successText, 10%); }
-
-.text-left           { text-align: left; }
-.text-right          { text-align: right; }
-.text-center         { text-align: center; }
-
-
-// Headings
-// -------------------------
-
-h1, h2, h3, h4, h5, h6 {
-  margin: (@baseLineHeight / 2) 0;
-  font-family: @headingsFontFamily;
-  font-weight: @headingsFontWeight;
-  line-height: @baseLineHeight;
-  color: @headingsColor;
-  text-rendering: optimizelegibility; // Fix the character spacing for headings
-  small {
-    font-weight: normal;
-    line-height: 1;
-    color: @grayLight;
-  }
-}
-
-h1,
-h2,
-h3 { line-height: @baseLineHeight * 2; }
-
-h1 { font-size: @baseFontSize * 2.75; } // ~38px
-h2 { font-size: @baseFontSize * 2.25; } // ~32px
-h3 { font-size: @baseFontSize * 1.75; } // ~24px
-h4 { font-size: @baseFontSize * 1.25; } // ~18px
-h5 { font-size: @baseFontSize; }
-h6 { font-size: @baseFontSize * 0.85; } // ~12px
-
-h1 small { font-size: @baseFontSize * 1.75; } // ~24px
-h2 small { font-size: @baseFontSize * 1.25; } // ~18px
-h3 small { font-size: @baseFontSize; }
-h4 small { font-size: @baseFontSize; }
-
-
-// Page header
-// -------------------------
-
-.page-header {
-  padding-bottom: (@baseLineHeight / 2) - 1;
-  margin: @baseLineHeight 0 (@baseLineHeight * 1.5);
-  border-bottom: 1px solid @grayLighter;
-}
-
-
-
-// Lists
-// --------------------------------------------------
-
-// Unordered and Ordered lists
-ul, ol {
-  padding: 0;
-  margin: 0 0 @baseLineHeight / 2 25px;
-}
-ul ul,
-ul ol,
-ol ol,
-ol ul {
-  margin-bottom: 0;
-}
-li {
-  line-height: @baseLineHeight;
-}
-
-// Remove default list styles
-ul.unstyled,
-ol.unstyled {
-  margin-left: 0;
-  list-style: none;
-}
-
-// Single-line list items
-ul.inline,
-ol.inline {
-  margin-left: 0;
-  list-style: none;
-  > li {
-    display: inline-block;
-    .ie7-inline-block();
-    padding-left: 5px;
-    padding-right: 5px;
-  }
-}
-
-// Description Lists
-dl {
-  margin-bottom: @baseLineHeight;
-}
-dt,
-dd {
-  line-height: @baseLineHeight;
-}
-dt {
-  font-weight: bold;
-}
-dd {
-  margin-left: @baseLineHeight / 2;
-}
-// Horizontal layout (like forms)
-.dl-horizontal {
-  .clearfix(); // Ensure dl clears floats if empty dd elements present
-  dt {
-    float: left;
-    width: @horizontalComponentOffset - 20;
-    clear: left;
-    text-align: right;
-    .text-overflow();
-  }
-  dd {
-    margin-left: @horizontalComponentOffset;
-  }
-}
-
-// MISC
-// ----
-
-// Horizontal rules
-hr {
-  margin: @baseLineHeight 0;
-  border: 0;
-  border-top: 1px solid @hrBorder;
-  border-bottom: 1px solid @white;
-}
-
-// Abbreviations and acronyms
-abbr[title],
-// Added data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257
-abbr[data-original-title] {
-  cursor: help;
-  border-bottom: 1px dotted @grayLight;
-}
-abbr.initialism {
-  font-size: 90%;
-  text-transform: uppercase;
-}
-
-// Blockquotes
-blockquote {
-  padding: 0 0 0 15px;
-  margin: 0 0 @baseLineHeight;
-  border-left: 5px solid @grayLighter;
-  p {
-    margin-bottom: 0;
-    font-size: @baseFontSize * 1.25;
-    font-weight: 300;
-    line-height: 1.25;
-  }
-  small {
-    display: block;
-    line-height: @baseLineHeight;
-    color: @grayLight;
-    &:before {
-      content: '\2014 \00A0';
-    }
-  }
-
-  // Float right with text-align: right
-  &.pull-right {
-    float: right;
-    padding-right: 15px;
-    padding-left: 0;
-    border-right: 5px solid @grayLighter;
-    border-left: 0;
-    p,
-    small {
-      text-align: right;
-    }
-    small {
-      &:before {
-        content: '';
-      }
-      &:after {
-        content: '\00A0 \2014';
-      }
-    }
-  }
-}
-
-// Quotes
-q:before,
-q:after,
-blockquote:before,
-blockquote:after {
-  content: "";
-}
-
-// Addresses
-address {
-  display: block;
-  margin-bottom: @baseLineHeight;
-  font-style: normal;
-  line-height: @baseLineHeight;
-}