mirror of
https://github.com/jhillyerd/inbucket.git
synced 2026-08-01 08:39:50 +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.
|
||||
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]
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ export INBUCKET_LOGLEVEL="debug"
|
||||
export INBUCKET_SMTP_DISCARDDOMAINS="bitbucket.local"
|
||||
export INBUCKET_WEB_TEMPLATECACHE="false"
|
||||
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_PARAMS="path:/tmp/inbucket"
|
||||
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))
|
||||
for i, msg := range messages {
|
||||
jmessages[i] = &model.JSONMessageHeaderV1{
|
||||
Mailbox: name,
|
||||
ID: msg.ID,
|
||||
From: stringutil.StringAddress(msg.From),
|
||||
To: stringutil.StringAddressList(msg.To),
|
||||
Subject: msg.Subject,
|
||||
Date: msg.Date,
|
||||
Size: msg.Size,
|
||||
Seen: msg.Seen,
|
||||
Mailbox: name,
|
||||
ID: msg.ID,
|
||||
From: stringutil.StringAddress(msg.From),
|
||||
To: stringutil.StringAddressList(msg.To),
|
||||
Subject: msg.Subject,
|
||||
Date: msg.Date,
|
||||
PosixMillis: msg.Date.UnixNano() / 1000000,
|
||||
Size: msg.Size,
|
||||
Seen: msg.Seen,
|
||||
}
|
||||
}
|
||||
return web.RenderJSON(w, jmessages)
|
||||
@@ -77,15 +78,16 @@ func MailboxShowV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) (
|
||||
}
|
||||
return web.RenderJSON(w,
|
||||
&model.JSONMessageV1{
|
||||
Mailbox: name,
|
||||
ID: msg.ID,
|
||||
From: stringutil.StringAddress(msg.From),
|
||||
To: stringutil.StringAddressList(msg.To),
|
||||
Subject: msg.Subject,
|
||||
Date: msg.Date,
|
||||
Size: msg.Size,
|
||||
Seen: msg.Seen,
|
||||
Header: msg.Header(),
|
||||
Mailbox: name,
|
||||
ID: msg.ID,
|
||||
From: stringutil.StringAddress(msg.From),
|
||||
To: stringutil.StringAddressList(msg.To),
|
||||
Subject: msg.Subject,
|
||||
Date: msg.Date,
|
||||
PosixMillis: msg.Date.UnixNano() / 1000000,
|
||||
Size: msg.Size,
|
||||
Seen: msg.Seen,
|
||||
Header: msg.Header(),
|
||||
Body: &model.JSONMessageBodyV1{
|
||||
Text: msg.Text(),
|
||||
HTML: msg.HTML(),
|
||||
|
||||
@@ -112,6 +112,7 @@ func TestRestMailboxList(t *testing.T) {
|
||||
decodedStringEquals(t, result, "[0]/to/[0]", "<to1@host>")
|
||||
decodedStringEquals(t, result, "[0]/subject", "subject 1")
|
||||
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)
|
||||
decodedBoolEquals(t, result, "[0]/seen", false)
|
||||
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]/subject", "subject 2")
|
||||
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)
|
||||
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, "subject", "subject 1")
|
||||
decodedStringEquals(t, result, "date", "2012-02-01T10:11:12.000000253-08:00")
|
||||
decodedNumberEquals(t, result, "posix-millis", 1328119872000)
|
||||
decodedNumberEquals(t, result, "size", 0)
|
||||
decodedBoolEquals(t, result, "seen", true)
|
||||
decodedStringEquals(t, result, "body/text", "This is some text")
|
||||
|
||||
@@ -6,14 +6,15 @@ import (
|
||||
|
||||
// JSONMessageHeaderV1 contains the basic header data for a message
|
||||
type JSONMessageHeaderV1 struct {
|
||||
Mailbox string `json:"mailbox"`
|
||||
ID string `json:"id"`
|
||||
From string `json:"from"`
|
||||
To []string `json:"to"`
|
||||
Subject string `json:"subject"`
|
||||
Date time.Time `json:"date"`
|
||||
Size int64 `json:"size"`
|
||||
Seen bool `json:"seen"`
|
||||
Mailbox string `json:"mailbox"`
|
||||
ID string `json:"id"`
|
||||
From string `json:"from"`
|
||||
To []string `json:"to"`
|
||||
Subject string `json:"subject"`
|
||||
Date time.Time `json:"date"`
|
||||
PosixMillis int64 `json:"posix-millis"`
|
||||
Size int64 `json:"size"`
|
||||
Seen bool `json:"seen"`
|
||||
}
|
||||
|
||||
// JSONMessageV1 contains the same data as the header plus a JSONMessageBody
|
||||
@@ -24,6 +25,7 @@ type JSONMessageV1 struct {
|
||||
To []string `json:"to"`
|
||||
Subject string `json:"subject"`
|
||||
Date time.Time `json:"date"`
|
||||
PosixMillis int64 `json:"posix-millis"`
|
||||
Size int64 `json:"size"`
|
||||
Seen bool `json:"seen"`
|
||||
Body *JSONMessageBodyV1 `json:"body"`
|
||||
|
||||
@@ -110,13 +110,14 @@ func (ml *msgListener) WSWriter(conn *websocket.Conn) {
|
||||
return
|
||||
}
|
||||
header := &model.JSONMessageHeaderV1{
|
||||
Mailbox: msg.Mailbox,
|
||||
ID: msg.ID,
|
||||
From: msg.From,
|
||||
To: msg.To,
|
||||
Subject: msg.Subject,
|
||||
Date: msg.Date,
|
||||
Size: msg.Size,
|
||||
Mailbox: msg.Mailbox,
|
||||
ID: msg.ID,
|
||||
From: msg.From,
|
||||
To: msg.To,
|
||||
Subject: msg.Subject,
|
||||
Date: msg.Date,
|
||||
PosixMillis: msg.Date.UnixNano() / 1000000,
|
||||
Size: msg.Size,
|
||||
}
|
||||
if conn.WriteJSON(header) != nil {
|
||||
// Write failed
|
||||
|
||||
@@ -79,12 +79,14 @@ func decodedNumberEquals(t *testing.T, json interface{}, path string, want float
|
||||
t.Errorf("JSON result%s", msg)
|
||||
return
|
||||
}
|
||||
if got, ok := val.(float64); ok {
|
||||
got, ok := val.(float64)
|
||||
if ok {
|
||||
if got == want {
|
||||
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) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"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,
|
||||
// returning specified statusCode to the client.
|
||||
func noMatchHandler(statusCode int, message string) http.Handler {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -47,7 +48,7 @@ func init() {
|
||||
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(
|
||||
conf *config.Root,
|
||||
shutdownChan chan bool,
|
||||
@@ -57,11 +58,11 @@ func Initialize(
|
||||
rootConfig = conf
|
||||
globalShutdown = shutdownChan
|
||||
|
||||
// NewContext() will use this DataStore for the web handlers
|
||||
// NewContext() will use this DataStore for the web handlers.
|
||||
msgHub = mh
|
||||
manager = mm
|
||||
|
||||
// Content Paths
|
||||
// Dynamic paths.
|
||||
log.Info().Str("module", "web").Str("phase", "startup").Str("path", conf.Web.UIDir).
|
||||
Msg("Web UI content mapped")
|
||||
Router.Handle("/debug/vars", expvar.Handler())
|
||||
@@ -74,13 +75,27 @@ func Initialize(
|
||||
log.Warn().Str("module", "web").Str("phase", "startup").
|
||||
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 == "" {
|
||||
log.Info().Str("module", "web").Str("phase", "startup").
|
||||
Msg("Generating random cookie.auth.key")
|
||||
|
||||
@@ -23,6 +23,7 @@ type JSONMessage struct {
|
||||
To []string `json:"to"`
|
||||
Subject string `json:"subject"`
|
||||
Date time.Time `json:"date"`
|
||||
PosixMillis int64 `json:"posix-millis"`
|
||||
Size int64 `json:"size"`
|
||||
Seen bool `json:"seen"`
|
||||
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),
|
||||
Subject: msg.Subject,
|
||||
Date: msg.Date,
|
||||
PosixMillis: msg.Date.UnixNano() / 1000000,
|
||||
Size: msg.Size,
|
||||
Seen: msg.Seen,
|
||||
Header: msg.Header(),
|
||||
|
||||
+15
-16
@@ -1,9 +1,7 @@
|
||||
# Inbucket 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.*
|
||||
|
||||
The UI was bootstrapped with [Create Elm App].
|
||||
It is written in [Elm] 0.19, a *delightful language for reliable webapps.*
|
||||
|
||||
## Development
|
||||
|
||||
@@ -11,15 +9,18 @@ With `$INBUCKET` as the root of the git repository.
|
||||
|
||||
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
|
||||
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
|
||||
make
|
||||
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
|
||||
cycle should favor the development server below.
|
||||
|
||||
In terminal 2 (elm-app development server):
|
||||
### Terminal 2: webpack development server
|
||||
|
||||
```
|
||||
cd $INBUCKET/ui
|
||||
elm-app start
|
||||
npm run dev
|
||||
```
|
||||
|
||||
[Create Elm App] will start a development HTTP server listening on port 3000.
|
||||
You should use this server for UI development, as it features hot reload and the
|
||||
Elm debugger.
|
||||
npm will start a development HTTP server listening on port 3000. You should use
|
||||
this server for UI development, as it features hot reload and the Elm debugger.
|
||||
|
||||
[Create Elm App]: https://github.com/halfzebra/create-elm-app
|
||||
[Elm]: https://elm-lang.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>
|
||||
<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">
|
||||
<!--
|
||||
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>
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"short_name": "Elm App",
|
||||
"name": "Create Elm App Sample",
|
||||
"short_name": "Inbucket",
|
||||
"name": "Inbucket",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "192x192",
|
||||
"src": "favicon.png",
|
||||
"sizes": "16x16",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
|
||||
+5
-15
@@ -1,21 +1,11 @@
|
||||
module Data.Date exposing (date)
|
||||
|
||||
import Date exposing (Date)
|
||||
import Json.Decode as Decode exposing (..)
|
||||
import Json.Decode exposing (..)
|
||||
import Time exposing (Posix)
|
||||
|
||||
|
||||
{-| Decode an ISO 8601 date
|
||||
{-| Decode a POSIX milliseconds timestamp.
|
||||
-}
|
||||
date : Decoder Date
|
||||
date : Decoder Posix
|
||||
date =
|
||||
let
|
||||
convert : String -> Decoder Date
|
||||
convert raw =
|
||||
case Date.fromString raw of
|
||||
Ok date ->
|
||||
succeed date
|
||||
|
||||
Err error ->
|
||||
fail error
|
||||
in
|
||||
string |> andThen convert
|
||||
int |> map Time.millisToPosix
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
module Data.Message exposing (Attachment, Message, attachmentDecoder, decoder)
|
||||
|
||||
import Data.Date exposing (date)
|
||||
import Date exposing (Date)
|
||||
import Json.Decode as Decode exposing (..)
|
||||
import Json.Decode exposing (..)
|
||||
import Json.Decode.Pipeline exposing (..)
|
||||
import Time exposing (Posix)
|
||||
|
||||
|
||||
type alias Message =
|
||||
@@ -12,7 +12,7 @@ type alias Message =
|
||||
, from : String
|
||||
, to : List String
|
||||
, subject : String
|
||||
, date : Date
|
||||
, date : Posix
|
||||
, size : Int
|
||||
, seen : Bool
|
||||
, text : String
|
||||
@@ -30,13 +30,13 @@ type alias Attachment =
|
||||
|
||||
decoder : Decoder Message
|
||||
decoder =
|
||||
decode Message
|
||||
succeed Message
|
||||
|> required "mailbox" string
|
||||
|> required "id" string
|
||||
|> optional "from" string ""
|
||||
|> required "to" (list string)
|
||||
|> optional "subject" string ""
|
||||
|> required "date" date
|
||||
|> required "posix-millis" date
|
||||
|> required "size" int
|
||||
|> required "seen" bool
|
||||
|> required "text" string
|
||||
@@ -46,7 +46,7 @@ decoder =
|
||||
|
||||
attachmentDecoder : Decoder Attachment
|
||||
attachmentDecoder =
|
||||
decode Attachment
|
||||
succeed Attachment
|
||||
|> required "id" string
|
||||
|> required "filename" string
|
||||
|> required "content-type" string
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
module Data.MessageHeader exposing (MessageHeader, decoder)
|
||||
|
||||
import Data.Date exposing (date)
|
||||
import Date exposing (Date)
|
||||
import Json.Decode as Decode exposing (..)
|
||||
import Json.Decode exposing (..)
|
||||
import Json.Decode.Pipeline exposing (..)
|
||||
import Time exposing (Posix)
|
||||
|
||||
|
||||
type alias MessageHeader =
|
||||
@@ -12,7 +12,7 @@ type alias MessageHeader =
|
||||
, from : String
|
||||
, to : List String
|
||||
, subject : String
|
||||
, date : Date
|
||||
, date : Posix
|
||||
, size : Int
|
||||
, seen : Bool
|
||||
}
|
||||
@@ -20,12 +20,12 @@ type alias MessageHeader =
|
||||
|
||||
decoder : Decoder MessageHeader
|
||||
decoder =
|
||||
decode MessageHeader
|
||||
succeed MessageHeader
|
||||
|> required "mailbox" string
|
||||
|> required "id" string
|
||||
|> optional "from" string ""
|
||||
|> required "to" (list string)
|
||||
|> optional "subject" string ""
|
||||
|> required "date" date
|
||||
|> required "posix-millis" date
|
||||
|> required "size" int
|
||||
|> required "seen" bool
|
||||
|
||||
@@ -31,7 +31,7 @@ type alias Metrics =
|
||||
|
||||
decoder : Decoder Metrics
|
||||
decoder =
|
||||
decode Metrics
|
||||
succeed Metrics
|
||||
|> requiredAt [ "memstats", "Sys" ] int
|
||||
|> requiredAt [ "memstats", "HeapSys" ] int
|
||||
|> requiredAt [ "memstats", "HeapAlloc" ] int
|
||||
@@ -59,4 +59,6 @@ decoder =
|
||||
-}
|
||||
decodeIntList : Decoder (List Int)
|
||||
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
|
||||
)
|
||||
|
||||
import Json.Decode as Decode exposing (..)
|
||||
import Browser.Navigation as Nav
|
||||
import Json.Decode exposing (..)
|
||||
import Json.Decode.Pipeline exposing (..)
|
||||
import Navigation exposing (Location)
|
||||
import Url exposing (Url)
|
||||
|
||||
|
||||
type alias Session =
|
||||
{ host : String
|
||||
{ key : Nav.Key
|
||||
, host : String
|
||||
, flash : String
|
||||
, routing : Bool
|
||||
, persistent : Persistent
|
||||
@@ -36,9 +38,9 @@ type Msg
|
||||
| AddRecent String
|
||||
|
||||
|
||||
init : Location -> Persistent -> Session
|
||||
init location persistent =
|
||||
Session location.host "" True persistent
|
||||
init : Nav.Key -> Url -> Persistent -> Session
|
||||
init key location persistent =
|
||||
Session key location.host "" True persistent
|
||||
|
||||
|
||||
update : Msg -> Session -> Session
|
||||
@@ -84,10 +86,10 @@ none =
|
||||
|
||||
decoder : Decoder Persistent
|
||||
decoder =
|
||||
decode Persistent
|
||||
succeed Persistent
|
||||
|> optional "recentMailboxes" (list string) []
|
||||
|
||||
|
||||
decodeValueWithDefault : Value -> Persistent
|
||||
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
|
||||
|
||||
|
||||
delete : String -> Http.Request ()
|
||||
delete url =
|
||||
delete : (Result Http.Error () -> msg) -> String -> Cmd msg
|
||||
delete msg url =
|
||||
Http.request
|
||||
{ method = "DELETE"
|
||||
, headers = []
|
||||
, url = url
|
||||
, body = Http.emptyBody
|
||||
, expect = Http.expectStringResponse (\_ -> Ok ())
|
||||
, expect = Http.expectWhatever msg
|
||||
, timeout = Nothing
|
||||
, withCredentials = False
|
||||
, tracker = Nothing
|
||||
}
|
||||
|
||||
|
||||
patch : String -> Http.Body -> Http.Request ()
|
||||
patch url body =
|
||||
patch : (Result Http.Error () -> msg) -> String -> Http.Body -> Cmd msg
|
||||
patch msg url body =
|
||||
Http.request
|
||||
{ method = "PATCH"
|
||||
, headers = []
|
||||
, url = url
|
||||
, body = body
|
||||
, expect = Http.expectStringResponse (\_ -> Ok ())
|
||||
, expect = Http.expectWhatever msg
|
||||
, timeout = Nothing
|
||||
, withCredentials = False
|
||||
, tracker = Nothing
|
||||
}
|
||||
|
||||
|
||||
@@ -42,11 +42,7 @@ errorString error =
|
||||
"HTTP Network error"
|
||||
|
||||
Http.BadStatus res ->
|
||||
"Bad HTTP status: " ++ toString res.status.code
|
||||
"Bad HTTP status: " ++ String.fromInt res
|
||||
|
||||
Http.BadPayload msg res ->
|
||||
"Bad HTTP payload: "
|
||||
++ msg
|
||||
++ " ("
|
||||
++ toString res.status.code
|
||||
++ ")"
|
||||
Http.BadBody msg ->
|
||||
"Bad HTTP body: " ++ msg
|
||||
|
||||
+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 Html exposing (..)
|
||||
import Json.Decode as Decode exposing (Value)
|
||||
import Navigation exposing (Location)
|
||||
import Json.Decode as D exposing (Value)
|
||||
import Page.Home as Home
|
||||
import Page.Mailbox as Mailbox
|
||||
import Page.Monitor as Monitor
|
||||
import Page.Status as Status
|
||||
import Ports
|
||||
import Route exposing (Route)
|
||||
import Url exposing (Url)
|
||||
import Views.Page as Page exposing (ActivePage(..), frame)
|
||||
|
||||
|
||||
@@ -31,11 +33,11 @@ type alias Model =
|
||||
}
|
||||
|
||||
|
||||
init : Value -> Location -> ( Model, Cmd Msg )
|
||||
init sessionValue location =
|
||||
init : Value -> Url -> Nav.Key -> ( Model, Cmd Msg )
|
||||
init sessionValue location key =
|
||||
let
|
||||
session =
|
||||
Session.init location (Session.decodeValueWithDefault sessionValue)
|
||||
Session.init key location (Session.decodeValueWithDefault sessionValue)
|
||||
|
||||
( subModel, _ ) =
|
||||
Home.init
|
||||
@@ -47,16 +49,17 @@ init sessionValue location =
|
||||
}
|
||||
|
||||
route =
|
||||
Route.fromLocation location
|
||||
Route.fromUrl location
|
||||
in
|
||||
applySession (setRoute route model)
|
||||
|
||||
|
||||
type Msg
|
||||
= SetRoute Route
|
||||
| NewRoute Route
|
||||
| UpdateSession (Result String Session.Persistent)
|
||||
| MailboxNameInput String
|
||||
| UrlChanged Url
|
||||
| LinkClicked UrlRequest
|
||||
| UpdateSession (Result D.Error Session.Persistent)
|
||||
| OnMailboxNameInput String
|
||||
| ViewMailbox String
|
||||
| HomeMsg Home.Msg
|
||||
| MailboxMsg Mailbox.Msg
|
||||
@@ -76,9 +79,9 @@ subscriptions model =
|
||||
]
|
||||
|
||||
|
||||
sessionChange : Sub (Result String Session.Persistent)
|
||||
sessionChange : Sub (Result D.Error Session.Persistent)
|
||||
sessionChange =
|
||||
Ports.onSessionChange (Decode.decodeValue Session.decoder)
|
||||
Ports.onSessionChange (D.decodeValue Session.decoder)
|
||||
|
||||
|
||||
pageSubscriptions : Page -> Sub Msg
|
||||
@@ -105,19 +108,27 @@ update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
applySession <|
|
||||
case msg of
|
||||
SetRoute route ->
|
||||
-- Updates broser URL to requested route.
|
||||
( model, Route.newUrl route, Session.none )
|
||||
LinkClicked req ->
|
||||
case req of
|
||||
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.
|
||||
if model.session.routing then
|
||||
setRoute route model
|
||||
setRoute (Route.fromUrl url) model
|
||||
|
||||
else
|
||||
-- Skip once, but re-enable routing.
|
||||
( 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) ->
|
||||
let
|
||||
session =
|
||||
@@ -129,18 +140,17 @@ update msg model =
|
||||
)
|
||||
|
||||
UpdateSession (Err error) ->
|
||||
let
|
||||
_ =
|
||||
Debug.log "Error decoding session" error
|
||||
in
|
||||
( model, Cmd.none, Session.none )
|
||||
( model
|
||||
, Cmd.none
|
||||
, Session.SetFlash ("Error decoding session: " ++ D.errorToString error)
|
||||
)
|
||||
|
||||
MailboxNameInput name ->
|
||||
OnMailboxNameInput name ->
|
||||
( { model | mailboxName = name }, Cmd.none, Session.none )
|
||||
|
||||
ViewMailbox name ->
|
||||
( { model | mailboxName = "" }
|
||||
, Route.newUrl (Route.Mailbox name)
|
||||
, Route.newUrl model.session.key (Route.Mailbox name)
|
||||
, Session.none
|
||||
)
|
||||
|
||||
@@ -230,10 +240,7 @@ setRoute route model =
|
||||
|
||||
Route.Status ->
|
||||
( { model | page = Status Status.init }
|
||||
, Cmd.batch
|
||||
[ Ports.windowTitle "Inbucket Status"
|
||||
, Cmd.map StatusMsg Status.load
|
||||
]
|
||||
, Cmd.map StatusMsg Status.load
|
||||
, Session.none
|
||||
)
|
||||
in
|
||||
@@ -269,7 +276,7 @@ applySession ( model, cmd, sessionMsg ) =
|
||||
-- VIEW
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view : Model -> Document Msg
|
||||
view model =
|
||||
let
|
||||
mailbox =
|
||||
@@ -282,31 +289,36 @@ view model =
|
||||
|
||||
controls =
|
||||
{ viewMailbox = ViewMailbox
|
||||
, mailboxOnInput = MailboxNameInput
|
||||
, mailboxOnInput = OnMailboxNameInput
|
||||
, mailboxValue = model.mailboxName
|
||||
, recentOptions = model.session.persistent.recentMailboxes
|
||||
, recentActive = mailbox
|
||||
}
|
||||
|
||||
frame =
|
||||
Page.frame controls model.session
|
||||
framePage :
|
||||
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
|
||||
case model.page of
|
||||
Home subModel ->
|
||||
Html.map HomeMsg (Home.view model.session subModel)
|
||||
|> frame Page.Other
|
||||
framePage Page.Other HomeMsg (Home.view model.session subModel)
|
||||
|
||||
Mailbox subModel ->
|
||||
Html.map MailboxMsg (Mailbox.view model.session subModel)
|
||||
|> frame Page.Mailbox
|
||||
framePage Page.Mailbox MailboxMsg (Mailbox.view model.session subModel)
|
||||
|
||||
Monitor subModel ->
|
||||
Html.map MonitorMsg (Monitor.view model.session subModel)
|
||||
|> frame Page.Monitor
|
||||
framePage Page.Monitor MonitorMsg (Monitor.view model.session subModel)
|
||||
|
||||
Status subModel ->
|
||||
Html.map StatusMsg (Status.view model.session subModel)
|
||||
|> frame Page.Status
|
||||
framePage Page.Status StatusMsg (Status.view model.session subModel)
|
||||
|
||||
|
||||
|
||||
@@ -315,9 +327,11 @@ view model =
|
||||
|
||||
main : Program Value Model Msg
|
||||
main =
|
||||
Navigation.programWithFlags (Route.fromLocation >> NewRoute)
|
||||
Browser.application
|
||||
{ init = init
|
||||
, view = view
|
||||
, update = update
|
||||
, subscriptions = subscriptions
|
||||
, onUrlChange = UrlChanged
|
||||
, onUrlRequest = LinkClicked
|
||||
}
|
||||
|
||||
+21
-22
@@ -19,18 +19,14 @@ type alias Model =
|
||||
|
||||
init : ( Model, Cmd Msg )
|
||||
init =
|
||||
( Model ""
|
||||
, Cmd.batch
|
||||
[ Ports.windowTitle "Inbucket"
|
||||
, cmdGreeting
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
cmdGreeting : Cmd Msg
|
||||
cmdGreeting =
|
||||
Http.send GreetingResult <|
|
||||
Http.getString "/serve/greeting"
|
||||
let
|
||||
cmdGreeting =
|
||||
Http.get
|
||||
{ url = "/serve/greeting"
|
||||
, expect = Http.expectString GreetingLoaded
|
||||
}
|
||||
in
|
||||
( Model "", cmdGreeting )
|
||||
|
||||
|
||||
|
||||
@@ -38,16 +34,16 @@ cmdGreeting =
|
||||
|
||||
|
||||
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 =
|
||||
case msg of
|
||||
GreetingResult (Ok greeting) ->
|
||||
GreetingLoaded (Ok greeting) ->
|
||||
( Model greeting, Cmd.none, Session.none )
|
||||
|
||||
GreetingResult (Err err) ->
|
||||
GreetingLoaded (Err err) ->
|
||||
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
||||
|
||||
|
||||
@@ -55,12 +51,15 @@ update session msg model =
|
||||
-- VIEW --
|
||||
|
||||
|
||||
view : Session -> Model -> Html Msg
|
||||
view : Session -> Model -> { title : String, content : Html Msg }
|
||||
view session model =
|
||||
div [ id "page" ]
|
||||
[ div
|
||||
[ class "greeting"
|
||||
, property "innerHTML" (Encode.string model.greeting)
|
||||
{ title = "Inbucket"
|
||||
, content =
|
||||
div [ id "page" ]
|
||||
[ 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.MessageHeader as MessageHeader exposing (MessageHeader)
|
||||
import Data.Session as Session exposing (Session)
|
||||
import Date exposing (Date)
|
||||
import DateFormat
|
||||
import DateFormat as DF
|
||||
import DateFormat.Relative as Relative
|
||||
import Html exposing (..)
|
||||
import Html.Attributes
|
||||
exposing
|
||||
( class
|
||||
, classList
|
||||
, downloadAs
|
||||
, download
|
||||
, href
|
||||
, id
|
||||
, placeholder
|
||||
@@ -28,7 +27,7 @@ import Json.Encode as Encode
|
||||
import Ports
|
||||
import Route
|
||||
import Task
|
||||
import Time exposing (Time)
|
||||
import Time exposing (Posix)
|
||||
|
||||
|
||||
|
||||
@@ -65,7 +64,7 @@ type alias MessageList =
|
||||
|
||||
type alias VisibleMessage =
|
||||
{ message : Message
|
||||
, markSeenAt : Maybe Time
|
||||
, markSeenAt : Maybe Int
|
||||
}
|
||||
|
||||
|
||||
@@ -74,13 +73,13 @@ type alias Model =
|
||||
, state : State
|
||||
, bodyMode : Body
|
||||
, searchInput : String
|
||||
, now : Date
|
||||
, now : Posix
|
||||
}
|
||||
|
||||
|
||||
init : String -> Maybe MessageID -> ( Model, Cmd Msg )
|
||||
init mailboxName selection =
|
||||
( Model mailboxName (LoadingList selection) SafeHtmlBody "" (Date.fromTime 0)
|
||||
( Model mailboxName (LoadingList selection) SafeHtmlBody "" (Time.millisToPosix 0)
|
||||
, load mailboxName
|
||||
)
|
||||
|
||||
@@ -88,8 +87,7 @@ init mailboxName selection =
|
||||
load : String -> Cmd Msg
|
||||
load mailboxName =
|
||||
Cmd.batch
|
||||
[ Ports.windowTitle (mailboxName ++ " - Inbucket")
|
||||
, Task.perform Tick Time.now
|
||||
[ Task.perform Tick Time.now
|
||||
, getList mailboxName
|
||||
]
|
||||
|
||||
@@ -108,13 +106,13 @@ subscriptions model =
|
||||
Sub.none
|
||||
|
||||
else
|
||||
Time.every (250 * Time.millisecond) SeenTick
|
||||
Time.every 250 MarkSeenTick
|
||||
|
||||
_ ->
|
||||
Sub.none
|
||||
in
|
||||
Sub.batch
|
||||
[ Time.every (30 * Time.second) Tick
|
||||
[ Time.every (30 * 1000) Tick
|
||||
, subSeen
|
||||
]
|
||||
|
||||
@@ -124,20 +122,20 @@ subscriptions model =
|
||||
|
||||
|
||||
type Msg
|
||||
= ClickMessage MessageID
|
||||
| DeleteMessage Message
|
||||
| DeleteMessageResult (Result Http.Error ())
|
||||
| ListResult (Result Http.Error (List MessageHeader))
|
||||
| MarkSeenResult (Result Http.Error ())
|
||||
| MessageResult (Result Http.Error Message)
|
||||
= ListLoaded (Result Http.Error (List MessageHeader))
|
||||
| ClickMessage MessageID
|
||||
| OpenMessage MessageID
|
||||
| MessageLoaded (Result Http.Error Message)
|
||||
| MessageBody Body
|
||||
| OpenedTime Time
|
||||
| Purge
|
||||
| PurgeResult (Result Http.Error ())
|
||||
| SearchInput String
|
||||
| SeenTick Time
|
||||
| Tick Time
|
||||
| ViewMessage MessageID
|
||||
| OpenedTime Posix
|
||||
| MarkSeenTick Posix
|
||||
| MarkedSeen (Result Http.Error ())
|
||||
| DeleteMessage Message
|
||||
| DeletedMessage (Result Http.Error ())
|
||||
| PurgeMailbox
|
||||
| PurgedMailbox (Result Http.Error ())
|
||||
| OnSearchInput String
|
||||
| Tick Posix
|
||||
|
||||
|
||||
update : Session -> Msg -> Model -> ( Model, Cmd Msg, Session.Msg )
|
||||
@@ -147,28 +145,25 @@ update session msg model =
|
||||
( updateSelected model id
|
||||
, Cmd.batch
|
||||
[ -- Update browser location.
|
||||
Route.newUrl (Route.Message model.mailboxName id)
|
||||
Route.newUrl session.key (Route.Message model.mailboxName id)
|
||||
, getMessage model.mailboxName id
|
||||
]
|
||||
, Session.DisableRouting
|
||||
)
|
||||
|
||||
ViewMessage id ->
|
||||
( updateSelected model id
|
||||
, getMessage model.mailboxName id
|
||||
, Session.AddRecent model.mailboxName
|
||||
)
|
||||
OpenMessage id ->
|
||||
updateOpenMessage session model id
|
||||
|
||||
DeleteMessage message ->
|
||||
updateDeleteMessage model message
|
||||
|
||||
DeleteMessageResult (Ok _) ->
|
||||
DeletedMessage (Ok _) ->
|
||||
( model, Cmd.none, Session.none )
|
||||
|
||||
DeleteMessageResult (Err err) ->
|
||||
DeletedMessage (Err err) ->
|
||||
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
||||
|
||||
ListResult (Ok headers) ->
|
||||
ListLoaded (Ok headers) ->
|
||||
case model.state of
|
||||
LoadingList selection ->
|
||||
let
|
||||
@@ -179,8 +174,7 @@ update session msg model =
|
||||
in
|
||||
case selection of
|
||||
Just id ->
|
||||
-- Recurse to select message id.
|
||||
update session (ViewMessage id) newModel
|
||||
updateOpenMessage session newModel id
|
||||
|
||||
Nothing ->
|
||||
( newModel, Cmd.none, Session.AddRecent model.mailboxName )
|
||||
@@ -188,25 +182,25 @@ update session msg model =
|
||||
_ ->
|
||||
( model, Cmd.none, Session.none )
|
||||
|
||||
ListResult (Err err) ->
|
||||
ListLoaded (Err err) ->
|
||||
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
||||
|
||||
MarkSeenResult (Ok _) ->
|
||||
MarkedSeen (Ok _) ->
|
||||
( model, Cmd.none, Session.none )
|
||||
|
||||
MarkSeenResult (Err err) ->
|
||||
MarkedSeen (Err err) ->
|
||||
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
||||
|
||||
MessageResult (Ok message) ->
|
||||
MessageLoaded (Ok message) ->
|
||||
updateMessageResult model message
|
||||
|
||||
MessageResult (Err err) ->
|
||||
MessageLoaded (Err err) ->
|
||||
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
||||
|
||||
MessageBody bodyMode ->
|
||||
( { model | bodyMode = bodyMode }, Cmd.none, Session.none )
|
||||
|
||||
SearchInput searchInput ->
|
||||
OnSearchInput searchInput ->
|
||||
updateSearchInput model searchInput
|
||||
|
||||
OpenedTime time ->
|
||||
@@ -216,13 +210,17 @@ update session msg model =
|
||||
( model, Cmd.none, Session.none )
|
||||
|
||||
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
|
||||
| state =
|
||||
ShowingList list
|
||||
(ShowingMessage
|
||||
{ visible
|
||||
| markSeenAt = Just (time + (1.5 * Time.second))
|
||||
| markSeenAt = Just markSeenAt
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -233,21 +231,21 @@ update session msg model =
|
||||
_ ->
|
||||
( model, Cmd.none, Session.none )
|
||||
|
||||
Purge ->
|
||||
PurgeMailbox ->
|
||||
updatePurge model
|
||||
|
||||
PurgeResult (Ok _) ->
|
||||
PurgedMailbox (Ok _) ->
|
||||
( model, Cmd.none, Session.none )
|
||||
|
||||
PurgeResult (Err err) ->
|
||||
PurgedMailbox (Err err) ->
|
||||
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
||||
|
||||
SeenTick now ->
|
||||
MarkSeenTick now ->
|
||||
case model.state of
|
||||
ShowingList _ (ShowingMessage { message, markSeenAt }) ->
|
||||
case markSeenAt of
|
||||
Just deadline ->
|
||||
if now >= deadline then
|
||||
if Time.posixToMillis now >= deadline then
|
||||
updateMarkMessageSeen model message
|
||||
|
||||
else
|
||||
@@ -260,7 +258,7 @@ update session msg model =
|
||||
( model, Cmd.none, Session.none )
|
||||
|
||||
Tick now ->
|
||||
( { model | now = Date.fromTime now }, Cmd.none, Session.none )
|
||||
( { model | now = now }, Cmd.none, Session.none )
|
||||
|
||||
|
||||
{-| Replace the currently displayed message.
|
||||
@@ -298,8 +296,7 @@ updatePurge model =
|
||||
cmd =
|
||||
"/api/v1/mailbox/"
|
||||
++ model.mailboxName
|
||||
|> HttpUtil.delete
|
||||
|> Http.send PurgeResult
|
||||
|> HttpUtil.delete PurgedMailbox
|
||||
in
|
||||
case model.state of
|
||||
ShowingList list _ ->
|
||||
@@ -371,8 +368,7 @@ updateDeleteMessage model message =
|
||||
"/api/v1/mailbox/" ++ message.mailbox ++ "/" ++ message.id
|
||||
|
||||
cmd =
|
||||
HttpUtil.delete url
|
||||
|> Http.send DeleteMessageResult
|
||||
HttpUtil.delete DeletedMessage url
|
||||
|
||||
filter f messageList =
|
||||
{ messageList | headers = List.filter f messageList.headers }
|
||||
@@ -411,8 +407,7 @@ updateMarkMessageSeen model message =
|
||||
-- desired change in the body.
|
||||
Encode.object [ ( "seen", Encode.bool True ) ]
|
||||
|> Http.jsonBody
|
||||
|> HttpUtil.patch url
|
||||
|> Http.send MarkSeenResult
|
||||
|> HttpUtil.patch MarkedSeen url
|
||||
|
||||
map f messageList =
|
||||
{ messageList | headers = List.map f messageList.headers }
|
||||
@@ -435,14 +430,24 @@ updateMarkMessageSeen model message =
|
||||
( 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 mailboxName =
|
||||
let
|
||||
url =
|
||||
"/api/v1/mailbox/" ++ mailboxName
|
||||
in
|
||||
Http.get url (Decode.list MessageHeader.decoder)
|
||||
|> Http.send ListResult
|
||||
Http.get
|
||||
{ url = url
|
||||
, expect = Http.expectJson ListLoaded (Decode.list MessageHeader.decoder)
|
||||
}
|
||||
|
||||
|
||||
getMessage : String -> MessageID -> Cmd Msg
|
||||
@@ -451,37 +456,42 @@ getMessage mailboxName id =
|
||||
url =
|
||||
"/serve/m/" ++ mailboxName ++ "/" ++ id
|
||||
in
|
||||
Http.get url Message.decoder
|
||||
|> Http.send MessageResult
|
||||
Http.get
|
||||
{ url = url
|
||||
, expect = Http.expectJson MessageLoaded Message.decoder
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- VIEW
|
||||
|
||||
|
||||
view : Session -> Model -> Html Msg
|
||||
view : Session -> Model -> { title : String, content : Html Msg }
|
||||
view session model =
|
||||
div [ id "page", class "mailbox" ]
|
||||
[ viewMessageList session model
|
||||
, main_
|
||||
[ id "message" ]
|
||||
[ case model.state of
|
||||
ShowingList _ NoMessage ->
|
||||
text
|
||||
("Select a message on the left,"
|
||||
++ " or enter a different username into the box on upper right."
|
||||
)
|
||||
{ title = model.mailboxName ++ " - Inbucket"
|
||||
, content =
|
||||
div [ id "page", class "mailbox" ]
|
||||
[ viewMessageList session model
|
||||
, main_
|
||||
[ id "message" ]
|
||||
[ case model.state of
|
||||
ShowingList _ NoMessage ->
|
||||
text
|
||||
("Select a message on the left,"
|
||||
++ " or enter a different username into the box on upper right."
|
||||
)
|
||||
|
||||
ShowingList _ (ShowingMessage { message }) ->
|
||||
viewMessage message model.bodyMode
|
||||
ShowingList _ (ShowingMessage { message }) ->
|
||||
viewMessage message model.bodyMode
|
||||
|
||||
ShowingList _ (Transitioning { message }) ->
|
||||
viewMessage message model.bodyMode
|
||||
ShowingList _ (Transitioning { message }) ->
|
||||
viewMessage message model.bodyMode
|
||||
|
||||
_ ->
|
||||
text ""
|
||||
_ ->
|
||||
text ""
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
viewMessageList : Session -> Model -> Html Msg
|
||||
@@ -491,11 +501,11 @@ viewMessageList session model =
|
||||
[ input
|
||||
[ type_ "search"
|
||||
, placeholder "search"
|
||||
, onInput SearchInput
|
||||
, onInput OnSearchInput
|
||||
, value model.searchInput
|
||||
]
|
||||
[]
|
||||
, button [ onClick Purge ] [ text "Purge" ]
|
||||
, button [ onClick PurgeMailbox ] [ text "Purge" ]
|
||||
]
|
||||
, case model.state of
|
||||
LoadingList _ ->
|
||||
@@ -530,14 +540,14 @@ messageChip model selected message =
|
||||
viewMessage : Message -> Body -> Html Msg
|
||||
viewMessage message bodyMode =
|
||||
let
|
||||
sourceUrl message =
|
||||
sourceUrl =
|
||||
"/serve/m/" ++ message.mailbox ++ "/" ++ message.id ++ "/source"
|
||||
in
|
||||
div []
|
||||
[ div [ class "button-bar" ]
|
||||
[ button [ class "danger", onClick (DeleteMessage message) ] [ text "Delete" ]
|
||||
, a
|
||||
[ href (sourceUrl message), target "_blank" ]
|
||||
[ href sourceUrl, target "_blank" ]
|
||||
[ button [] [ text "Source" ] ]
|
||||
]
|
||||
, dl [ id "message-header" ]
|
||||
@@ -584,10 +594,10 @@ messageBody message bodyMode =
|
||||
, article [ class "message-body" ]
|
||||
[ case bodyMode of
|
||||
SafeHtmlBody ->
|
||||
div [ property "innerHTML" (Encode.string message.html) ] []
|
||||
Html.node "rendered-html" [ property "content" (Encode.string message.html) ] []
|
||||
|
||||
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 ]
|
||||
, 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 =
|
||||
Relative.relativeTime model.now date |> text
|
||||
|
||||
|
||||
verboseDate : Date -> Html Msg
|
||||
verboseDate : Posix -> Html Msg
|
||||
verboseDate date =
|
||||
DateFormat.format
|
||||
[ DateFormat.monthNameFull
|
||||
, DateFormat.text " "
|
||||
, DateFormat.dayOfMonthSuffix
|
||||
, DateFormat.text ", "
|
||||
, DateFormat.yearNumber
|
||||
, DateFormat.text " "
|
||||
, DateFormat.hourNumber
|
||||
, DateFormat.text ":"
|
||||
, DateFormat.minuteFixed
|
||||
, DateFormat.text ":"
|
||||
, DateFormat.secondFixed
|
||||
, DateFormat.text " "
|
||||
, DateFormat.amPmUppercase
|
||||
]
|
||||
date
|
||||
|> text
|
||||
text <|
|
||||
DF.format
|
||||
[ DF.monthNameFull
|
||||
, DF.text " "
|
||||
, DF.dayOfMonthSuffix
|
||||
, DF.text ", "
|
||||
, DF.yearNumber
|
||||
, DF.text " "
|
||||
, DF.hourNumber
|
||||
, DF.text ":"
|
||||
, DF.minuteFixed
|
||||
, DF.text ":"
|
||||
, DF.secondFixed
|
||||
, DF.text " "
|
||||
, DF.amPmUppercase
|
||||
]
|
||||
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.Session as Session exposing (Session)
|
||||
import Date exposing (Date)
|
||||
import DateFormat
|
||||
exposing
|
||||
( amPmUppercase
|
||||
, dayOfMonthFixed
|
||||
, format
|
||||
, hourNumber
|
||||
, minuteFixed
|
||||
, monthNameFirstThree
|
||||
)
|
||||
import DateFormat as DF
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events as Events
|
||||
import Json.Decode as D
|
||||
import Ports
|
||||
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 False []
|
||||
, Cmd.batch
|
||||
[ Ports.windowTitle "Inbucket Monitor"
|
||||
, Ports.monitorCommand True
|
||||
]
|
||||
)
|
||||
( Model False [], Ports.monitorCommand True )
|
||||
|
||||
|
||||
|
||||
@@ -55,7 +47,7 @@ subscriptions model =
|
||||
|> D.decodeValue
|
||||
|> Ports.monitorMessage
|
||||
in
|
||||
Sub.map MonitorResult monitorMessage
|
||||
Sub.map MessageReceived monitorMessage
|
||||
|
||||
|
||||
|
||||
@@ -63,30 +55,25 @@ subscriptions model =
|
||||
|
||||
|
||||
type Msg
|
||||
= MonitorResult (Result String MonitorMessage)
|
||||
= MessageReceived (Result D.Error MonitorMessage)
|
||||
| OpenMessage MessageHeader
|
||||
|
||||
|
||||
type MonitorMessage
|
||||
= Connected Bool
|
||||
| Message MessageHeader
|
||||
|
||||
|
||||
update : Session -> Msg -> Model -> ( Model, Cmd Msg, Session.Msg )
|
||||
update session msg model =
|
||||
case msg of
|
||||
MonitorResult (Ok (Connected status)) ->
|
||||
MessageReceived (Ok (Connected status)) ->
|
||||
( { model | connected = status }, Cmd.none, Session.none )
|
||||
|
||||
MonitorResult (Ok (Message msg)) ->
|
||||
( { model | messages = msg :: model.messages }, Cmd.none, Session.none )
|
||||
MessageReceived (Ok (Message header)) ->
|
||||
( { model | messages = header :: model.messages }, Cmd.none, Session.none )
|
||||
|
||||
MonitorResult (Err err) ->
|
||||
( model, Cmd.none, Session.SetFlash err )
|
||||
MessageReceived (Err err) ->
|
||||
( model, Cmd.none, Session.SetFlash (D.errorToString err) )
|
||||
|
||||
OpenMessage msg ->
|
||||
OpenMessage header ->
|
||||
( model
|
||||
, Route.newUrl (Route.Message msg.mailbox msg.id)
|
||||
, Route.newUrl session.key (Route.Message header.mailbox header.id)
|
||||
, Session.none
|
||||
)
|
||||
|
||||
@@ -95,32 +82,35 @@ update session msg model =
|
||||
-- VIEW
|
||||
|
||||
|
||||
view : Session -> Model -> Html Msg
|
||||
view : Session -> Model -> { title : String, content : Html Msg }
|
||||
view session model =
|
||||
div [ id "page" ]
|
||||
[ h1 [] [ text "Monitor" ]
|
||||
, p []
|
||||
[ text "Messages will be listed here shortly after delivery. "
|
||||
, em []
|
||||
[ text
|
||||
(if model.connected then
|
||||
"Connected."
|
||||
{ title = "Inbucket Monitor"
|
||||
, content =
|
||||
div [ id "page" ]
|
||||
[ h1 [] [ text "Monitor" ]
|
||||
, p []
|
||||
[ text "Messages will be listed here shortly after delivery. "
|
||||
, em []
|
||||
[ text
|
||||
(if model.connected then
|
||||
"Connected."
|
||||
|
||||
else
|
||||
"Disconnected!"
|
||||
)
|
||||
else
|
||||
"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
|
||||
@@ -133,18 +123,19 @@ viewMessage message =
|
||||
]
|
||||
|
||||
|
||||
shortDate : Date -> Html Msg
|
||||
shortDate : Posix -> Html Msg
|
||||
shortDate date =
|
||||
format
|
||||
[ dayOfMonthFixed
|
||||
, DateFormat.text "-"
|
||||
, monthNameFirstThree
|
||||
, DateFormat.text " "
|
||||
, hourNumber
|
||||
, DateFormat.text ":"
|
||||
, minuteFixed
|
||||
, DateFormat.text " "
|
||||
, amPmUppercase
|
||||
DF.format
|
||||
[ DF.dayOfMonthFixed
|
||||
, DF.text "-"
|
||||
, DF.monthNameAbbreviated
|
||||
, DF.text " "
|
||||
, DF.hourNumber
|
||||
, DF.text ":"
|
||||
, DF.minuteFixed
|
||||
, DF.text " "
|
||||
, DF.amPmUppercase
|
||||
]
|
||||
Time.utc
|
||||
date
|
||||
|> text
|
||||
|
||||
+70
-61
@@ -7,9 +7,9 @@ import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Http exposing (Error)
|
||||
import HttpUtil
|
||||
import Sparkline exposing (DataSet, Point, Size, sparkline)
|
||||
import Sparkline as Spark
|
||||
import Svg.Attributes as SvgAttrib
|
||||
import Time exposing (Time)
|
||||
import Time exposing (Posix)
|
||||
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ type alias Metric =
|
||||
{ label : String
|
||||
, value : Int
|
||||
, formatter : Int -> String
|
||||
, graph : DataSet -> Html Msg
|
||||
, history : DataSet
|
||||
, graph : Spark.DataSet -> Html Msg
|
||||
, history : Spark.DataSet
|
||||
, minutes : Int
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ init =
|
||||
}
|
||||
|
||||
|
||||
initDataSet : DataSet
|
||||
initDataSet : Spark.DataSet
|
||||
initDataSet =
|
||||
List.range 0 59
|
||||
|> List.map (\x -> ( toFloat x, 0 ))
|
||||
@@ -84,7 +84,7 @@ load =
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions model =
|
||||
Time.every (10 * Time.second) Tick
|
||||
Time.every (10 * 1000) Tick
|
||||
|
||||
|
||||
|
||||
@@ -92,17 +92,17 @@ subscriptions model =
|
||||
|
||||
|
||||
type Msg
|
||||
= NewMetrics (Result Http.Error Metrics)
|
||||
| Tick Time
|
||||
= MetricsReceived (Result Http.Error Metrics)
|
||||
| Tick Posix
|
||||
|
||||
|
||||
update : Session -> Msg -> Model -> ( Model, Cmd Msg, Session.Msg )
|
||||
update session msg model =
|
||||
case msg of
|
||||
NewMetrics (Ok metrics) ->
|
||||
MetricsReceived (Ok metrics) ->
|
||||
( updateMetrics metrics model, Cmd.none, Session.none )
|
||||
|
||||
NewMetrics (Err err) ->
|
||||
MetricsReceived (Err err) ->
|
||||
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
||||
|
||||
Tick time ->
|
||||
@@ -207,46 +207,51 @@ updateRemoteTotal metric value history =
|
||||
|
||||
getMetrics : Cmd Msg
|
||||
getMetrics =
|
||||
Http.get "/debug/vars" Metrics.decoder
|
||||
|> Http.send NewMetrics
|
||||
Http.get
|
||||
{ url = "/debug/vars"
|
||||
, expect = Http.expectJson MetricsReceived Metrics.decoder
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- VIEW --
|
||||
|
||||
|
||||
view : Session -> Model -> Html Msg
|
||||
view : Session -> Model -> { title : String, content : Html Msg }
|
||||
view session model =
|
||||
div [ id "page" ]
|
||||
[ h1 [] [ text "Status" ]
|
||||
, case model.metrics of
|
||||
Nothing ->
|
||||
div [] [ text "Loading metrics..." ]
|
||||
{ title = "Inbucket Status"
|
||||
, content =
|
||||
div [ id "page" ]
|
||||
[ h1 [] [ text "Status" ]
|
||||
, case model.metrics of
|
||||
Nothing ->
|
||||
div [] [ text "Loading metrics..." ]
|
||||
|
||||
Just metrics ->
|
||||
div []
|
||||
[ framePanel "General Metrics"
|
||||
[ viewMetric model.sysMem
|
||||
, viewMetric model.heapSize
|
||||
, viewMetric model.heapUsed
|
||||
, viewMetric model.heapObjects
|
||||
, viewMetric model.goRoutines
|
||||
, viewMetric model.webSockets
|
||||
Just metrics ->
|
||||
div []
|
||||
[ framePanel "General Metrics"
|
||||
[ viewMetric model.sysMem
|
||||
, viewMetric model.heapSize
|
||||
, viewMetric model.heapUsed
|
||||
, viewMetric model.heapObjects
|
||||
, viewMetric model.goRoutines
|
||||
, 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
|
||||
@@ -256,7 +261,7 @@ viewMetric metric =
|
||||
, div [ class "value" ] [ text (metric.formatter metric.value) ]
|
||||
, div [ class "graph" ]
|
||||
[ metric.graph metric.history
|
||||
, text ("(" ++ toString metric.minutes ++ "min)")
|
||||
, text ("(" ++ String.fromInt metric.minutes ++ "min)")
|
||||
]
|
||||
]
|
||||
|
||||
@@ -278,30 +283,34 @@ graphNull =
|
||||
div [] []
|
||||
|
||||
|
||||
graphSize : Size
|
||||
graphSize : Spark.Size
|
||||
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 =
|
||||
Sparkline.Style
|
||||
Spark.Style
|
||||
[ SvgAttrib.fill "rgba(50,100,255,0.3)"
|
||||
, SvgAttrib.stroke "rgba(50,100,255,1.0)"
|
||||
, SvgAttrib.strokeWidth "1.0"
|
||||
]
|
||||
|
||||
|
||||
barStyle : Sparkline.Param a -> Sparkline.Param a
|
||||
barStyle : Spark.Param a -> Spark.Param a
|
||||
barStyle =
|
||||
Sparkline.Style
|
||||
Spark.Style
|
||||
[ SvgAttrib.fill "rgba(50,200,50,0.7)"
|
||||
]
|
||||
|
||||
|
||||
zeroStyle : Sparkline.Param a -> Sparkline.Param a
|
||||
zeroStyle : Spark.Param a -> Spark.Param a
|
||||
zeroStyle =
|
||||
Sparkline.Style
|
||||
Spark.Style
|
||||
[ SvgAttrib.stroke "rgba(0,0,0,0.2)"
|
||||
, SvgAttrib.strokeWidth "1.0"
|
||||
]
|
||||
@@ -309,7 +318,7 @@ zeroStyle =
|
||||
|
||||
{-| Bar graph to be used with updateRemoteTotal metrics (change instead of absolute values).
|
||||
-}
|
||||
graphChange : DataSet -> Html a
|
||||
graphChange : Spark.DataSet -> Html a
|
||||
graphChange data =
|
||||
let
|
||||
-- Used with Domain to stop sparkline forgetting about zero; continue scrolling graph.
|
||||
@@ -321,16 +330,16 @@ graphChange data =
|
||||
Just point ->
|
||||
Tuple.first point
|
||||
in
|
||||
sparkline graphSize
|
||||
[ Sparkline.Bar 2.5 data |> barStyle
|
||||
, Sparkline.ZeroLine |> zeroStyle
|
||||
, Sparkline.Domain [ ( x, 0 ), ( x, 1 ) ]
|
||||
Spark.sparkline graphSize
|
||||
[ Spark.Bar 2.5 data |> barStyle
|
||||
, Spark.ZeroLine |> zeroStyle
|
||||
, Spark.Domain [ ( x, 0 ), ( x, 1 ) ]
|
||||
]
|
||||
|
||||
|
||||
{-| Zero based area graph, for charting absolute values relative to 0.
|
||||
-}
|
||||
graphZero : DataSet -> Html a
|
||||
graphZero : Spark.DataSet -> Html a
|
||||
graphZero data =
|
||||
let
|
||||
-- Used with Domain to stop sparkline forgetting about zero; continue scrolling graph.
|
||||
@@ -342,10 +351,10 @@ graphZero data =
|
||||
Just point ->
|
||||
Tuple.first point
|
||||
in
|
||||
sparkline graphSize
|
||||
[ Sparkline.Area data |> areaStyle
|
||||
, Sparkline.ZeroLine |> zeroStyle
|
||||
, Sparkline.Domain [ ( x, 0 ), ( x, 1 ) ]
|
||||
Spark.sparkline graphSize
|
||||
[ Spark.Area data |> areaStyle
|
||||
, Spark.ZeroLine |> zeroStyle
|
||||
, Spark.Domain [ ( x, 0 ), ( x, 1 ) ]
|
||||
]
|
||||
|
||||
|
||||
@@ -400,4 +409,4 @@ fmtInt n =
|
||||
else
|
||||
thousands (String.slice 0 -3 str) ++ "," ++ String.right 3 str
|
||||
in
|
||||
thousands (toString n)
|
||||
thousands (String.fromInt n)
|
||||
|
||||
@@ -3,7 +3,6 @@ port module Ports exposing
|
||||
, monitorMessage
|
||||
, onSessionChange
|
||||
, storeSession
|
||||
, windowTitle
|
||||
)
|
||||
|
||||
import Data.Session exposing (Persistent)
|
||||
@@ -20,6 +19,3 @@ port onSessionChange : (Value -> msg) -> Sub 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.Attributes as Attr
|
||||
import Navigation exposing (Location)
|
||||
import UrlParser as Url exposing ((</>), Parser, parseHash, s, string)
|
||||
import Url exposing (Url)
|
||||
import Url.Parser as Parser exposing ((</>), Parser, map, oneOf, s, string, top)
|
||||
|
||||
|
||||
type Route
|
||||
@@ -15,17 +16,20 @@ type Route
|
||||
| Status
|
||||
|
||||
|
||||
matcher : Parser (Route -> a) a
|
||||
matcher =
|
||||
Url.oneOf
|
||||
[ Url.map Home (s "")
|
||||
, Url.map Message (s "m" </> string </> string)
|
||||
, Url.map Mailbox (s "m" </> string)
|
||||
, Url.map Monitor (s "monitor")
|
||||
, Url.map Status (s "status")
|
||||
]
|
||||
{-| Routes our application handles.
|
||||
-}
|
||||
routes : List (Parser (Route -> a) a)
|
||||
routes =
|
||||
[ map Home top
|
||||
, map Message (s "m" </> string </> string)
|
||||
, map Mailbox (s "m" </> string)
|
||||
, map Monitor (s "monitor")
|
||||
, map Status (s "status")
|
||||
]
|
||||
|
||||
|
||||
{-| Convert route to a URI.
|
||||
-}
|
||||
routeToString : Route -> String
|
||||
routeToString page =
|
||||
let
|
||||
@@ -49,37 +53,35 @@ routeToString page =
|
||||
Status ->
|
||||
[ "status" ]
|
||||
in
|
||||
"/#/" ++ String.join "/" pieces
|
||||
"/" ++ String.join "/" pieces
|
||||
|
||||
|
||||
|
||||
-- PUBLIC HELPERS
|
||||
|
||||
|
||||
href : Route -> Attribute msg
|
||||
href route =
|
||||
href : Key -> Route -> Attribute msg
|
||||
href key route =
|
||||
Attr.href (routeToString route)
|
||||
|
||||
|
||||
modifyUrl : Route -> Cmd msg
|
||||
modifyUrl =
|
||||
routeToString >> Navigation.modifyUrl
|
||||
modifyUrl : Key -> Route -> Cmd msg
|
||||
modifyUrl key =
|
||||
routeToString >> Navigation.replaceUrl key
|
||||
|
||||
|
||||
newUrl : Route -> Cmd msg
|
||||
newUrl =
|
||||
routeToString >> Navigation.newUrl
|
||||
newUrl : Key -> Route -> Cmd msg
|
||||
newUrl key =
|
||||
routeToString >> Navigation.pushUrl key
|
||||
|
||||
|
||||
fromLocation : Location -> Route
|
||||
fromLocation location =
|
||||
if String.isEmpty location.hash then
|
||||
Home
|
||||
{-| Returns the Route for a given URL.
|
||||
-}
|
||||
fromUrl : Url -> Route
|
||||
fromUrl location =
|
||||
case Parser.parse (oneOf routes) location of
|
||||
Nothing ->
|
||||
Unknown location.path
|
||||
|
||||
else
|
||||
case parseHash matcher location of
|
||||
Nothing ->
|
||||
Unknown location.hash
|
||||
|
||||
Just route ->
|
||||
route
|
||||
Just route ->
|
||||
route
|
||||
|
||||
+26
-18
@@ -10,7 +10,9 @@ import Html.Attributes
|
||||
, href
|
||||
, id
|
||||
, placeholder
|
||||
, rel
|
||||
, selected
|
||||
, target
|
||||
, type_
|
||||
, value
|
||||
)
|
||||
@@ -39,10 +41,11 @@ frame controls session page content =
|
||||
div [ id "app" ]
|
||||
[ header []
|
||||
[ ul [ class "navbar", attribute "role" "navigation" ]
|
||||
[ li [ id "navbar-brand" ] [ a [ Route.href Route.Home ] [ text "@ inbucket" ] ]
|
||||
, navbarLink page Route.Monitor [ text "Monitor" ]
|
||||
, navbarLink page Route.Status [ text "Status" ]
|
||||
, navbarRecent page controls
|
||||
[ li [ id "navbar-brand" ]
|
||||
[ a [ Route.href session.key Route.Home ] [ text "@ inbucket" ] ]
|
||||
, navbarLink session page Route.Monitor [ text "Monitor" ]
|
||||
, navbarLink session page Route.Status [ text "Status" ]
|
||||
, navbarRecent session page controls
|
||||
, li [ id "navbar-mailbox" ]
|
||||
[ form [ Events.onSubmit (controls.viewMailbox controls.mailboxValue) ]
|
||||
[ input
|
||||
@@ -61,33 +64,35 @@ frame controls session page content =
|
||||
, content
|
||||
, 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 "
|
||||
, a [ href "https://github.com/jhillyerd/inbucket" ] [ text "GitHub" ]
|
||||
, externalLink "https://github.com/jhillyerd/inbucket" "GitHub"
|
||||
, text "."
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
navbarLink : ActivePage -> Route -> List (Html a) -> Html a
|
||||
navbarLink page route linkContent =
|
||||
externalLink : String -> String -> Html a
|
||||
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 ) ] ]
|
||||
[ a [ Route.href route ] linkContent ]
|
||||
[ a [ Route.href session.key route ] linkContent ]
|
||||
|
||||
|
||||
{-| Renders list of recent mailboxes, selecting the currently active mailbox.
|
||||
-}
|
||||
navbarRecent : ActivePage -> FrameControls msg -> Html msg
|
||||
navbarRecent page controls =
|
||||
navbarRecent : Session -> ActivePage -> FrameControls msg -> Html msg
|
||||
navbarRecent session page controls =
|
||||
let
|
||||
recentItemLink mailbox =
|
||||
a [ Route.href (Route.Mailbox mailbox) ] [ text mailbox ]
|
||||
|
||||
active =
|
||||
page == Mailbox
|
||||
|
||||
-- Navbar tab title, is current mailbox when active.
|
||||
-- Recent tab title is the name of the current mailbox when active.
|
||||
title =
|
||||
if active then
|
||||
controls.recentActive
|
||||
@@ -95,20 +100,23 @@ navbarRecent page controls =
|
||||
else
|
||||
"Recent Mailboxes"
|
||||
|
||||
-- Items to show in recent list, doesn't include active mailbox.
|
||||
items =
|
||||
-- Mailboxes to show in recent list, doesn't include active mailbox.
|
||||
recentMailboxes =
|
||||
if active then
|
||||
List.tail controls.recentOptions |> Maybe.withDefault []
|
||||
|
||||
else
|
||||
controls.recentOptions
|
||||
|
||||
recentLink mailbox =
|
||||
a [ Route.href session.key (Route.Mailbox mailbox) ] [ text mailbox ]
|
||||
in
|
||||
li
|
||||
[ id "navbar-recent"
|
||||
, classList [ ( "navbar-dropdown", True ), ( "navbar-active", active ) ]
|
||||
]
|
||||
[ 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 } from './Main.elm'
|
||||
import registerServiceWorker from './registerServiceWorker'
|
||||
import { Elm } from './Main.elm'
|
||||
import registerMonitorPorts from './registerMonitor'
|
||||
import './renderedHtml'
|
||||
|
||||
// App startup.
|
||||
var app = Main.embed(document.getElementById('root'), sessionObject())
|
||||
var app = Elm.Main.init({
|
||||
node: document.getElementById('root'),
|
||||
flags: sessionObject()
|
||||
})
|
||||
|
||||
// Message monitor.
|
||||
registerMonitorPorts(app)
|
||||
@@ -31,10 +34,3 @@ function sessionObject() {
|
||||
}
|
||||
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