mirror of
https://github.com/jhillyerd/inbucket.git
synced 2026-08-10 14:38:54 +00:00
Merge elm 0.19 upgrade, closes #125
This commit is contained in:
@@ -4,6 +4,12 @@ Change Log
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `posix-millis` field to REST message and header responses for easier date
|
||||||
|
parsing.
|
||||||
|
|
||||||
|
|
||||||
## [v2.1.0-beta1]
|
## [v2.1.0-beta1]
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ export INBUCKET_LOGLEVEL="debug"
|
|||||||
export INBUCKET_SMTP_DISCARDDOMAINS="bitbucket.local"
|
export INBUCKET_SMTP_DISCARDDOMAINS="bitbucket.local"
|
||||||
export INBUCKET_WEB_TEMPLATECACHE="false"
|
export INBUCKET_WEB_TEMPLATECACHE="false"
|
||||||
export INBUCKET_WEB_COOKIEAUTHKEY="not-secret"
|
export INBUCKET_WEB_COOKIEAUTHKEY="not-secret"
|
||||||
export INBUCKET_WEB_UIDIR="ui/build"
|
export INBUCKET_WEB_UIDIR="ui/dist"
|
||||||
export INBUCKET_STORAGE_TYPE="file"
|
export INBUCKET_STORAGE_TYPE="file"
|
||||||
export INBUCKET_STORAGE_PARAMS="path:/tmp/inbucket"
|
export INBUCKET_STORAGE_PARAMS="path:/tmp/inbucket"
|
||||||
export INBUCKET_STORAGE_RETENTIONPERIOD="3h"
|
export INBUCKET_STORAGE_RETENTIONPERIOD="3h"
|
||||||
|
|||||||
@@ -31,14 +31,15 @@ func MailboxListV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) (
|
|||||||
jmessages := make([]*model.JSONMessageHeaderV1, len(messages))
|
jmessages := make([]*model.JSONMessageHeaderV1, len(messages))
|
||||||
for i, msg := range messages {
|
for i, msg := range messages {
|
||||||
jmessages[i] = &model.JSONMessageHeaderV1{
|
jmessages[i] = &model.JSONMessageHeaderV1{
|
||||||
Mailbox: name,
|
Mailbox: name,
|
||||||
ID: msg.ID,
|
ID: msg.ID,
|
||||||
From: stringutil.StringAddress(msg.From),
|
From: stringutil.StringAddress(msg.From),
|
||||||
To: stringutil.StringAddressList(msg.To),
|
To: stringutil.StringAddressList(msg.To),
|
||||||
Subject: msg.Subject,
|
Subject: msg.Subject,
|
||||||
Date: msg.Date,
|
Date: msg.Date,
|
||||||
Size: msg.Size,
|
PosixMillis: msg.Date.UnixNano() / 1000000,
|
||||||
Seen: msg.Seen,
|
Size: msg.Size,
|
||||||
|
Seen: msg.Seen,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return web.RenderJSON(w, jmessages)
|
return web.RenderJSON(w, jmessages)
|
||||||
@@ -77,15 +78,16 @@ func MailboxShowV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) (
|
|||||||
}
|
}
|
||||||
return web.RenderJSON(w,
|
return web.RenderJSON(w,
|
||||||
&model.JSONMessageV1{
|
&model.JSONMessageV1{
|
||||||
Mailbox: name,
|
Mailbox: name,
|
||||||
ID: msg.ID,
|
ID: msg.ID,
|
||||||
From: stringutil.StringAddress(msg.From),
|
From: stringutil.StringAddress(msg.From),
|
||||||
To: stringutil.StringAddressList(msg.To),
|
To: stringutil.StringAddressList(msg.To),
|
||||||
Subject: msg.Subject,
|
Subject: msg.Subject,
|
||||||
Date: msg.Date,
|
Date: msg.Date,
|
||||||
Size: msg.Size,
|
PosixMillis: msg.Date.UnixNano() / 1000000,
|
||||||
Seen: msg.Seen,
|
Size: msg.Size,
|
||||||
Header: msg.Header(),
|
Seen: msg.Seen,
|
||||||
|
Header: msg.Header(),
|
||||||
Body: &model.JSONMessageBodyV1{
|
Body: &model.JSONMessageBodyV1{
|
||||||
Text: msg.Text(),
|
Text: msg.Text(),
|
||||||
HTML: msg.HTML(),
|
HTML: msg.HTML(),
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ func TestRestMailboxList(t *testing.T) {
|
|||||||
decodedStringEquals(t, result, "[0]/to/[0]", "<to1@host>")
|
decodedStringEquals(t, result, "[0]/to/[0]", "<to1@host>")
|
||||||
decodedStringEquals(t, result, "[0]/subject", "subject 1")
|
decodedStringEquals(t, result, "[0]/subject", "subject 1")
|
||||||
decodedStringEquals(t, result, "[0]/date", "2012-02-01T10:11:12.000000253-08:00")
|
decodedStringEquals(t, result, "[0]/date", "2012-02-01T10:11:12.000000253-08:00")
|
||||||
|
decodedNumberEquals(t, result, "[0]/posix-millis", 1328119872000)
|
||||||
decodedNumberEquals(t, result, "[0]/size", 0)
|
decodedNumberEquals(t, result, "[0]/size", 0)
|
||||||
decodedBoolEquals(t, result, "[0]/seen", false)
|
decodedBoolEquals(t, result, "[0]/seen", false)
|
||||||
decodedStringEquals(t, result, "[1]/mailbox", "good")
|
decodedStringEquals(t, result, "[1]/mailbox", "good")
|
||||||
@@ -120,6 +121,7 @@ func TestRestMailboxList(t *testing.T) {
|
|||||||
decodedStringEquals(t, result, "[1]/to/[0]", "<to1@host>")
|
decodedStringEquals(t, result, "[1]/to/[0]", "<to1@host>")
|
||||||
decodedStringEquals(t, result, "[1]/subject", "subject 2")
|
decodedStringEquals(t, result, "[1]/subject", "subject 2")
|
||||||
decodedStringEquals(t, result, "[1]/date", "2012-07-01T10:11:12.000000253-07:00")
|
decodedStringEquals(t, result, "[1]/date", "2012-07-01T10:11:12.000000253-07:00")
|
||||||
|
decodedNumberEquals(t, result, "[1]/posix-millis", 1341162672000)
|
||||||
decodedNumberEquals(t, result, "[1]/size", 0)
|
decodedNumberEquals(t, result, "[1]/size", 0)
|
||||||
decodedBoolEquals(t, result, "[1]/seen", false)
|
decodedBoolEquals(t, result, "[1]/seen", false)
|
||||||
|
|
||||||
@@ -221,6 +223,7 @@ func TestRestMessage(t *testing.T) {
|
|||||||
decodedStringEquals(t, result, "to/[0]", "<to1@host>")
|
decodedStringEquals(t, result, "to/[0]", "<to1@host>")
|
||||||
decodedStringEquals(t, result, "subject", "subject 1")
|
decodedStringEquals(t, result, "subject", "subject 1")
|
||||||
decodedStringEquals(t, result, "date", "2012-02-01T10:11:12.000000253-08:00")
|
decodedStringEquals(t, result, "date", "2012-02-01T10:11:12.000000253-08:00")
|
||||||
|
decodedNumberEquals(t, result, "posix-millis", 1328119872000)
|
||||||
decodedNumberEquals(t, result, "size", 0)
|
decodedNumberEquals(t, result, "size", 0)
|
||||||
decodedBoolEquals(t, result, "seen", true)
|
decodedBoolEquals(t, result, "seen", true)
|
||||||
decodedStringEquals(t, result, "body/text", "This is some text")
|
decodedStringEquals(t, result, "body/text", "This is some text")
|
||||||
|
|||||||
@@ -6,14 +6,15 @@ import (
|
|||||||
|
|
||||||
// JSONMessageHeaderV1 contains the basic header data for a message
|
// JSONMessageHeaderV1 contains the basic header data for a message
|
||||||
type JSONMessageHeaderV1 struct {
|
type JSONMessageHeaderV1 struct {
|
||||||
Mailbox string `json:"mailbox"`
|
Mailbox string `json:"mailbox"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
From string `json:"from"`
|
From string `json:"from"`
|
||||||
To []string `json:"to"`
|
To []string `json:"to"`
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
Size int64 `json:"size"`
|
PosixMillis int64 `json:"posix-millis"`
|
||||||
Seen bool `json:"seen"`
|
Size int64 `json:"size"`
|
||||||
|
Seen bool `json:"seen"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONMessageV1 contains the same data as the header plus a JSONMessageBody
|
// JSONMessageV1 contains the same data as the header plus a JSONMessageBody
|
||||||
@@ -24,6 +25,7 @@ type JSONMessageV1 struct {
|
|||||||
To []string `json:"to"`
|
To []string `json:"to"`
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
|
PosixMillis int64 `json:"posix-millis"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
Seen bool `json:"seen"`
|
Seen bool `json:"seen"`
|
||||||
Body *JSONMessageBodyV1 `json:"body"`
|
Body *JSONMessageBodyV1 `json:"body"`
|
||||||
|
|||||||
@@ -110,13 +110,14 @@ func (ml *msgListener) WSWriter(conn *websocket.Conn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
header := &model.JSONMessageHeaderV1{
|
header := &model.JSONMessageHeaderV1{
|
||||||
Mailbox: msg.Mailbox,
|
Mailbox: msg.Mailbox,
|
||||||
ID: msg.ID,
|
ID: msg.ID,
|
||||||
From: msg.From,
|
From: msg.From,
|
||||||
To: msg.To,
|
To: msg.To,
|
||||||
Subject: msg.Subject,
|
Subject: msg.Subject,
|
||||||
Date: msg.Date,
|
Date: msg.Date,
|
||||||
Size: msg.Size,
|
PosixMillis: msg.Date.UnixNano() / 1000000,
|
||||||
|
Size: msg.Size,
|
||||||
}
|
}
|
||||||
if conn.WriteJSON(header) != nil {
|
if conn.WriteJSON(header) != nil {
|
||||||
// Write failed
|
// Write failed
|
||||||
|
|||||||
@@ -79,12 +79,14 @@ func decodedNumberEquals(t *testing.T, json interface{}, path string, want float
|
|||||||
t.Errorf("JSON result%s", msg)
|
t.Errorf("JSON result%s", msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if got, ok := val.(float64); ok {
|
got, ok := val.(float64)
|
||||||
|
if ok {
|
||||||
if got == want {
|
if got == want {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.Errorf("JSON result/%s == %v (%T), want: %v", path, val, val, want)
|
t.Errorf("JSON result/%s == %v (%T) %v (int64),\nwant: %v / %v",
|
||||||
|
path, val, val, int64(got), want, int64(want))
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodedStringEquals(t *testing.T, json interface{}, path string, want string) {
|
func decodedStringEquals(t *testing.T, json interface{}, path string, want string) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
@@ -30,6 +31,29 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fileHandler creates a handler that sends the named file regardless of the requested URL.
|
||||||
|
func fileHandler(name string) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
f, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Str("module", "web").Str("path", req.RequestURI).Str("file", name).Err(err).
|
||||||
|
Msg("Error opening file")
|
||||||
|
http.Error(w, "Error opening file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
d, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Str("module", "web").Str("path", req.RequestURI).Str("file", name).Err(err).
|
||||||
|
Msg("Error stating file")
|
||||||
|
http.Error(w, "Error opening file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.ServeContent(w, req, d.Name(), d.ModTime(), f)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// noMatchHandler creates a handler to log requests that Gorilla mux is unable to route,
|
// noMatchHandler creates a handler to log requests that Gorilla mux is unable to route,
|
||||||
// returning specified statusCode to the client.
|
// returning specified statusCode to the client.
|
||||||
func noMatchHandler(statusCode int, message string) http.Handler {
|
func noMatchHandler(statusCode int, message string) http.Handler {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@@ -47,7 +48,7 @@ func init() {
|
|||||||
m.Set("WebSocketConnectsCurrent", ExpWebSocketConnectsCurrent)
|
m.Set("WebSocketConnectsCurrent", ExpWebSocketConnectsCurrent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize sets up things for unit tests or the Start() method
|
// Initialize sets up things for unit tests or the Start() method.
|
||||||
func Initialize(
|
func Initialize(
|
||||||
conf *config.Root,
|
conf *config.Root,
|
||||||
shutdownChan chan bool,
|
shutdownChan chan bool,
|
||||||
@@ -57,11 +58,11 @@ func Initialize(
|
|||||||
rootConfig = conf
|
rootConfig = conf
|
||||||
globalShutdown = shutdownChan
|
globalShutdown = shutdownChan
|
||||||
|
|
||||||
// NewContext() will use this DataStore for the web handlers
|
// NewContext() will use this DataStore for the web handlers.
|
||||||
msgHub = mh
|
msgHub = mh
|
||||||
manager = mm
|
manager = mm
|
||||||
|
|
||||||
// Content Paths
|
// Dynamic paths.
|
||||||
log.Info().Str("module", "web").Str("phase", "startup").Str("path", conf.Web.UIDir).
|
log.Info().Str("module", "web").Str("phase", "startup").Str("path", conf.Web.UIDir).
|
||||||
Msg("Web UI content mapped")
|
Msg("Web UI content mapped")
|
||||||
Router.Handle("/debug/vars", expvar.Handler())
|
Router.Handle("/debug/vars", expvar.Handler())
|
||||||
@@ -74,13 +75,27 @@ func Initialize(
|
|||||||
log.Warn().Str("module", "web").Str("phase", "startup").
|
log.Warn().Str("module", "web").Str("phase", "startup").
|
||||||
Msg("Go pprof tools installed to /debug/pprof")
|
Msg("Go pprof tools installed to /debug/pprof")
|
||||||
}
|
}
|
||||||
// If no other route matches, attempt to service as UI element.
|
|
||||||
Router.PathPrefix("/").Handler(http.StripPrefix("/", http.FileServer(http.Dir(conf.Web.UIDir))))
|
|
||||||
Router.NotFoundHandler = noMatchHandler(http.StatusNotFound, "No route matches URI path")
|
|
||||||
Router.MethodNotAllowedHandler = noMatchHandler(http.StatusMethodNotAllowed,
|
|
||||||
"Method not allowed for URI path")
|
|
||||||
|
|
||||||
// Session cookie setup
|
// Static paths.
|
||||||
|
Router.PathPrefix("/static").Handler(
|
||||||
|
http.StripPrefix("/", http.FileServer(http.Dir(conf.Web.UIDir))))
|
||||||
|
Router.Path("/favicon.png").Handler(
|
||||||
|
fileHandler(filepath.Join(conf.Web.UIDir, "favicon.png")))
|
||||||
|
|
||||||
|
// SPA managed paths.
|
||||||
|
spaHandler := fileHandler(filepath.Join(conf.Web.UIDir, "index.html"))
|
||||||
|
Router.Path("/").Handler(spaHandler)
|
||||||
|
Router.Path("/monitor").Handler(spaHandler)
|
||||||
|
Router.Path("/status").Handler(spaHandler)
|
||||||
|
Router.PathPrefix("/m/").Handler(spaHandler)
|
||||||
|
|
||||||
|
// Error handlers.
|
||||||
|
Router.NotFoundHandler = noMatchHandler(
|
||||||
|
http.StatusNotFound, "No route matches URI path")
|
||||||
|
Router.MethodNotAllowedHandler = noMatchHandler(
|
||||||
|
http.StatusMethodNotAllowed, "Method not allowed for URI path")
|
||||||
|
|
||||||
|
// Session cookie setup.
|
||||||
if conf.Web.CookieAuthKey == "" {
|
if conf.Web.CookieAuthKey == "" {
|
||||||
log.Info().Str("module", "web").Str("phase", "startup").
|
log.Info().Str("module", "web").Str("phase", "startup").
|
||||||
Msg("Generating random cookie.auth.key")
|
Msg("Generating random cookie.auth.key")
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type JSONMessage struct {
|
|||||||
To []string `json:"to"`
|
To []string `json:"to"`
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
|
PosixMillis int64 `json:"posix-millis"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
Seen bool `json:"seen"`
|
Seen bool `json:"seen"`
|
||||||
Header map[string][]string `json:"header"`
|
Header map[string][]string `json:"header"`
|
||||||
@@ -81,6 +82,7 @@ func MailboxMessage(w http.ResponseWriter, req *http.Request, ctx *web.Context)
|
|||||||
To: stringutil.StringAddressList(msg.To),
|
To: stringutil.StringAddressList(msg.To),
|
||||||
Subject: msg.Subject,
|
Subject: msg.Subject,
|
||||||
Date: msg.Date,
|
Date: msg.Date,
|
||||||
|
PosixMillis: msg.Date.UnixNano() / 1000000,
|
||||||
Size: msg.Size,
|
Size: msg.Size,
|
||||||
Seen: msg.Seen,
|
Seen: msg.Seen,
|
||||||
Header: msg.Header(),
|
Header: msg.Header(),
|
||||||
|
|||||||
+15
-16
@@ -1,9 +1,7 @@
|
|||||||
# Inbucket User Interface
|
# Inbucket User Interface
|
||||||
|
|
||||||
This directory contains the source code for the Inbucket web user interface.
|
This directory contains the source code for the Inbucket web user interface.
|
||||||
It is written in [Elm] 0.18, a *delightful language for reliable webapps.*
|
It is written in [Elm] 0.19, a *delightful language for reliable webapps.*
|
||||||
|
|
||||||
The UI was bootstrapped with [Create Elm App].
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -11,15 +9,18 @@ With `$INBUCKET` as the root of the git repository.
|
|||||||
|
|
||||||
One time setup (assuming [Node.js] is already installed):
|
One time setup (assuming [Node.js] is already installed):
|
||||||
|
|
||||||
```
|
|
||||||
npm i create-elm-app@1.10.4 -g
|
|
||||||
```
|
|
||||||
|
|
||||||
In terminal 1 (inbucket daemon):
|
|
||||||
|
|
||||||
```
|
```
|
||||||
cd $INBUCKET/ui
|
cd $INBUCKET/ui
|
||||||
elm-app build
|
npm i elm -g
|
||||||
|
npm i
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will the create `node_modules`, `elm-stuff`, and `dist` directories.
|
||||||
|
|
||||||
|
### Terminal 1: inbucket daemon
|
||||||
|
|
||||||
|
```
|
||||||
cd $INBUCKET
|
cd $INBUCKET
|
||||||
make
|
make
|
||||||
etc/dev-start.sh
|
etc/dev-start.sh
|
||||||
@@ -29,17 +30,15 @@ Inbucket will start, with HTTP listening on port 9000. You may verify the web
|
|||||||
UI is functional if this is your first time building Inbucket, but your dev/test
|
UI is functional if this is your first time building Inbucket, but your dev/test
|
||||||
cycle should favor the development server below.
|
cycle should favor the development server below.
|
||||||
|
|
||||||
In terminal 2 (elm-app development server):
|
### Terminal 2: webpack development server
|
||||||
|
|
||||||
```
|
```
|
||||||
cd $INBUCKET/ui
|
cd $INBUCKET/ui
|
||||||
elm-app start
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
[Create Elm App] will start a development HTTP server listening on port 3000.
|
npm will start a development HTTP server listening on port 3000. You should use
|
||||||
You should use this server for UI development, as it features hot reload and the
|
this server for UI development, as it features hot reload and the Elm debugger.
|
||||||
Elm debugger.
|
|
||||||
|
|
||||||
[Create Elm App]: https://github.com/halfzebra/create-elm-app
|
|
||||||
[Elm]: https://elm-lang.org
|
[Elm]: https://elm-lang.org
|
||||||
[Node.js]: https://nodejs.org
|
[Node.js]: https://nodejs.org
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="x-ua-compatible" content="ie=edge"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><link rel="manifest" href="/manifest.json"><link rel="shortcut icon" href="/favicon.png" type="image/png"><title>Inbucket</title><link href="/static/css/main.8d438738.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script type="text/javascript" src="/static/js/main.04b41a91.js"></script></body></html>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"short_name": "Elm App",
|
|
||||||
"name": "Create Elm App Sample",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "favicon.ico",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": "./index.html",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
"use strict";var precacheConfig=[["/index.html","ed9e929e14fae109b0cd008ba5adb822"],["/static/css/main.8d438738.css","8d438738f900913d8f787dc7ef9b05f9"],["/static/js/main.04b41a91.js","c9dc7ec55e7e303354fc7423a2afaa38"]],cacheName="sw-precache-v3-sw-precache-webpack-plugin-"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(e){return e.redirected?("body"in e?Promise.resolve(e.body):e.blob()).then(function(t){return new Response(t,{headers:e.headers,status:e.status,statusText:e.statusText})}):Promise.resolve(e)},createCacheKey=function(e,t,n,r){var a=new URL(e);return r&&a.pathname.match(r)||(a.search+=(a.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),a.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.hash="",n.search=n.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],r=new URL(t,self.location),a=createCacheKey(r,hashParamName,n,/\.\w{8}\./);return[r.toString(),a]}));function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n)){var r=new Request(n,{credentials:"same-origin"});return fetch(r).then(function(t){if(!t.ok)throw new Error("Request for "+n+" returned a response with status "+t.status);return cleanResponse(t).then(function(t){return e.put(n,t)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);(t=urlsToCacheKeys.has(n))||(n=addDirectoryIndex(n,"index.html"),t=urlsToCacheKeys.has(n));!t&&"navigate"===e.request.mode&&isPathWhitelisted(["^(?!\\/__).*"],e.request.url)&&(n=new URL("/index.html",self.location).toString(),t=urlsToCacheKeys.has(n)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}});
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
{"main":{"js":"/static/js/main.04b41a91.js","css":"/static/css/main.8d438738.css"},"":{"html":"/index.html"}}
|
|
||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="theme-color" content="#000000">
|
||||||
|
<title>Inbucket</title>
|
||||||
|
<link rel="shortcut icon" href="/favicon.png"></head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
You need to enable JavaScript to run this app.
|
||||||
|
</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="text/javascript" src="/static/main.js"></script></body>
|
||||||
|
</html>
|
||||||
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.0.0",
|
|
||||||
"summary": "Elm powered UI for Inbucket",
|
|
||||||
"repository": "https://github.com/jhillyerd/inbucket.git",
|
|
||||||
"license": "MIT",
|
|
||||||
"source-directories": [
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"exposed-modules": [],
|
|
||||||
"proxy": {
|
|
||||||
"/api": {
|
|
||||||
"target": "http://localhost:9000",
|
|
||||||
"ws": true
|
|
||||||
},
|
|
||||||
"/debug": {
|
|
||||||
"target": "http://localhost:9000"
|
|
||||||
},
|
|
||||||
"/serve": {
|
|
||||||
"target": "http://localhost:9000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0",
|
|
||||||
"basti1302/elm-human-readable-filesize": "1.1.0 <= v < 2.0.0",
|
|
||||||
"elm-lang/core": "5.1.1 <= v < 6.0.0",
|
|
||||||
"elm-lang/html": "2.0.0 <= v < 3.0.0",
|
|
||||||
"elm-lang/http": "1.0.0 <= v < 2.0.0",
|
|
||||||
"elm-lang/navigation": "2.1.0 <= v < 3.0.0",
|
|
||||||
"elm-lang/svg": "2.0.0 <= v < 3.0.0",
|
|
||||||
"evancz/url-parser": "2.0.1 <= v < 3.0.0",
|
|
||||||
"jweir/sparkline": "3.0.0 <= v < 4.0.0",
|
|
||||||
"ryannhg/elm-date-format": "2.1.2 <= v < 3.0.0"
|
|
||||||
},
|
|
||||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
|
||||||
}
|
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"type": "application",
|
||||||
|
"source-directories": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"elm-version": "0.19.0",
|
||||||
|
"dependencies": {
|
||||||
|
"direct": {
|
||||||
|
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||||
|
"basti1302/elm-human-readable-filesize": "1.1.1",
|
||||||
|
"elm/browser": "1.0.1",
|
||||||
|
"elm/core": "1.0.2",
|
||||||
|
"elm/html": "1.0.0",
|
||||||
|
"elm/http": "2.0.0",
|
||||||
|
"elm/json": "1.1.2",
|
||||||
|
"elm/svg": "1.0.1",
|
||||||
|
"elm/time": "1.0.0",
|
||||||
|
"elm/url": "1.0.0",
|
||||||
|
"jweir/sparkline": "4.0.0",
|
||||||
|
"ryannhg/date-format": "2.1.0"
|
||||||
|
},
|
||||||
|
"indirect": {
|
||||||
|
"elm/bytes": "1.0.3",
|
||||||
|
"elm/file": "1.0.1",
|
||||||
|
"elm/regex": "1.0.0",
|
||||||
|
"elm/virtual-dom": "1.0.2",
|
||||||
|
"myrho/elm-round": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test-dependencies": {
|
||||||
|
"direct": {},
|
||||||
|
"indirect": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+8339
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "inbucket-ui",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --mode production",
|
||||||
|
"watch": "webpack --mode development --watch",
|
||||||
|
"dev": "webpack-dev-server --mode development --port 3000 --hot",
|
||||||
|
"errors": "webpack --mode development --display-error-details"
|
||||||
|
},
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.1.6",
|
||||||
|
"@babel/preset-env": "^7.1.6",
|
||||||
|
"babel-loader": "^8.0.4",
|
||||||
|
"css-loader": "^1.0.1",
|
||||||
|
"elm-hot-webpack-loader": "^1.0.2",
|
||||||
|
"elm-webpack-loader": "^5.0.0",
|
||||||
|
"html-webpack-plugin": "^3.2.0",
|
||||||
|
"node-elm-compiler": "^5.0.1",
|
||||||
|
"style-loader": "^0.23.1",
|
||||||
|
"webpack": "^4.25.1",
|
||||||
|
"webpack-cli": "^3.1.2",
|
||||||
|
"webpack-dev-server": "^3.1.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-15
@@ -1,22 +1,16 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<meta name="theme-color" content="#000000">
|
<meta name="theme-color" content="#000000">
|
||||||
<!--
|
<title>Inbucket</title>
|
||||||
manifest.json provides metadata used when your web app is added to the
|
|
||||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
|
||||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png" type="image/png">
|
|
||||||
<title>Inbucket</title>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
You need to enable JavaScript to run this app.
|
You need to enable JavaScript to run this app.
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"short_name": "Elm App",
|
"short_name": "Inbucket",
|
||||||
"name": "Create Elm App Sample",
|
"name": "Inbucket",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.png",
|
||||||
"sizes": "192x192",
|
"sizes": "16x16",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
+5
-15
@@ -1,21 +1,11 @@
|
|||||||
module Data.Date exposing (date)
|
module Data.Date exposing (date)
|
||||||
|
|
||||||
import Date exposing (Date)
|
import Json.Decode exposing (..)
|
||||||
import Json.Decode as Decode exposing (..)
|
import Time exposing (Posix)
|
||||||
|
|
||||||
|
|
||||||
{-| Decode an ISO 8601 date
|
{-| Decode a POSIX milliseconds timestamp.
|
||||||
-}
|
-}
|
||||||
date : Decoder Date
|
date : Decoder Posix
|
||||||
date =
|
date =
|
||||||
let
|
int |> map Time.millisToPosix
|
||||||
convert : String -> Decoder Date
|
|
||||||
convert raw =
|
|
||||||
case Date.fromString raw of
|
|
||||||
Ok date ->
|
|
||||||
succeed date
|
|
||||||
|
|
||||||
Err error ->
|
|
||||||
fail error
|
|
||||||
in
|
|
||||||
string |> andThen convert
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
module Data.Message exposing (Attachment, Message, attachmentDecoder, decoder)
|
module Data.Message exposing (Attachment, Message, attachmentDecoder, decoder)
|
||||||
|
|
||||||
import Data.Date exposing (date)
|
import Data.Date exposing (date)
|
||||||
import Date exposing (Date)
|
import Json.Decode exposing (..)
|
||||||
import Json.Decode as Decode exposing (..)
|
|
||||||
import Json.Decode.Pipeline exposing (..)
|
import Json.Decode.Pipeline exposing (..)
|
||||||
|
import Time exposing (Posix)
|
||||||
|
|
||||||
|
|
||||||
type alias Message =
|
type alias Message =
|
||||||
@@ -12,7 +12,7 @@ type alias Message =
|
|||||||
, from : String
|
, from : String
|
||||||
, to : List String
|
, to : List String
|
||||||
, subject : String
|
, subject : String
|
||||||
, date : Date
|
, date : Posix
|
||||||
, size : Int
|
, size : Int
|
||||||
, seen : Bool
|
, seen : Bool
|
||||||
, text : String
|
, text : String
|
||||||
@@ -30,13 +30,13 @@ type alias Attachment =
|
|||||||
|
|
||||||
decoder : Decoder Message
|
decoder : Decoder Message
|
||||||
decoder =
|
decoder =
|
||||||
decode Message
|
succeed Message
|
||||||
|> required "mailbox" string
|
|> required "mailbox" string
|
||||||
|> required "id" string
|
|> required "id" string
|
||||||
|> optional "from" string ""
|
|> optional "from" string ""
|
||||||
|> required "to" (list string)
|
|> required "to" (list string)
|
||||||
|> optional "subject" string ""
|
|> optional "subject" string ""
|
||||||
|> required "date" date
|
|> required "posix-millis" date
|
||||||
|> required "size" int
|
|> required "size" int
|
||||||
|> required "seen" bool
|
|> required "seen" bool
|
||||||
|> required "text" string
|
|> required "text" string
|
||||||
@@ -46,7 +46,7 @@ decoder =
|
|||||||
|
|
||||||
attachmentDecoder : Decoder Attachment
|
attachmentDecoder : Decoder Attachment
|
||||||
attachmentDecoder =
|
attachmentDecoder =
|
||||||
decode Attachment
|
succeed Attachment
|
||||||
|> required "id" string
|
|> required "id" string
|
||||||
|> required "filename" string
|
|> required "filename" string
|
||||||
|> required "content-type" string
|
|> required "content-type" string
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
module Data.MessageHeader exposing (MessageHeader, decoder)
|
module Data.MessageHeader exposing (MessageHeader, decoder)
|
||||||
|
|
||||||
import Data.Date exposing (date)
|
import Data.Date exposing (date)
|
||||||
import Date exposing (Date)
|
import Json.Decode exposing (..)
|
||||||
import Json.Decode as Decode exposing (..)
|
|
||||||
import Json.Decode.Pipeline exposing (..)
|
import Json.Decode.Pipeline exposing (..)
|
||||||
|
import Time exposing (Posix)
|
||||||
|
|
||||||
|
|
||||||
type alias MessageHeader =
|
type alias MessageHeader =
|
||||||
@@ -12,7 +12,7 @@ type alias MessageHeader =
|
|||||||
, from : String
|
, from : String
|
||||||
, to : List String
|
, to : List String
|
||||||
, subject : String
|
, subject : String
|
||||||
, date : Date
|
, date : Posix
|
||||||
, size : Int
|
, size : Int
|
||||||
, seen : Bool
|
, seen : Bool
|
||||||
}
|
}
|
||||||
@@ -20,12 +20,12 @@ type alias MessageHeader =
|
|||||||
|
|
||||||
decoder : Decoder MessageHeader
|
decoder : Decoder MessageHeader
|
||||||
decoder =
|
decoder =
|
||||||
decode MessageHeader
|
succeed MessageHeader
|
||||||
|> required "mailbox" string
|
|> required "mailbox" string
|
||||||
|> required "id" string
|
|> required "id" string
|
||||||
|> optional "from" string ""
|
|> optional "from" string ""
|
||||||
|> required "to" (list string)
|
|> required "to" (list string)
|
||||||
|> optional "subject" string ""
|
|> optional "subject" string ""
|
||||||
|> required "date" date
|
|> required "posix-millis" date
|
||||||
|> required "size" int
|
|> required "size" int
|
||||||
|> required "seen" bool
|
|> required "seen" bool
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ type alias Metrics =
|
|||||||
|
|
||||||
decoder : Decoder Metrics
|
decoder : Decoder Metrics
|
||||||
decoder =
|
decoder =
|
||||||
decode Metrics
|
succeed Metrics
|
||||||
|> requiredAt [ "memstats", "Sys" ] int
|
|> requiredAt [ "memstats", "Sys" ] int
|
||||||
|> requiredAt [ "memstats", "HeapSys" ] int
|
|> requiredAt [ "memstats", "HeapSys" ] int
|
||||||
|> requiredAt [ "memstats", "HeapAlloc" ] int
|
|> requiredAt [ "memstats", "HeapAlloc" ] int
|
||||||
@@ -59,4 +59,6 @@ decoder =
|
|||||||
-}
|
-}
|
||||||
decodeIntList : Decoder (List Int)
|
decodeIntList : Decoder (List Int)
|
||||||
decodeIntList =
|
decodeIntList =
|
||||||
map (String.split "," >> List.map (String.toInt >> Result.withDefault 0)) string
|
string
|
||||||
|
|> map (String.split ",")
|
||||||
|
|> map (List.map (String.toInt >> Maybe.withDefault 0))
|
||||||
|
|||||||
+10
-8
@@ -9,13 +9,15 @@ module Data.Session exposing
|
|||||||
, update
|
, update
|
||||||
)
|
)
|
||||||
|
|
||||||
import Json.Decode as Decode exposing (..)
|
import Browser.Navigation as Nav
|
||||||
|
import Json.Decode exposing (..)
|
||||||
import Json.Decode.Pipeline exposing (..)
|
import Json.Decode.Pipeline exposing (..)
|
||||||
import Navigation exposing (Location)
|
import Url exposing (Url)
|
||||||
|
|
||||||
|
|
||||||
type alias Session =
|
type alias Session =
|
||||||
{ host : String
|
{ key : Nav.Key
|
||||||
|
, host : String
|
||||||
, flash : String
|
, flash : String
|
||||||
, routing : Bool
|
, routing : Bool
|
||||||
, persistent : Persistent
|
, persistent : Persistent
|
||||||
@@ -36,9 +38,9 @@ type Msg
|
|||||||
| AddRecent String
|
| AddRecent String
|
||||||
|
|
||||||
|
|
||||||
init : Location -> Persistent -> Session
|
init : Nav.Key -> Url -> Persistent -> Session
|
||||||
init location persistent =
|
init key location persistent =
|
||||||
Session location.host "" True persistent
|
Session key location.host "" True persistent
|
||||||
|
|
||||||
|
|
||||||
update : Msg -> Session -> Session
|
update : Msg -> Session -> Session
|
||||||
@@ -84,10 +86,10 @@ none =
|
|||||||
|
|
||||||
decoder : Decoder Persistent
|
decoder : Decoder Persistent
|
||||||
decoder =
|
decoder =
|
||||||
decode Persistent
|
succeed Persistent
|
||||||
|> optional "recentMailboxes" (list string) []
|
|> optional "recentMailboxes" (list string) []
|
||||||
|
|
||||||
|
|
||||||
decodeValueWithDefault : Value -> Persistent
|
decodeValueWithDefault : Value -> Persistent
|
||||||
decodeValueWithDefault =
|
decodeValueWithDefault =
|
||||||
Decode.decodeValue decoder >> Result.withDefault { recentMailboxes = [] }
|
decodeValue decoder >> Result.withDefault { recentMailboxes = [] }
|
||||||
|
|||||||
+11
-15
@@ -3,29 +3,29 @@ module HttpUtil exposing (delete, errorString, patch)
|
|||||||
import Http
|
import Http
|
||||||
|
|
||||||
|
|
||||||
delete : String -> Http.Request ()
|
delete : (Result Http.Error () -> msg) -> String -> Cmd msg
|
||||||
delete url =
|
delete msg url =
|
||||||
Http.request
|
Http.request
|
||||||
{ method = "DELETE"
|
{ method = "DELETE"
|
||||||
, headers = []
|
, headers = []
|
||||||
, url = url
|
, url = url
|
||||||
, body = Http.emptyBody
|
, body = Http.emptyBody
|
||||||
, expect = Http.expectStringResponse (\_ -> Ok ())
|
, expect = Http.expectWhatever msg
|
||||||
, timeout = Nothing
|
, timeout = Nothing
|
||||||
, withCredentials = False
|
, tracker = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
patch : String -> Http.Body -> Http.Request ()
|
patch : (Result Http.Error () -> msg) -> String -> Http.Body -> Cmd msg
|
||||||
patch url body =
|
patch msg url body =
|
||||||
Http.request
|
Http.request
|
||||||
{ method = "PATCH"
|
{ method = "PATCH"
|
||||||
, headers = []
|
, headers = []
|
||||||
, url = url
|
, url = url
|
||||||
, body = body
|
, body = body
|
||||||
, expect = Http.expectStringResponse (\_ -> Ok ())
|
, expect = Http.expectWhatever msg
|
||||||
, timeout = Nothing
|
, timeout = Nothing
|
||||||
, withCredentials = False
|
, tracker = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -42,11 +42,7 @@ errorString error =
|
|||||||
"HTTP Network error"
|
"HTTP Network error"
|
||||||
|
|
||||||
Http.BadStatus res ->
|
Http.BadStatus res ->
|
||||||
"Bad HTTP status: " ++ toString res.status.code
|
"Bad HTTP status: " ++ String.fromInt res
|
||||||
|
|
||||||
Http.BadPayload msg res ->
|
Http.BadBody msg ->
|
||||||
"Bad HTTP payload: "
|
"Bad HTTP body: " ++ msg
|
||||||
++ msg
|
|
||||||
++ " ("
|
|
||||||
++ toString res.status.code
|
|
||||||
++ ")"
|
|
||||||
|
|||||||
+55
-41
@@ -1,15 +1,17 @@
|
|||||||
module Main exposing (Model, Msg(..), Page(..), applySession, init, main, pageSubscriptions, sessionChange, setRoute, subscriptions, update, updatePage, view)
|
module Main exposing (main)
|
||||||
|
|
||||||
|
import Browser exposing (Document, UrlRequest)
|
||||||
|
import Browser.Navigation as Nav
|
||||||
import Data.Session as Session exposing (Session, decoder)
|
import Data.Session as Session exposing (Session, decoder)
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Json.Decode as Decode exposing (Value)
|
import Json.Decode as D exposing (Value)
|
||||||
import Navigation exposing (Location)
|
|
||||||
import Page.Home as Home
|
import Page.Home as Home
|
||||||
import Page.Mailbox as Mailbox
|
import Page.Mailbox as Mailbox
|
||||||
import Page.Monitor as Monitor
|
import Page.Monitor as Monitor
|
||||||
import Page.Status as Status
|
import Page.Status as Status
|
||||||
import Ports
|
import Ports
|
||||||
import Route exposing (Route)
|
import Route exposing (Route)
|
||||||
|
import Url exposing (Url)
|
||||||
import Views.Page as Page exposing (ActivePage(..), frame)
|
import Views.Page as Page exposing (ActivePage(..), frame)
|
||||||
|
|
||||||
|
|
||||||
@@ -31,11 +33,11 @@ type alias Model =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
init : Value -> Location -> ( Model, Cmd Msg )
|
init : Value -> Url -> Nav.Key -> ( Model, Cmd Msg )
|
||||||
init sessionValue location =
|
init sessionValue location key =
|
||||||
let
|
let
|
||||||
session =
|
session =
|
||||||
Session.init location (Session.decodeValueWithDefault sessionValue)
|
Session.init key location (Session.decodeValueWithDefault sessionValue)
|
||||||
|
|
||||||
( subModel, _ ) =
|
( subModel, _ ) =
|
||||||
Home.init
|
Home.init
|
||||||
@@ -47,16 +49,17 @@ init sessionValue location =
|
|||||||
}
|
}
|
||||||
|
|
||||||
route =
|
route =
|
||||||
Route.fromLocation location
|
Route.fromUrl location
|
||||||
in
|
in
|
||||||
applySession (setRoute route model)
|
applySession (setRoute route model)
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= SetRoute Route
|
= SetRoute Route
|
||||||
| NewRoute Route
|
| UrlChanged Url
|
||||||
| UpdateSession (Result String Session.Persistent)
|
| LinkClicked UrlRequest
|
||||||
| MailboxNameInput String
|
| UpdateSession (Result D.Error Session.Persistent)
|
||||||
|
| OnMailboxNameInput String
|
||||||
| ViewMailbox String
|
| ViewMailbox String
|
||||||
| HomeMsg Home.Msg
|
| HomeMsg Home.Msg
|
||||||
| MailboxMsg Mailbox.Msg
|
| MailboxMsg Mailbox.Msg
|
||||||
@@ -76,9 +79,9 @@ subscriptions model =
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
sessionChange : Sub (Result String Session.Persistent)
|
sessionChange : Sub (Result D.Error Session.Persistent)
|
||||||
sessionChange =
|
sessionChange =
|
||||||
Ports.onSessionChange (Decode.decodeValue Session.decoder)
|
Ports.onSessionChange (D.decodeValue Session.decoder)
|
||||||
|
|
||||||
|
|
||||||
pageSubscriptions : Page -> Sub Msg
|
pageSubscriptions : Page -> Sub Msg
|
||||||
@@ -105,19 +108,27 @@ update : Msg -> Model -> ( Model, Cmd Msg )
|
|||||||
update msg model =
|
update msg model =
|
||||||
applySession <|
|
applySession <|
|
||||||
case msg of
|
case msg of
|
||||||
SetRoute route ->
|
LinkClicked req ->
|
||||||
-- Updates broser URL to requested route.
|
case req of
|
||||||
( model, Route.newUrl route, Session.none )
|
Browser.Internal url ->
|
||||||
|
( model, Nav.pushUrl model.session.key (Url.toString url), Session.none )
|
||||||
|
|
||||||
NewRoute route ->
|
Browser.External url ->
|
||||||
|
( model, Nav.load url, Session.none )
|
||||||
|
|
||||||
|
UrlChanged url ->
|
||||||
-- Responds to new browser URL.
|
-- Responds to new browser URL.
|
||||||
if model.session.routing then
|
if model.session.routing then
|
||||||
setRoute route model
|
setRoute (Route.fromUrl url) model
|
||||||
|
|
||||||
else
|
else
|
||||||
-- Skip once, but re-enable routing.
|
-- Skip once, but re-enable routing.
|
||||||
( model, Cmd.none, Session.EnableRouting )
|
( model, Cmd.none, Session.EnableRouting )
|
||||||
|
|
||||||
|
SetRoute route ->
|
||||||
|
-- Updates broser URL to requested route.
|
||||||
|
( model, Route.newUrl model.session.key route, Session.none )
|
||||||
|
|
||||||
UpdateSession (Ok persistent) ->
|
UpdateSession (Ok persistent) ->
|
||||||
let
|
let
|
||||||
session =
|
session =
|
||||||
@@ -129,18 +140,17 @@ update msg model =
|
|||||||
)
|
)
|
||||||
|
|
||||||
UpdateSession (Err error) ->
|
UpdateSession (Err error) ->
|
||||||
let
|
( model
|
||||||
_ =
|
, Cmd.none
|
||||||
Debug.log "Error decoding session" error
|
, Session.SetFlash ("Error decoding session: " ++ D.errorToString error)
|
||||||
in
|
)
|
||||||
( model, Cmd.none, Session.none )
|
|
||||||
|
|
||||||
MailboxNameInput name ->
|
OnMailboxNameInput name ->
|
||||||
( { model | mailboxName = name }, Cmd.none, Session.none )
|
( { model | mailboxName = name }, Cmd.none, Session.none )
|
||||||
|
|
||||||
ViewMailbox name ->
|
ViewMailbox name ->
|
||||||
( { model | mailboxName = "" }
|
( { model | mailboxName = "" }
|
||||||
, Route.newUrl (Route.Mailbox name)
|
, Route.newUrl model.session.key (Route.Mailbox name)
|
||||||
, Session.none
|
, Session.none
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -230,10 +240,7 @@ setRoute route model =
|
|||||||
|
|
||||||
Route.Status ->
|
Route.Status ->
|
||||||
( { model | page = Status Status.init }
|
( { model | page = Status Status.init }
|
||||||
, Cmd.batch
|
, Cmd.map StatusMsg Status.load
|
||||||
[ Ports.windowTitle "Inbucket Status"
|
|
||||||
, Cmd.map StatusMsg Status.load
|
|
||||||
]
|
|
||||||
, Session.none
|
, Session.none
|
||||||
)
|
)
|
||||||
in
|
in
|
||||||
@@ -269,7 +276,7 @@ applySession ( model, cmd, sessionMsg ) =
|
|||||||
-- VIEW
|
-- VIEW
|
||||||
|
|
||||||
|
|
||||||
view : Model -> Html Msg
|
view : Model -> Document Msg
|
||||||
view model =
|
view model =
|
||||||
let
|
let
|
||||||
mailbox =
|
mailbox =
|
||||||
@@ -282,31 +289,36 @@ view model =
|
|||||||
|
|
||||||
controls =
|
controls =
|
||||||
{ viewMailbox = ViewMailbox
|
{ viewMailbox = ViewMailbox
|
||||||
, mailboxOnInput = MailboxNameInput
|
, mailboxOnInput = OnMailboxNameInput
|
||||||
, mailboxValue = model.mailboxName
|
, mailboxValue = model.mailboxName
|
||||||
, recentOptions = model.session.persistent.recentMailboxes
|
, recentOptions = model.session.persistent.recentMailboxes
|
||||||
, recentActive = mailbox
|
, recentActive = mailbox
|
||||||
}
|
}
|
||||||
|
|
||||||
frame =
|
framePage :
|
||||||
Page.frame controls model.session
|
ActivePage
|
||||||
|
-> (msg -> Msg)
|
||||||
|
-> { title : String, content : Html msg }
|
||||||
|
-> Document Msg
|
||||||
|
framePage page toMsg { title, content } =
|
||||||
|
Document title
|
||||||
|
[ content
|
||||||
|
|> Html.map toMsg
|
||||||
|
|> Page.frame controls model.session page
|
||||||
|
]
|
||||||
in
|
in
|
||||||
case model.page of
|
case model.page of
|
||||||
Home subModel ->
|
Home subModel ->
|
||||||
Html.map HomeMsg (Home.view model.session subModel)
|
framePage Page.Other HomeMsg (Home.view model.session subModel)
|
||||||
|> frame Page.Other
|
|
||||||
|
|
||||||
Mailbox subModel ->
|
Mailbox subModel ->
|
||||||
Html.map MailboxMsg (Mailbox.view model.session subModel)
|
framePage Page.Mailbox MailboxMsg (Mailbox.view model.session subModel)
|
||||||
|> frame Page.Mailbox
|
|
||||||
|
|
||||||
Monitor subModel ->
|
Monitor subModel ->
|
||||||
Html.map MonitorMsg (Monitor.view model.session subModel)
|
framePage Page.Monitor MonitorMsg (Monitor.view model.session subModel)
|
||||||
|> frame Page.Monitor
|
|
||||||
|
|
||||||
Status subModel ->
|
Status subModel ->
|
||||||
Html.map StatusMsg (Status.view model.session subModel)
|
framePage Page.Status StatusMsg (Status.view model.session subModel)
|
||||||
|> frame Page.Status
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -315,9 +327,11 @@ view model =
|
|||||||
|
|
||||||
main : Program Value Model Msg
|
main : Program Value Model Msg
|
||||||
main =
|
main =
|
||||||
Navigation.programWithFlags (Route.fromLocation >> NewRoute)
|
Browser.application
|
||||||
{ init = init
|
{ init = init
|
||||||
, view = view
|
, view = view
|
||||||
, update = update
|
, update = update
|
||||||
, subscriptions = subscriptions
|
, subscriptions = subscriptions
|
||||||
|
, onUrlChange = UrlChanged
|
||||||
|
, onUrlRequest = LinkClicked
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-22
@@ -19,18 +19,14 @@ type alias Model =
|
|||||||
|
|
||||||
init : ( Model, Cmd Msg )
|
init : ( Model, Cmd Msg )
|
||||||
init =
|
init =
|
||||||
( Model ""
|
let
|
||||||
, Cmd.batch
|
cmdGreeting =
|
||||||
[ Ports.windowTitle "Inbucket"
|
Http.get
|
||||||
, cmdGreeting
|
{ url = "/serve/greeting"
|
||||||
]
|
, expect = Http.expectString GreetingLoaded
|
||||||
)
|
}
|
||||||
|
in
|
||||||
|
( Model "", cmdGreeting )
|
||||||
cmdGreeting : Cmd Msg
|
|
||||||
cmdGreeting =
|
|
||||||
Http.send GreetingResult <|
|
|
||||||
Http.getString "/serve/greeting"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -38,16 +34,16 @@ cmdGreeting =
|
|||||||
|
|
||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= GreetingResult (Result Http.Error String)
|
= GreetingLoaded (Result Http.Error String)
|
||||||
|
|
||||||
|
|
||||||
update : Session -> Msg -> Model -> ( Model, Cmd Msg, Session.Msg )
|
update : Session -> Msg -> Model -> ( Model, Cmd Msg, Session.Msg )
|
||||||
update session msg model =
|
update session msg model =
|
||||||
case msg of
|
case msg of
|
||||||
GreetingResult (Ok greeting) ->
|
GreetingLoaded (Ok greeting) ->
|
||||||
( Model greeting, Cmd.none, Session.none )
|
( Model greeting, Cmd.none, Session.none )
|
||||||
|
|
||||||
GreetingResult (Err err) ->
|
GreetingLoaded (Err err) ->
|
||||||
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
||||||
|
|
||||||
|
|
||||||
@@ -55,12 +51,15 @@ update session msg model =
|
|||||||
-- VIEW --
|
-- VIEW --
|
||||||
|
|
||||||
|
|
||||||
view : Session -> Model -> Html Msg
|
view : Session -> Model -> { title : String, content : Html Msg }
|
||||||
view session model =
|
view session model =
|
||||||
div [ id "page" ]
|
{ title = "Inbucket"
|
||||||
[ div
|
, content =
|
||||||
[ class "greeting"
|
div [ id "page" ]
|
||||||
, property "innerHTML" (Encode.string model.greeting)
|
[ Html.node "rendered-html"
|
||||||
|
[ class "greeting"
|
||||||
|
, property "content" (Encode.string model.greeting)
|
||||||
|
]
|
||||||
|
[]
|
||||||
]
|
]
|
||||||
[]
|
}
|
||||||
]
|
|
||||||
|
|||||||
+114
-103
@@ -3,15 +3,14 @@ module Page.Mailbox exposing (Model, Msg, init, load, subscriptions, update, vie
|
|||||||
import Data.Message as Message exposing (Message)
|
import Data.Message as Message exposing (Message)
|
||||||
import Data.MessageHeader as MessageHeader exposing (MessageHeader)
|
import Data.MessageHeader as MessageHeader exposing (MessageHeader)
|
||||||
import Data.Session as Session exposing (Session)
|
import Data.Session as Session exposing (Session)
|
||||||
import Date exposing (Date)
|
import DateFormat as DF
|
||||||
import DateFormat
|
|
||||||
import DateFormat.Relative as Relative
|
import DateFormat.Relative as Relative
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes
|
import Html.Attributes
|
||||||
exposing
|
exposing
|
||||||
( class
|
( class
|
||||||
, classList
|
, classList
|
||||||
, downloadAs
|
, download
|
||||||
, href
|
, href
|
||||||
, id
|
, id
|
||||||
, placeholder
|
, placeholder
|
||||||
@@ -28,7 +27,7 @@ import Json.Encode as Encode
|
|||||||
import Ports
|
import Ports
|
||||||
import Route
|
import Route
|
||||||
import Task
|
import Task
|
||||||
import Time exposing (Time)
|
import Time exposing (Posix)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -65,7 +64,7 @@ type alias MessageList =
|
|||||||
|
|
||||||
type alias VisibleMessage =
|
type alias VisibleMessage =
|
||||||
{ message : Message
|
{ message : Message
|
||||||
, markSeenAt : Maybe Time
|
, markSeenAt : Maybe Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -74,13 +73,13 @@ type alias Model =
|
|||||||
, state : State
|
, state : State
|
||||||
, bodyMode : Body
|
, bodyMode : Body
|
||||||
, searchInput : String
|
, searchInput : String
|
||||||
, now : Date
|
, now : Posix
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
init : String -> Maybe MessageID -> ( Model, Cmd Msg )
|
init : String -> Maybe MessageID -> ( Model, Cmd Msg )
|
||||||
init mailboxName selection =
|
init mailboxName selection =
|
||||||
( Model mailboxName (LoadingList selection) SafeHtmlBody "" (Date.fromTime 0)
|
( Model mailboxName (LoadingList selection) SafeHtmlBody "" (Time.millisToPosix 0)
|
||||||
, load mailboxName
|
, load mailboxName
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,8 +87,7 @@ init mailboxName selection =
|
|||||||
load : String -> Cmd Msg
|
load : String -> Cmd Msg
|
||||||
load mailboxName =
|
load mailboxName =
|
||||||
Cmd.batch
|
Cmd.batch
|
||||||
[ Ports.windowTitle (mailboxName ++ " - Inbucket")
|
[ Task.perform Tick Time.now
|
||||||
, Task.perform Tick Time.now
|
|
||||||
, getList mailboxName
|
, getList mailboxName
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -108,13 +106,13 @@ subscriptions model =
|
|||||||
Sub.none
|
Sub.none
|
||||||
|
|
||||||
else
|
else
|
||||||
Time.every (250 * Time.millisecond) SeenTick
|
Time.every 250 MarkSeenTick
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
Sub.none
|
Sub.none
|
||||||
in
|
in
|
||||||
Sub.batch
|
Sub.batch
|
||||||
[ Time.every (30 * Time.second) Tick
|
[ Time.every (30 * 1000) Tick
|
||||||
, subSeen
|
, subSeen
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -124,20 +122,20 @@ subscriptions model =
|
|||||||
|
|
||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= ClickMessage MessageID
|
= ListLoaded (Result Http.Error (List MessageHeader))
|
||||||
| DeleteMessage Message
|
| ClickMessage MessageID
|
||||||
| DeleteMessageResult (Result Http.Error ())
|
| OpenMessage MessageID
|
||||||
| ListResult (Result Http.Error (List MessageHeader))
|
| MessageLoaded (Result Http.Error Message)
|
||||||
| MarkSeenResult (Result Http.Error ())
|
|
||||||
| MessageResult (Result Http.Error Message)
|
|
||||||
| MessageBody Body
|
| MessageBody Body
|
||||||
| OpenedTime Time
|
| OpenedTime Posix
|
||||||
| Purge
|
| MarkSeenTick Posix
|
||||||
| PurgeResult (Result Http.Error ())
|
| MarkedSeen (Result Http.Error ())
|
||||||
| SearchInput String
|
| DeleteMessage Message
|
||||||
| SeenTick Time
|
| DeletedMessage (Result Http.Error ())
|
||||||
| Tick Time
|
| PurgeMailbox
|
||||||
| ViewMessage MessageID
|
| PurgedMailbox (Result Http.Error ())
|
||||||
|
| OnSearchInput String
|
||||||
|
| Tick Posix
|
||||||
|
|
||||||
|
|
||||||
update : Session -> Msg -> Model -> ( Model, Cmd Msg, Session.Msg )
|
update : Session -> Msg -> Model -> ( Model, Cmd Msg, Session.Msg )
|
||||||
@@ -147,28 +145,25 @@ update session msg model =
|
|||||||
( updateSelected model id
|
( updateSelected model id
|
||||||
, Cmd.batch
|
, Cmd.batch
|
||||||
[ -- Update browser location.
|
[ -- Update browser location.
|
||||||
Route.newUrl (Route.Message model.mailboxName id)
|
Route.newUrl session.key (Route.Message model.mailboxName id)
|
||||||
, getMessage model.mailboxName id
|
, getMessage model.mailboxName id
|
||||||
]
|
]
|
||||||
, Session.DisableRouting
|
, Session.DisableRouting
|
||||||
)
|
)
|
||||||
|
|
||||||
ViewMessage id ->
|
OpenMessage id ->
|
||||||
( updateSelected model id
|
updateOpenMessage session model id
|
||||||
, getMessage model.mailboxName id
|
|
||||||
, Session.AddRecent model.mailboxName
|
|
||||||
)
|
|
||||||
|
|
||||||
DeleteMessage message ->
|
DeleteMessage message ->
|
||||||
updateDeleteMessage model message
|
updateDeleteMessage model message
|
||||||
|
|
||||||
DeleteMessageResult (Ok _) ->
|
DeletedMessage (Ok _) ->
|
||||||
( model, Cmd.none, Session.none )
|
( model, Cmd.none, Session.none )
|
||||||
|
|
||||||
DeleteMessageResult (Err err) ->
|
DeletedMessage (Err err) ->
|
||||||
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
||||||
|
|
||||||
ListResult (Ok headers) ->
|
ListLoaded (Ok headers) ->
|
||||||
case model.state of
|
case model.state of
|
||||||
LoadingList selection ->
|
LoadingList selection ->
|
||||||
let
|
let
|
||||||
@@ -179,8 +174,7 @@ update session msg model =
|
|||||||
in
|
in
|
||||||
case selection of
|
case selection of
|
||||||
Just id ->
|
Just id ->
|
||||||
-- Recurse to select message id.
|
updateOpenMessage session newModel id
|
||||||
update session (ViewMessage id) newModel
|
|
||||||
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
( newModel, Cmd.none, Session.AddRecent model.mailboxName )
|
( newModel, Cmd.none, Session.AddRecent model.mailboxName )
|
||||||
@@ -188,25 +182,25 @@ update session msg model =
|
|||||||
_ ->
|
_ ->
|
||||||
( model, Cmd.none, Session.none )
|
( model, Cmd.none, Session.none )
|
||||||
|
|
||||||
ListResult (Err err) ->
|
ListLoaded (Err err) ->
|
||||||
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
||||||
|
|
||||||
MarkSeenResult (Ok _) ->
|
MarkedSeen (Ok _) ->
|
||||||
( model, Cmd.none, Session.none )
|
( model, Cmd.none, Session.none )
|
||||||
|
|
||||||
MarkSeenResult (Err err) ->
|
MarkedSeen (Err err) ->
|
||||||
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
||||||
|
|
||||||
MessageResult (Ok message) ->
|
MessageLoaded (Ok message) ->
|
||||||
updateMessageResult model message
|
updateMessageResult model message
|
||||||
|
|
||||||
MessageResult (Err err) ->
|
MessageLoaded (Err err) ->
|
||||||
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
||||||
|
|
||||||
MessageBody bodyMode ->
|
MessageBody bodyMode ->
|
||||||
( { model | bodyMode = bodyMode }, Cmd.none, Session.none )
|
( { model | bodyMode = bodyMode }, Cmd.none, Session.none )
|
||||||
|
|
||||||
SearchInput searchInput ->
|
OnSearchInput searchInput ->
|
||||||
updateSearchInput model searchInput
|
updateSearchInput model searchInput
|
||||||
|
|
||||||
OpenedTime time ->
|
OpenedTime time ->
|
||||||
@@ -216,13 +210,17 @@ update session msg model =
|
|||||||
( model, Cmd.none, Session.none )
|
( model, Cmd.none, Session.none )
|
||||||
|
|
||||||
else
|
else
|
||||||
-- Set delay before reporting message as seen to backend.
|
-- Set 1500ms delay before reporting message as seen to backend.
|
||||||
|
let
|
||||||
|
markSeenAt =
|
||||||
|
Time.posixToMillis time + 1500
|
||||||
|
in
|
||||||
( { model
|
( { model
|
||||||
| state =
|
| state =
|
||||||
ShowingList list
|
ShowingList list
|
||||||
(ShowingMessage
|
(ShowingMessage
|
||||||
{ visible
|
{ visible
|
||||||
| markSeenAt = Just (time + (1.5 * Time.second))
|
| markSeenAt = Just markSeenAt
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -233,21 +231,21 @@ update session msg model =
|
|||||||
_ ->
|
_ ->
|
||||||
( model, Cmd.none, Session.none )
|
( model, Cmd.none, Session.none )
|
||||||
|
|
||||||
Purge ->
|
PurgeMailbox ->
|
||||||
updatePurge model
|
updatePurge model
|
||||||
|
|
||||||
PurgeResult (Ok _) ->
|
PurgedMailbox (Ok _) ->
|
||||||
( model, Cmd.none, Session.none )
|
( model, Cmd.none, Session.none )
|
||||||
|
|
||||||
PurgeResult (Err err) ->
|
PurgedMailbox (Err err) ->
|
||||||
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
||||||
|
|
||||||
SeenTick now ->
|
MarkSeenTick now ->
|
||||||
case model.state of
|
case model.state of
|
||||||
ShowingList _ (ShowingMessage { message, markSeenAt }) ->
|
ShowingList _ (ShowingMessage { message, markSeenAt }) ->
|
||||||
case markSeenAt of
|
case markSeenAt of
|
||||||
Just deadline ->
|
Just deadline ->
|
||||||
if now >= deadline then
|
if Time.posixToMillis now >= deadline then
|
||||||
updateMarkMessageSeen model message
|
updateMarkMessageSeen model message
|
||||||
|
|
||||||
else
|
else
|
||||||
@@ -260,7 +258,7 @@ update session msg model =
|
|||||||
( model, Cmd.none, Session.none )
|
( model, Cmd.none, Session.none )
|
||||||
|
|
||||||
Tick now ->
|
Tick now ->
|
||||||
( { model | now = Date.fromTime now }, Cmd.none, Session.none )
|
( { model | now = now }, Cmd.none, Session.none )
|
||||||
|
|
||||||
|
|
||||||
{-| Replace the currently displayed message.
|
{-| Replace the currently displayed message.
|
||||||
@@ -298,8 +296,7 @@ updatePurge model =
|
|||||||
cmd =
|
cmd =
|
||||||
"/api/v1/mailbox/"
|
"/api/v1/mailbox/"
|
||||||
++ model.mailboxName
|
++ model.mailboxName
|
||||||
|> HttpUtil.delete
|
|> HttpUtil.delete PurgedMailbox
|
||||||
|> Http.send PurgeResult
|
|
||||||
in
|
in
|
||||||
case model.state of
|
case model.state of
|
||||||
ShowingList list _ ->
|
ShowingList list _ ->
|
||||||
@@ -371,8 +368,7 @@ updateDeleteMessage model message =
|
|||||||
"/api/v1/mailbox/" ++ message.mailbox ++ "/" ++ message.id
|
"/api/v1/mailbox/" ++ message.mailbox ++ "/" ++ message.id
|
||||||
|
|
||||||
cmd =
|
cmd =
|
||||||
HttpUtil.delete url
|
HttpUtil.delete DeletedMessage url
|
||||||
|> Http.send DeleteMessageResult
|
|
||||||
|
|
||||||
filter f messageList =
|
filter f messageList =
|
||||||
{ messageList | headers = List.filter f messageList.headers }
|
{ messageList | headers = List.filter f messageList.headers }
|
||||||
@@ -411,8 +407,7 @@ updateMarkMessageSeen model message =
|
|||||||
-- desired change in the body.
|
-- desired change in the body.
|
||||||
Encode.object [ ( "seen", Encode.bool True ) ]
|
Encode.object [ ( "seen", Encode.bool True ) ]
|
||||||
|> Http.jsonBody
|
|> Http.jsonBody
|
||||||
|> HttpUtil.patch url
|
|> HttpUtil.patch MarkedSeen url
|
||||||
|> Http.send MarkSeenResult
|
|
||||||
|
|
||||||
map f messageList =
|
map f messageList =
|
||||||
{ messageList | headers = List.map f messageList.headers }
|
{ messageList | headers = List.map f messageList.headers }
|
||||||
@@ -435,14 +430,24 @@ updateMarkMessageSeen model message =
|
|||||||
( model, Cmd.none, Session.none )
|
( model, Cmd.none, Session.none )
|
||||||
|
|
||||||
|
|
||||||
|
updateOpenMessage : Session -> Model -> String -> ( Model, Cmd Msg, Session.Msg )
|
||||||
|
updateOpenMessage session model id =
|
||||||
|
( updateSelected model id
|
||||||
|
, getMessage model.mailboxName id
|
||||||
|
, Session.AddRecent model.mailboxName
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
getList : String -> Cmd Msg
|
getList : String -> Cmd Msg
|
||||||
getList mailboxName =
|
getList mailboxName =
|
||||||
let
|
let
|
||||||
url =
|
url =
|
||||||
"/api/v1/mailbox/" ++ mailboxName
|
"/api/v1/mailbox/" ++ mailboxName
|
||||||
in
|
in
|
||||||
Http.get url (Decode.list MessageHeader.decoder)
|
Http.get
|
||||||
|> Http.send ListResult
|
{ url = url
|
||||||
|
, expect = Http.expectJson ListLoaded (Decode.list MessageHeader.decoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
getMessage : String -> MessageID -> Cmd Msg
|
getMessage : String -> MessageID -> Cmd Msg
|
||||||
@@ -451,37 +456,42 @@ getMessage mailboxName id =
|
|||||||
url =
|
url =
|
||||||
"/serve/m/" ++ mailboxName ++ "/" ++ id
|
"/serve/m/" ++ mailboxName ++ "/" ++ id
|
||||||
in
|
in
|
||||||
Http.get url Message.decoder
|
Http.get
|
||||||
|> Http.send MessageResult
|
{ url = url
|
||||||
|
, expect = Http.expectJson MessageLoaded Message.decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- VIEW
|
-- VIEW
|
||||||
|
|
||||||
|
|
||||||
view : Session -> Model -> Html Msg
|
view : Session -> Model -> { title : String, content : Html Msg }
|
||||||
view session model =
|
view session model =
|
||||||
div [ id "page", class "mailbox" ]
|
{ title = model.mailboxName ++ " - Inbucket"
|
||||||
[ viewMessageList session model
|
, content =
|
||||||
, main_
|
div [ id "page", class "mailbox" ]
|
||||||
[ id "message" ]
|
[ viewMessageList session model
|
||||||
[ case model.state of
|
, main_
|
||||||
ShowingList _ NoMessage ->
|
[ id "message" ]
|
||||||
text
|
[ case model.state of
|
||||||
("Select a message on the left,"
|
ShowingList _ NoMessage ->
|
||||||
++ " or enter a different username into the box on upper right."
|
text
|
||||||
)
|
("Select a message on the left,"
|
||||||
|
++ " or enter a different username into the box on upper right."
|
||||||
|
)
|
||||||
|
|
||||||
ShowingList _ (ShowingMessage { message }) ->
|
ShowingList _ (ShowingMessage { message }) ->
|
||||||
viewMessage message model.bodyMode
|
viewMessage message model.bodyMode
|
||||||
|
|
||||||
ShowingList _ (Transitioning { message }) ->
|
ShowingList _ (Transitioning { message }) ->
|
||||||
viewMessage message model.bodyMode
|
viewMessage message model.bodyMode
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
text ""
|
text ""
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
}
|
||||||
|
|
||||||
|
|
||||||
viewMessageList : Session -> Model -> Html Msg
|
viewMessageList : Session -> Model -> Html Msg
|
||||||
@@ -491,11 +501,11 @@ viewMessageList session model =
|
|||||||
[ input
|
[ input
|
||||||
[ type_ "search"
|
[ type_ "search"
|
||||||
, placeholder "search"
|
, placeholder "search"
|
||||||
, onInput SearchInput
|
, onInput OnSearchInput
|
||||||
, value model.searchInput
|
, value model.searchInput
|
||||||
]
|
]
|
||||||
[]
|
[]
|
||||||
, button [ onClick Purge ] [ text "Purge" ]
|
, button [ onClick PurgeMailbox ] [ text "Purge" ]
|
||||||
]
|
]
|
||||||
, case model.state of
|
, case model.state of
|
||||||
LoadingList _ ->
|
LoadingList _ ->
|
||||||
@@ -530,14 +540,14 @@ messageChip model selected message =
|
|||||||
viewMessage : Message -> Body -> Html Msg
|
viewMessage : Message -> Body -> Html Msg
|
||||||
viewMessage message bodyMode =
|
viewMessage message bodyMode =
|
||||||
let
|
let
|
||||||
sourceUrl message =
|
sourceUrl =
|
||||||
"/serve/m/" ++ message.mailbox ++ "/" ++ message.id ++ "/source"
|
"/serve/m/" ++ message.mailbox ++ "/" ++ message.id ++ "/source"
|
||||||
in
|
in
|
||||||
div []
|
div []
|
||||||
[ div [ class "button-bar" ]
|
[ div [ class "button-bar" ]
|
||||||
[ button [ class "danger", onClick (DeleteMessage message) ] [ text "Delete" ]
|
[ button [ class "danger", onClick (DeleteMessage message) ] [ text "Delete" ]
|
||||||
, a
|
, a
|
||||||
[ href (sourceUrl message), target "_blank" ]
|
[ href sourceUrl, target "_blank" ]
|
||||||
[ button [] [ text "Source" ] ]
|
[ button [] [ text "Source" ] ]
|
||||||
]
|
]
|
||||||
, dl [ id "message-header" ]
|
, dl [ id "message-header" ]
|
||||||
@@ -584,10 +594,10 @@ messageBody message bodyMode =
|
|||||||
, article [ class "message-body" ]
|
, article [ class "message-body" ]
|
||||||
[ case bodyMode of
|
[ case bodyMode of
|
||||||
SafeHtmlBody ->
|
SafeHtmlBody ->
|
||||||
div [ property "innerHTML" (Encode.string message.html) ] []
|
Html.node "rendered-html" [ property "content" (Encode.string message.html) ] []
|
||||||
|
|
||||||
TextBody ->
|
TextBody ->
|
||||||
div [ property "innerHTML" (Encode.string message.text) ] []
|
Html.node "rendered-html" [ property "content" (Encode.string message.text) ] []
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -616,34 +626,35 @@ attachmentRow baseUrl attach =
|
|||||||
[ a [ href url, target "_blank" ] [ text attach.fileName ]
|
[ a [ href url, target "_blank" ] [ text attach.fileName ]
|
||||||
, text (" (" ++ attach.contentType ++ ") ")
|
, text (" (" ++ attach.contentType ++ ") ")
|
||||||
]
|
]
|
||||||
, td [] [ a [ href url, downloadAs attach.fileName, class "button" ] [ text "Download" ] ]
|
, td [] [ a [ href url, download attach.fileName, class "button" ] [ text "Download" ] ]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
relativeDate : Model -> Date -> Html Msg
|
relativeDate : Model -> Posix -> Html Msg
|
||||||
relativeDate model date =
|
relativeDate model date =
|
||||||
Relative.relativeTime model.now date |> text
|
Relative.relativeTime model.now date |> text
|
||||||
|
|
||||||
|
|
||||||
verboseDate : Date -> Html Msg
|
verboseDate : Posix -> Html Msg
|
||||||
verboseDate date =
|
verboseDate date =
|
||||||
DateFormat.format
|
text <|
|
||||||
[ DateFormat.monthNameFull
|
DF.format
|
||||||
, DateFormat.text " "
|
[ DF.monthNameFull
|
||||||
, DateFormat.dayOfMonthSuffix
|
, DF.text " "
|
||||||
, DateFormat.text ", "
|
, DF.dayOfMonthSuffix
|
||||||
, DateFormat.yearNumber
|
, DF.text ", "
|
||||||
, DateFormat.text " "
|
, DF.yearNumber
|
||||||
, DateFormat.hourNumber
|
, DF.text " "
|
||||||
, DateFormat.text ":"
|
, DF.hourNumber
|
||||||
, DateFormat.minuteFixed
|
, DF.text ":"
|
||||||
, DateFormat.text ":"
|
, DF.minuteFixed
|
||||||
, DateFormat.secondFixed
|
, DF.text ":"
|
||||||
, DateFormat.text " "
|
, DF.secondFixed
|
||||||
, DateFormat.amPmUppercase
|
, DF.text " "
|
||||||
]
|
, DF.amPmUppercase
|
||||||
date
|
]
|
||||||
|> text
|
Time.utc
|
||||||
|
date
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+54
-63
@@ -2,22 +2,14 @@ module Page.Monitor exposing (Model, Msg, init, subscriptions, update, view)
|
|||||||
|
|
||||||
import Data.MessageHeader as MessageHeader exposing (MessageHeader)
|
import Data.MessageHeader as MessageHeader exposing (MessageHeader)
|
||||||
import Data.Session as Session exposing (Session)
|
import Data.Session as Session exposing (Session)
|
||||||
import Date exposing (Date)
|
import DateFormat as DF
|
||||||
import DateFormat
|
|
||||||
exposing
|
|
||||||
( amPmUppercase
|
|
||||||
, dayOfMonthFixed
|
|
||||||
, format
|
|
||||||
, hourNumber
|
|
||||||
, minuteFixed
|
|
||||||
, monthNameFirstThree
|
|
||||||
)
|
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (..)
|
import Html.Attributes exposing (..)
|
||||||
import Html.Events as Events
|
import Html.Events as Events
|
||||||
import Json.Decode as D
|
import Json.Decode as D
|
||||||
import Ports
|
import Ports
|
||||||
import Route
|
import Route
|
||||||
|
import Time exposing (Posix)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -30,14 +22,14 @@ type alias Model =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type MonitorMessage
|
||||||
|
= Connected Bool
|
||||||
|
| Message MessageHeader
|
||||||
|
|
||||||
|
|
||||||
init : ( Model, Cmd Msg )
|
init : ( Model, Cmd Msg )
|
||||||
init =
|
init =
|
||||||
( Model False []
|
( Model False [], Ports.monitorCommand True )
|
||||||
, Cmd.batch
|
|
||||||
[ Ports.windowTitle "Inbucket Monitor"
|
|
||||||
, Ports.monitorCommand True
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -55,7 +47,7 @@ subscriptions model =
|
|||||||
|> D.decodeValue
|
|> D.decodeValue
|
||||||
|> Ports.monitorMessage
|
|> Ports.monitorMessage
|
||||||
in
|
in
|
||||||
Sub.map MonitorResult monitorMessage
|
Sub.map MessageReceived monitorMessage
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -63,30 +55,25 @@ subscriptions model =
|
|||||||
|
|
||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= MonitorResult (Result String MonitorMessage)
|
= MessageReceived (Result D.Error MonitorMessage)
|
||||||
| OpenMessage MessageHeader
|
| OpenMessage MessageHeader
|
||||||
|
|
||||||
|
|
||||||
type MonitorMessage
|
|
||||||
= Connected Bool
|
|
||||||
| Message MessageHeader
|
|
||||||
|
|
||||||
|
|
||||||
update : Session -> Msg -> Model -> ( Model, Cmd Msg, Session.Msg )
|
update : Session -> Msg -> Model -> ( Model, Cmd Msg, Session.Msg )
|
||||||
update session msg model =
|
update session msg model =
|
||||||
case msg of
|
case msg of
|
||||||
MonitorResult (Ok (Connected status)) ->
|
MessageReceived (Ok (Connected status)) ->
|
||||||
( { model | connected = status }, Cmd.none, Session.none )
|
( { model | connected = status }, Cmd.none, Session.none )
|
||||||
|
|
||||||
MonitorResult (Ok (Message msg)) ->
|
MessageReceived (Ok (Message header)) ->
|
||||||
( { model | messages = msg :: model.messages }, Cmd.none, Session.none )
|
( { model | messages = header :: model.messages }, Cmd.none, Session.none )
|
||||||
|
|
||||||
MonitorResult (Err err) ->
|
MessageReceived (Err err) ->
|
||||||
( model, Cmd.none, Session.SetFlash err )
|
( model, Cmd.none, Session.SetFlash (D.errorToString err) )
|
||||||
|
|
||||||
OpenMessage msg ->
|
OpenMessage header ->
|
||||||
( model
|
( model
|
||||||
, Route.newUrl (Route.Message msg.mailbox msg.id)
|
, Route.newUrl session.key (Route.Message header.mailbox header.id)
|
||||||
, Session.none
|
, Session.none
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -95,32 +82,35 @@ update session msg model =
|
|||||||
-- VIEW
|
-- VIEW
|
||||||
|
|
||||||
|
|
||||||
view : Session -> Model -> Html Msg
|
view : Session -> Model -> { title : String, content : Html Msg }
|
||||||
view session model =
|
view session model =
|
||||||
div [ id "page" ]
|
{ title = "Inbucket Monitor"
|
||||||
[ h1 [] [ text "Monitor" ]
|
, content =
|
||||||
, p []
|
div [ id "page" ]
|
||||||
[ text "Messages will be listed here shortly after delivery. "
|
[ h1 [] [ text "Monitor" ]
|
||||||
, em []
|
, p []
|
||||||
[ text
|
[ text "Messages will be listed here shortly after delivery. "
|
||||||
(if model.connected then
|
, em []
|
||||||
"Connected."
|
[ text
|
||||||
|
(if model.connected then
|
||||||
|
"Connected."
|
||||||
|
|
||||||
else
|
else
|
||||||
"Disconnected!"
|
"Disconnected!"
|
||||||
)
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, table [ id "monitor" ]
|
||||||
|
[ thead []
|
||||||
|
[ th [] [ text "Date" ]
|
||||||
|
, th [ class "desktop" ] [ text "From" ]
|
||||||
|
, th [] [ text "Mailbox" ]
|
||||||
|
, th [] [ text "Subject" ]
|
||||||
|
]
|
||||||
|
, tbody [] (List.map viewMessage model.messages)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
, table [ id "monitor" ]
|
}
|
||||||
[ thead []
|
|
||||||
[ th [] [ text "Date" ]
|
|
||||||
, th [ class "desktop" ] [ text "From" ]
|
|
||||||
, th [] [ text "Mailbox" ]
|
|
||||||
, th [] [ text "Subject" ]
|
|
||||||
]
|
|
||||||
, tbody [] (List.map viewMessage model.messages)
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
viewMessage : MessageHeader -> Html Msg
|
viewMessage : MessageHeader -> Html Msg
|
||||||
@@ -133,18 +123,19 @@ viewMessage message =
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
shortDate : Date -> Html Msg
|
shortDate : Posix -> Html Msg
|
||||||
shortDate date =
|
shortDate date =
|
||||||
format
|
DF.format
|
||||||
[ dayOfMonthFixed
|
[ DF.dayOfMonthFixed
|
||||||
, DateFormat.text "-"
|
, DF.text "-"
|
||||||
, monthNameFirstThree
|
, DF.monthNameAbbreviated
|
||||||
, DateFormat.text " "
|
, DF.text " "
|
||||||
, hourNumber
|
, DF.hourNumber
|
||||||
, DateFormat.text ":"
|
, DF.text ":"
|
||||||
, minuteFixed
|
, DF.minuteFixed
|
||||||
, DateFormat.text " "
|
, DF.text " "
|
||||||
, amPmUppercase
|
, DF.amPmUppercase
|
||||||
]
|
]
|
||||||
|
Time.utc
|
||||||
date
|
date
|
||||||
|> text
|
|> text
|
||||||
|
|||||||
+70
-61
@@ -7,9 +7,9 @@ import Html exposing (..)
|
|||||||
import Html.Attributes exposing (..)
|
import Html.Attributes exposing (..)
|
||||||
import Http exposing (Error)
|
import Http exposing (Error)
|
||||||
import HttpUtil
|
import HttpUtil
|
||||||
import Sparkline exposing (DataSet, Point, Size, sparkline)
|
import Sparkline as Spark
|
||||||
import Svg.Attributes as SvgAttrib
|
import Svg.Attributes as SvgAttrib
|
||||||
import Time exposing (Time)
|
import Time exposing (Posix)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -40,8 +40,8 @@ type alias Metric =
|
|||||||
{ label : String
|
{ label : String
|
||||||
, value : Int
|
, value : Int
|
||||||
, formatter : Int -> String
|
, formatter : Int -> String
|
||||||
, graph : DataSet -> Html Msg
|
, graph : Spark.DataSet -> Html Msg
|
||||||
, history : DataSet
|
, history : Spark.DataSet
|
||||||
, minutes : Int
|
, minutes : Int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ init =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
initDataSet : DataSet
|
initDataSet : Spark.DataSet
|
||||||
initDataSet =
|
initDataSet =
|
||||||
List.range 0 59
|
List.range 0 59
|
||||||
|> List.map (\x -> ( toFloat x, 0 ))
|
|> List.map (\x -> ( toFloat x, 0 ))
|
||||||
@@ -84,7 +84,7 @@ load =
|
|||||||
|
|
||||||
subscriptions : Model -> Sub Msg
|
subscriptions : Model -> Sub Msg
|
||||||
subscriptions model =
|
subscriptions model =
|
||||||
Time.every (10 * Time.second) Tick
|
Time.every (10 * 1000) Tick
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -92,17 +92,17 @@ subscriptions model =
|
|||||||
|
|
||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= NewMetrics (Result Http.Error Metrics)
|
= MetricsReceived (Result Http.Error Metrics)
|
||||||
| Tick Time
|
| Tick Posix
|
||||||
|
|
||||||
|
|
||||||
update : Session -> Msg -> Model -> ( Model, Cmd Msg, Session.Msg )
|
update : Session -> Msg -> Model -> ( Model, Cmd Msg, Session.Msg )
|
||||||
update session msg model =
|
update session msg model =
|
||||||
case msg of
|
case msg of
|
||||||
NewMetrics (Ok metrics) ->
|
MetricsReceived (Ok metrics) ->
|
||||||
( updateMetrics metrics model, Cmd.none, Session.none )
|
( updateMetrics metrics model, Cmd.none, Session.none )
|
||||||
|
|
||||||
NewMetrics (Err err) ->
|
MetricsReceived (Err err) ->
|
||||||
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
||||||
|
|
||||||
Tick time ->
|
Tick time ->
|
||||||
@@ -207,46 +207,51 @@ updateRemoteTotal metric value history =
|
|||||||
|
|
||||||
getMetrics : Cmd Msg
|
getMetrics : Cmd Msg
|
||||||
getMetrics =
|
getMetrics =
|
||||||
Http.get "/debug/vars" Metrics.decoder
|
Http.get
|
||||||
|> Http.send NewMetrics
|
{ url = "/debug/vars"
|
||||||
|
, expect = Http.expectJson MetricsReceived Metrics.decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- VIEW --
|
-- VIEW --
|
||||||
|
|
||||||
|
|
||||||
view : Session -> Model -> Html Msg
|
view : Session -> Model -> { title : String, content : Html Msg }
|
||||||
view session model =
|
view session model =
|
||||||
div [ id "page" ]
|
{ title = "Inbucket Status"
|
||||||
[ h1 [] [ text "Status" ]
|
, content =
|
||||||
, case model.metrics of
|
div [ id "page" ]
|
||||||
Nothing ->
|
[ h1 [] [ text "Status" ]
|
||||||
div [] [ text "Loading metrics..." ]
|
, case model.metrics of
|
||||||
|
Nothing ->
|
||||||
|
div [] [ text "Loading metrics..." ]
|
||||||
|
|
||||||
Just metrics ->
|
Just metrics ->
|
||||||
div []
|
div []
|
||||||
[ framePanel "General Metrics"
|
[ framePanel "General Metrics"
|
||||||
[ viewMetric model.sysMem
|
[ viewMetric model.sysMem
|
||||||
, viewMetric model.heapSize
|
, viewMetric model.heapSize
|
||||||
, viewMetric model.heapUsed
|
, viewMetric model.heapUsed
|
||||||
, viewMetric model.heapObjects
|
, viewMetric model.heapObjects
|
||||||
, viewMetric model.goRoutines
|
, viewMetric model.goRoutines
|
||||||
, viewMetric model.webSockets
|
, viewMetric model.webSockets
|
||||||
|
]
|
||||||
|
, framePanel "SMTP Metrics"
|
||||||
|
[ viewMetric model.smtpConnOpen
|
||||||
|
, viewMetric model.smtpConnTotal
|
||||||
|
, viewMetric model.smtpReceivedTotal
|
||||||
|
, viewMetric model.smtpErrorsTotal
|
||||||
|
, viewMetric model.smtpWarnsTotal
|
||||||
|
]
|
||||||
|
, framePanel "Storage Metrics"
|
||||||
|
[ viewMetric model.retentionDeletesTotal
|
||||||
|
, viewMetric model.retainedCount
|
||||||
|
, viewMetric model.retainedSize
|
||||||
|
]
|
||||||
]
|
]
|
||||||
, framePanel "SMTP Metrics"
|
]
|
||||||
[ viewMetric model.smtpConnOpen
|
}
|
||||||
, viewMetric model.smtpConnTotal
|
|
||||||
, viewMetric model.smtpReceivedTotal
|
|
||||||
, viewMetric model.smtpErrorsTotal
|
|
||||||
, viewMetric model.smtpWarnsTotal
|
|
||||||
]
|
|
||||||
, framePanel "Storage Metrics"
|
|
||||||
[ viewMetric model.retentionDeletesTotal
|
|
||||||
, viewMetric model.retainedCount
|
|
||||||
, viewMetric model.retainedSize
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
viewMetric : Metric -> Html Msg
|
viewMetric : Metric -> Html Msg
|
||||||
@@ -256,7 +261,7 @@ viewMetric metric =
|
|||||||
, div [ class "value" ] [ text (metric.formatter metric.value) ]
|
, div [ class "value" ] [ text (metric.formatter metric.value) ]
|
||||||
, div [ class "graph" ]
|
, div [ class "graph" ]
|
||||||
[ metric.graph metric.history
|
[ metric.graph metric.history
|
||||||
, text ("(" ++ toString metric.minutes ++ "min)")
|
, text ("(" ++ String.fromInt metric.minutes ++ "min)")
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -278,30 +283,34 @@ graphNull =
|
|||||||
div [] []
|
div [] []
|
||||||
|
|
||||||
|
|
||||||
graphSize : Size
|
graphSize : Spark.Size
|
||||||
graphSize =
|
graphSize =
|
||||||
( 180, 16, 0, 0 )
|
{ width = 180
|
||||||
|
, height = 16
|
||||||
|
, marginLR = 0
|
||||||
|
, marginTB = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
areaStyle : Sparkline.Param a -> Sparkline.Param a
|
areaStyle : Spark.Param a -> Spark.Param a
|
||||||
areaStyle =
|
areaStyle =
|
||||||
Sparkline.Style
|
Spark.Style
|
||||||
[ SvgAttrib.fill "rgba(50,100,255,0.3)"
|
[ SvgAttrib.fill "rgba(50,100,255,0.3)"
|
||||||
, SvgAttrib.stroke "rgba(50,100,255,1.0)"
|
, SvgAttrib.stroke "rgba(50,100,255,1.0)"
|
||||||
, SvgAttrib.strokeWidth "1.0"
|
, SvgAttrib.strokeWidth "1.0"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
barStyle : Sparkline.Param a -> Sparkline.Param a
|
barStyle : Spark.Param a -> Spark.Param a
|
||||||
barStyle =
|
barStyle =
|
||||||
Sparkline.Style
|
Spark.Style
|
||||||
[ SvgAttrib.fill "rgba(50,200,50,0.7)"
|
[ SvgAttrib.fill "rgba(50,200,50,0.7)"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
zeroStyle : Sparkline.Param a -> Sparkline.Param a
|
zeroStyle : Spark.Param a -> Spark.Param a
|
||||||
zeroStyle =
|
zeroStyle =
|
||||||
Sparkline.Style
|
Spark.Style
|
||||||
[ SvgAttrib.stroke "rgba(0,0,0,0.2)"
|
[ SvgAttrib.stroke "rgba(0,0,0,0.2)"
|
||||||
, SvgAttrib.strokeWidth "1.0"
|
, SvgAttrib.strokeWidth "1.0"
|
||||||
]
|
]
|
||||||
@@ -309,7 +318,7 @@ zeroStyle =
|
|||||||
|
|
||||||
{-| Bar graph to be used with updateRemoteTotal metrics (change instead of absolute values).
|
{-| Bar graph to be used with updateRemoteTotal metrics (change instead of absolute values).
|
||||||
-}
|
-}
|
||||||
graphChange : DataSet -> Html a
|
graphChange : Spark.DataSet -> Html a
|
||||||
graphChange data =
|
graphChange data =
|
||||||
let
|
let
|
||||||
-- Used with Domain to stop sparkline forgetting about zero; continue scrolling graph.
|
-- Used with Domain to stop sparkline forgetting about zero; continue scrolling graph.
|
||||||
@@ -321,16 +330,16 @@ graphChange data =
|
|||||||
Just point ->
|
Just point ->
|
||||||
Tuple.first point
|
Tuple.first point
|
||||||
in
|
in
|
||||||
sparkline graphSize
|
Spark.sparkline graphSize
|
||||||
[ Sparkline.Bar 2.5 data |> barStyle
|
[ Spark.Bar 2.5 data |> barStyle
|
||||||
, Sparkline.ZeroLine |> zeroStyle
|
, Spark.ZeroLine |> zeroStyle
|
||||||
, Sparkline.Domain [ ( x, 0 ), ( x, 1 ) ]
|
, Spark.Domain [ ( x, 0 ), ( x, 1 ) ]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
{-| Zero based area graph, for charting absolute values relative to 0.
|
{-| Zero based area graph, for charting absolute values relative to 0.
|
||||||
-}
|
-}
|
||||||
graphZero : DataSet -> Html a
|
graphZero : Spark.DataSet -> Html a
|
||||||
graphZero data =
|
graphZero data =
|
||||||
let
|
let
|
||||||
-- Used with Domain to stop sparkline forgetting about zero; continue scrolling graph.
|
-- Used with Domain to stop sparkline forgetting about zero; continue scrolling graph.
|
||||||
@@ -342,10 +351,10 @@ graphZero data =
|
|||||||
Just point ->
|
Just point ->
|
||||||
Tuple.first point
|
Tuple.first point
|
||||||
in
|
in
|
||||||
sparkline graphSize
|
Spark.sparkline graphSize
|
||||||
[ Sparkline.Area data |> areaStyle
|
[ Spark.Area data |> areaStyle
|
||||||
, Sparkline.ZeroLine |> zeroStyle
|
, Spark.ZeroLine |> zeroStyle
|
||||||
, Sparkline.Domain [ ( x, 0 ), ( x, 1 ) ]
|
, Spark.Domain [ ( x, 0 ), ( x, 1 ) ]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -400,4 +409,4 @@ fmtInt n =
|
|||||||
else
|
else
|
||||||
thousands (String.slice 0 -3 str) ++ "," ++ String.right 3 str
|
thousands (String.slice 0 -3 str) ++ "," ++ String.right 3 str
|
||||||
in
|
in
|
||||||
thousands (toString n)
|
thousands (String.fromInt n)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ port module Ports exposing
|
|||||||
, monitorMessage
|
, monitorMessage
|
||||||
, onSessionChange
|
, onSessionChange
|
||||||
, storeSession
|
, storeSession
|
||||||
, windowTitle
|
|
||||||
)
|
)
|
||||||
|
|
||||||
import Data.Session exposing (Persistent)
|
import Data.Session exposing (Persistent)
|
||||||
@@ -20,6 +19,3 @@ port onSessionChange : (Value -> msg) -> Sub msg
|
|||||||
|
|
||||||
|
|
||||||
port storeSession : Persistent -> Cmd msg
|
port storeSession : Persistent -> Cmd msg
|
||||||
|
|
||||||
|
|
||||||
port windowTitle : String -> Cmd msg
|
|
||||||
|
|||||||
+34
-32
@@ -1,9 +1,10 @@
|
|||||||
module Route exposing (Route(..), fromLocation, href, modifyUrl, newUrl)
|
module Route exposing (Route(..), fromUrl, href, modifyUrl, newUrl)
|
||||||
|
|
||||||
|
import Browser.Navigation as Navigation exposing (Key)
|
||||||
import Html exposing (Attribute)
|
import Html exposing (Attribute)
|
||||||
import Html.Attributes as Attr
|
import Html.Attributes as Attr
|
||||||
import Navigation exposing (Location)
|
import Url exposing (Url)
|
||||||
import UrlParser as Url exposing ((</>), Parser, parseHash, s, string)
|
import Url.Parser as Parser exposing ((</>), Parser, map, oneOf, s, string, top)
|
||||||
|
|
||||||
|
|
||||||
type Route
|
type Route
|
||||||
@@ -15,17 +16,20 @@ type Route
|
|||||||
| Status
|
| Status
|
||||||
|
|
||||||
|
|
||||||
matcher : Parser (Route -> a) a
|
{-| Routes our application handles.
|
||||||
matcher =
|
-}
|
||||||
Url.oneOf
|
routes : List (Parser (Route -> a) a)
|
||||||
[ Url.map Home (s "")
|
routes =
|
||||||
, Url.map Message (s "m" </> string </> string)
|
[ map Home top
|
||||||
, Url.map Mailbox (s "m" </> string)
|
, map Message (s "m" </> string </> string)
|
||||||
, Url.map Monitor (s "monitor")
|
, map Mailbox (s "m" </> string)
|
||||||
, Url.map Status (s "status")
|
, map Monitor (s "monitor")
|
||||||
]
|
, map Status (s "status")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Convert route to a URI.
|
||||||
|
-}
|
||||||
routeToString : Route -> String
|
routeToString : Route -> String
|
||||||
routeToString page =
|
routeToString page =
|
||||||
let
|
let
|
||||||
@@ -49,37 +53,35 @@ routeToString page =
|
|||||||
Status ->
|
Status ->
|
||||||
[ "status" ]
|
[ "status" ]
|
||||||
in
|
in
|
||||||
"/#/" ++ String.join "/" pieces
|
"/" ++ String.join "/" pieces
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- PUBLIC HELPERS
|
-- PUBLIC HELPERS
|
||||||
|
|
||||||
|
|
||||||
href : Route -> Attribute msg
|
href : Key -> Route -> Attribute msg
|
||||||
href route =
|
href key route =
|
||||||
Attr.href (routeToString route)
|
Attr.href (routeToString route)
|
||||||
|
|
||||||
|
|
||||||
modifyUrl : Route -> Cmd msg
|
modifyUrl : Key -> Route -> Cmd msg
|
||||||
modifyUrl =
|
modifyUrl key =
|
||||||
routeToString >> Navigation.modifyUrl
|
routeToString >> Navigation.replaceUrl key
|
||||||
|
|
||||||
|
|
||||||
newUrl : Route -> Cmd msg
|
newUrl : Key -> Route -> Cmd msg
|
||||||
newUrl =
|
newUrl key =
|
||||||
routeToString >> Navigation.newUrl
|
routeToString >> Navigation.pushUrl key
|
||||||
|
|
||||||
|
|
||||||
fromLocation : Location -> Route
|
{-| Returns the Route for a given URL.
|
||||||
fromLocation location =
|
-}
|
||||||
if String.isEmpty location.hash then
|
fromUrl : Url -> Route
|
||||||
Home
|
fromUrl location =
|
||||||
|
case Parser.parse (oneOf routes) location of
|
||||||
|
Nothing ->
|
||||||
|
Unknown location.path
|
||||||
|
|
||||||
else
|
Just route ->
|
||||||
case parseHash matcher location of
|
route
|
||||||
Nothing ->
|
|
||||||
Unknown location.hash
|
|
||||||
|
|
||||||
Just route ->
|
|
||||||
route
|
|
||||||
|
|||||||
+26
-18
@@ -10,7 +10,9 @@ import Html.Attributes
|
|||||||
, href
|
, href
|
||||||
, id
|
, id
|
||||||
, placeholder
|
, placeholder
|
||||||
|
, rel
|
||||||
, selected
|
, selected
|
||||||
|
, target
|
||||||
, type_
|
, type_
|
||||||
, value
|
, value
|
||||||
)
|
)
|
||||||
@@ -39,10 +41,11 @@ frame controls session page content =
|
|||||||
div [ id "app" ]
|
div [ id "app" ]
|
||||||
[ header []
|
[ header []
|
||||||
[ ul [ class "navbar", attribute "role" "navigation" ]
|
[ ul [ class "navbar", attribute "role" "navigation" ]
|
||||||
[ li [ id "navbar-brand" ] [ a [ Route.href Route.Home ] [ text "@ inbucket" ] ]
|
[ li [ id "navbar-brand" ]
|
||||||
, navbarLink page Route.Monitor [ text "Monitor" ]
|
[ a [ Route.href session.key Route.Home ] [ text "@ inbucket" ] ]
|
||||||
, navbarLink page Route.Status [ text "Status" ]
|
, navbarLink session page Route.Monitor [ text "Monitor" ]
|
||||||
, navbarRecent page controls
|
, navbarLink session page Route.Status [ text "Status" ]
|
||||||
|
, navbarRecent session page controls
|
||||||
, li [ id "navbar-mailbox" ]
|
, li [ id "navbar-mailbox" ]
|
||||||
[ form [ Events.onSubmit (controls.viewMailbox controls.mailboxValue) ]
|
[ form [ Events.onSubmit (controls.viewMailbox controls.mailboxValue) ]
|
||||||
[ input
|
[ input
|
||||||
@@ -61,33 +64,35 @@ frame controls session page content =
|
|||||||
, content
|
, content
|
||||||
, footer []
|
, footer []
|
||||||
[ div [ id "footer" ]
|
[ div [ id "footer" ]
|
||||||
[ a [ href "https://www.inbucket.org" ] [ text "Inbucket" ]
|
[ externalLink "https://www.inbucket.org" "Inbucket"
|
||||||
, text " is an open source projected hosted at "
|
, text " is an open source projected hosted at "
|
||||||
, a [ href "https://github.com/jhillyerd/inbucket" ] [ text "GitHub" ]
|
, externalLink "https://github.com/jhillyerd/inbucket" "GitHub"
|
||||||
, text "."
|
, text "."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
navbarLink : ActivePage -> Route -> List (Html a) -> Html a
|
externalLink : String -> String -> Html a
|
||||||
navbarLink page route linkContent =
|
externalLink url title =
|
||||||
|
a [ href url, target "_blank", rel "noopener" ] [ text title ]
|
||||||
|
|
||||||
|
|
||||||
|
navbarLink : Session -> ActivePage -> Route -> List (Html a) -> Html a
|
||||||
|
navbarLink session page route linkContent =
|
||||||
li [ classList [ ( "navbar-active", isActive page route ) ] ]
|
li [ classList [ ( "navbar-active", isActive page route ) ] ]
|
||||||
[ a [ Route.href route ] linkContent ]
|
[ a [ Route.href session.key route ] linkContent ]
|
||||||
|
|
||||||
|
|
||||||
{-| Renders list of recent mailboxes, selecting the currently active mailbox.
|
{-| Renders list of recent mailboxes, selecting the currently active mailbox.
|
||||||
-}
|
-}
|
||||||
navbarRecent : ActivePage -> FrameControls msg -> Html msg
|
navbarRecent : Session -> ActivePage -> FrameControls msg -> Html msg
|
||||||
navbarRecent page controls =
|
navbarRecent session page controls =
|
||||||
let
|
let
|
||||||
recentItemLink mailbox =
|
|
||||||
a [ Route.href (Route.Mailbox mailbox) ] [ text mailbox ]
|
|
||||||
|
|
||||||
active =
|
active =
|
||||||
page == Mailbox
|
page == Mailbox
|
||||||
|
|
||||||
-- Navbar tab title, is current mailbox when active.
|
-- Recent tab title is the name of the current mailbox when active.
|
||||||
title =
|
title =
|
||||||
if active then
|
if active then
|
||||||
controls.recentActive
|
controls.recentActive
|
||||||
@@ -95,20 +100,23 @@ navbarRecent page controls =
|
|||||||
else
|
else
|
||||||
"Recent Mailboxes"
|
"Recent Mailboxes"
|
||||||
|
|
||||||
-- Items to show in recent list, doesn't include active mailbox.
|
-- Mailboxes to show in recent list, doesn't include active mailbox.
|
||||||
items =
|
recentMailboxes =
|
||||||
if active then
|
if active then
|
||||||
List.tail controls.recentOptions |> Maybe.withDefault []
|
List.tail controls.recentOptions |> Maybe.withDefault []
|
||||||
|
|
||||||
else
|
else
|
||||||
controls.recentOptions
|
controls.recentOptions
|
||||||
|
|
||||||
|
recentLink mailbox =
|
||||||
|
a [ Route.href session.key (Route.Mailbox mailbox) ] [ text mailbox ]
|
||||||
in
|
in
|
||||||
li
|
li
|
||||||
[ id "navbar-recent"
|
[ id "navbar-recent"
|
||||||
, classList [ ( "navbar-dropdown", True ), ( "navbar-active", active ) ]
|
, classList [ ( "navbar-dropdown", True ), ( "navbar-active", active ) ]
|
||||||
]
|
]
|
||||||
[ span [] [ text title ]
|
[ span [] [ text title ]
|
||||||
, div [ class "navbar-dropdown-content" ] (List.map recentItemLink items)
|
, div [ class "navbar-dropdown-content" ] (List.map recentLink recentMailboxes)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+6
-10
@@ -1,10 +1,13 @@
|
|||||||
import './main.css'
|
import './main.css'
|
||||||
import { Main } from './Main.elm'
|
import { Elm } from './Main.elm'
|
||||||
import registerServiceWorker from './registerServiceWorker'
|
|
||||||
import registerMonitorPorts from './registerMonitor'
|
import registerMonitorPorts from './registerMonitor'
|
||||||
|
import './renderedHtml'
|
||||||
|
|
||||||
// App startup.
|
// App startup.
|
||||||
var app = Main.embed(document.getElementById('root'), sessionObject())
|
var app = Elm.Main.init({
|
||||||
|
node: document.getElementById('root'),
|
||||||
|
flags: sessionObject()
|
||||||
|
})
|
||||||
|
|
||||||
// Message monitor.
|
// Message monitor.
|
||||||
registerMonitorPorts(app)
|
registerMonitorPorts(app)
|
||||||
@@ -31,10 +34,3 @@ function sessionObject() {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window title.
|
|
||||||
app.ports.windowTitle.subscribe(function (title) {
|
|
||||||
document.title = title
|
|
||||||
})
|
|
||||||
|
|
||||||
registerServiceWorker()
|
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
// In production, we register a service worker to serve assets from local cache.
|
|
||||||
|
|
||||||
// This lets the app load faster on subsequent visits in production, and gives
|
|
||||||
// it offline capabilities. However, it also means that developers (and users)
|
|
||||||
// will only see deployed updates on the "N+1" visit to a page, since previously
|
|
||||||
// cached resources are updated in the background.
|
|
||||||
|
|
||||||
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
|
|
||||||
// This link also includes instructions on opting out of this behavior.
|
|
||||||
|
|
||||||
const isLocalhost = Boolean(
|
|
||||||
window.location.hostname === 'localhost' ||
|
|
||||||
// [::1] is the IPv6 localhost address.
|
|
||||||
window.location.hostname === '[::1]' ||
|
|
||||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
|
||||||
window.location.hostname.match(
|
|
||||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function register() {
|
|
||||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
|
||||||
// The URL constructor is available in all browsers that support SW.
|
|
||||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
|
|
||||||
if (publicUrl.origin !== window.location.origin) {
|
|
||||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
|
||||||
// from what our page is served on. This might happen if a CDN is used to
|
|
||||||
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
|
||||||
|
|
||||||
if (!isLocalhost) {
|
|
||||||
// Is not local host. Just register service worker
|
|
||||||
registerValidSW(swUrl);
|
|
||||||
} else {
|
|
||||||
// This is running on localhost. Lets check if a service worker still exists or not.
|
|
||||||
checkValidServiceWorker(swUrl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerValidSW(swUrl) {
|
|
||||||
navigator.serviceWorker
|
|
||||||
.register(swUrl)
|
|
||||||
.then(registration => {
|
|
||||||
registration.onupdatefound = () => {
|
|
||||||
const installingWorker = registration.installing;
|
|
||||||
installingWorker.onstatechange = () => {
|
|
||||||
if (installingWorker.state === 'installed') {
|
|
||||||
if (navigator.serviceWorker.controller) {
|
|
||||||
// At this point, the old content will have been purged and
|
|
||||||
// the fresh content will have been added to the cache.
|
|
||||||
// It's the perfect time to display a "New content is
|
|
||||||
// available; please refresh." message in your web app.
|
|
||||||
console.log('New content is available; please refresh.');
|
|
||||||
} else {
|
|
||||||
// At this point, everything has been precached.
|
|
||||||
// It's the perfect time to display a
|
|
||||||
// "Content is cached for offline use." message.
|
|
||||||
console.log('Content is cached for offline use.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error during service worker registration:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkValidServiceWorker(swUrl) {
|
|
||||||
// Check if the service worker can be found. If it can't reload the page.
|
|
||||||
fetch(swUrl)
|
|
||||||
.then(response => {
|
|
||||||
// Ensure service worker exists, and that we really are getting a JS file.
|
|
||||||
if (
|
|
||||||
response.status === 404 ||
|
|
||||||
response.headers.get('content-type').indexOf('javascript') === -1
|
|
||||||
) {
|
|
||||||
// No service worker found. Probably a different app. Reload the page.
|
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
|
||||||
registration.unregister().then(() => {
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Service worker found. Proceed as normal.
|
|
||||||
registerValidSW(swUrl);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
console.log(
|
|
||||||
'No internet connection found. App is running in offline mode.'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unregister() {
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
|
||||||
registration.unregister();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// This element allows Inbucket to draw server rendered HTML, aka HTML email.
|
||||||
|
// https://leveljournal.com/server-rendered-html-in-elm
|
||||||
|
|
||||||
|
customElements.define(
|
||||||
|
"rendered-html",
|
||||||
|
class RenderedHtml extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
this._content = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
set content(value) {
|
||||||
|
if (this._content === value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this._content = value
|
||||||
|
this.innerHTML = value
|
||||||
|
}
|
||||||
|
|
||||||
|
get content() {
|
||||||
|
return this._content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||||
|
const webpack = require('webpack')
|
||||||
|
|
||||||
|
module.exports = (env, argv) => {
|
||||||
|
const production = argv.mode === 'production'
|
||||||
|
const config = {
|
||||||
|
output: {
|
||||||
|
filename: 'static/[name].js',
|
||||||
|
publicPath: '/',
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: [/elm-stuff/, /node_modules/],
|
||||||
|
loader: 'babel-loader',
|
||||||
|
query: {
|
||||||
|
presets: [
|
||||||
|
'@babel/preset-env',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.elm$/,
|
||||||
|
exclude: [/elm-stuff/, /node_modules/],
|
||||||
|
use: [
|
||||||
|
{ loader: 'elm-hot-webpack-loader' },
|
||||||
|
{
|
||||||
|
loader: 'elm-webpack-loader',
|
||||||
|
options: {
|
||||||
|
debug: !production,
|
||||||
|
optimize: production,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
exclude: [/node_modules/],
|
||||||
|
loader: ['style-loader', 'css-loader'],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: 'public/index.html',
|
||||||
|
favicon: 'public/favicon.png',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
devServer: {
|
||||||
|
inline: true,
|
||||||
|
historyApiFallback: true,
|
||||||
|
stats: { colors: true },
|
||||||
|
overlay: true,
|
||||||
|
open: true,
|
||||||
|
proxy: [{
|
||||||
|
context: ['/api', '/debug', '/serve'],
|
||||||
|
target: 'http://localhost:9000',
|
||||||
|
ws: true,
|
||||||
|
}],
|
||||||
|
watchOptions: {
|
||||||
|
ignored: /node_modules/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (argv.hot) {
|
||||||
|
config.plugins.push(new webpack.HotModuleReplacementPlugin())
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user