mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-18 18:17:03 +00:00
ui: Re-implement websockets with ports+JS
This commit is contained in:
@@ -1 +1 @@
|
||||
<!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.d6280c5d.js"></script></body></html>
|
||||
<!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 +1 @@
|
||||
"use strict";var precacheConfig=[["/index.html","3beb8060f28c17789ca66e2c6616160f"],["/static/css/main.8d438738.css","8d438738f900913d8f787dc7ef9b05f9"],["/static/js/main.d6280c5d.js","55b008360cb9147787b0a8ed62c0783d"]],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)}))}});
|
||||
"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)}))}});
|
||||
1
ui/build/static/js/main.04b41a91.js
Normal file
1
ui/build/static/js/main.04b41a91.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"main":{"js":"/static/js/main.d6280c5d.js","css":"/static/css/main.8d438738.css"},"":{"html":"/index.html"}}
|
||||
{"main":{"js":"/static/js/main.04b41a91.js","css":"/static/css/main.8d438738.css"},"":{"html":"/index.html"}}
|
||||
@@ -27,7 +27,6 @@
|
||||
"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",
|
||||
"elm-lang/websocket": "1.0.2 <= v < 2.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"
|
||||
|
||||
@@ -180,6 +180,8 @@ updatePage msg model =
|
||||
|
||||
setRoute : Route -> Model -> ( Model, Cmd Msg, Session.Msg )
|
||||
setRoute route model =
|
||||
let
|
||||
( newModel, newCmd, newSession ) =
|
||||
case route of
|
||||
Route.Unknown hash ->
|
||||
( model, Cmd.none, Session.SetFlash ("Unknown route requested: " ++ hash) )
|
||||
@@ -215,8 +217,12 @@ setRoute route model =
|
||||
)
|
||||
|
||||
Route.Monitor ->
|
||||
( { model | page = Monitor (Monitor.init model.session.host) }
|
||||
, Ports.windowTitle "Inbucket Monitor"
|
||||
let
|
||||
( subModel, subCmd ) =
|
||||
Monitor.init
|
||||
in
|
||||
( { model | page = Monitor subModel }
|
||||
, Cmd.map MonitorMsg subCmd
|
||||
, Session.none
|
||||
)
|
||||
|
||||
@@ -228,6 +234,14 @@ setRoute route model =
|
||||
]
|
||||
, Session.none
|
||||
)
|
||||
in
|
||||
case model.page of
|
||||
Monitor _ ->
|
||||
-- Leaving Monitor page, shut down the web socket.
|
||||
( newModel, Cmd.batch [ Ports.monitorCommand False, newCmd ], newSession )
|
||||
|
||||
_ ->
|
||||
( newModel, newCmd, newSession )
|
||||
|
||||
|
||||
applySession : ( Model, Cmd Msg, Session.Msg ) -> ( Model, Cmd Msg )
|
||||
|
||||
@@ -15,25 +15,28 @@ import DateFormat
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events as Events
|
||||
import Json.Decode exposing (decodeString)
|
||||
import Json.Decode as D
|
||||
import Ports
|
||||
import Route
|
||||
import WebSocket
|
||||
|
||||
|
||||
-- MODEL
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ wsUrl : String
|
||||
{ connected : Bool
|
||||
, messages : List MessageHeader
|
||||
}
|
||||
|
||||
|
||||
init : String -> Model
|
||||
init host =
|
||||
{ wsUrl = "ws://" ++ host ++ "/api/v1/monitor/messages"
|
||||
, messages = []
|
||||
}
|
||||
init : ( Model, Cmd Msg )
|
||||
init =
|
||||
( Model False []
|
||||
, Cmd.batch
|
||||
[ Ports.windowTitle "Inbucket Monitor"
|
||||
, Ports.monitorCommand True
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -42,7 +45,16 @@ init host =
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions model =
|
||||
WebSocket.listen model.wsUrl (decodeString MessageHeader.decoder >> NewMessage)
|
||||
let
|
||||
monitorMessage =
|
||||
D.oneOf
|
||||
[ D.map Message MessageHeader.decoder
|
||||
, D.map Connected D.bool
|
||||
]
|
||||
|> D.decodeValue
|
||||
|> Ports.monitorMessage
|
||||
in
|
||||
Sub.map MonitorResult monitorMessage
|
||||
|
||||
|
||||
|
||||
@@ -50,17 +62,25 @@ subscriptions model =
|
||||
|
||||
|
||||
type Msg
|
||||
= NewMessage (Result String MessageHeader)
|
||||
= MonitorResult (Result String 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
|
||||
NewMessage (Ok msg) ->
|
||||
MonitorResult (Ok (Connected status)) ->
|
||||
( { model | connected = status }, Cmd.none, Session.none )
|
||||
|
||||
MonitorResult (Ok (Message msg)) ->
|
||||
( { model | messages = msg :: model.messages }, Cmd.none, Session.none )
|
||||
|
||||
NewMessage (Err err) ->
|
||||
MonitorResult (Err err) ->
|
||||
( model, Cmd.none, Session.SetFlash err )
|
||||
|
||||
OpenMessage msg ->
|
||||
@@ -78,7 +98,17 @@ view : Session -> Model -> Html Msg
|
||||
view session model =
|
||||
div [ id "page" ]
|
||||
[ h1 [] [ text "Monitor" ]
|
||||
, p [] [ text "Messages will be listed here shortly after delivery." ]
|
||||
, p []
|
||||
[ text "Messages will be listed here shortly after delivery. "
|
||||
, em []
|
||||
[ text
|
||||
(if model.connected then
|
||||
"Connected."
|
||||
else
|
||||
"Disconnected!"
|
||||
)
|
||||
]
|
||||
]
|
||||
, table [ id "monitor" ]
|
||||
[ thead []
|
||||
[ th [] [ text "Date" ]
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
port module Ports exposing (onSessionChange, storeSession, windowTitle)
|
||||
port module Ports
|
||||
exposing
|
||||
( monitorCommand
|
||||
, monitorMessage
|
||||
, onSessionChange
|
||||
, storeSession
|
||||
, windowTitle
|
||||
)
|
||||
|
||||
import Data.Session exposing (Persistent)
|
||||
import Json.Encode exposing (Value)
|
||||
|
||||
|
||||
port monitorCommand : Bool -> Cmd msg
|
||||
|
||||
|
||||
port monitorMessage : (Value -> msg) -> Sub msg
|
||||
|
||||
|
||||
port onSessionChange : (Value -> msg) -> Sub msg
|
||||
|
||||
|
||||
|
||||
@@ -1,33 +1,40 @@
|
||||
import './main.css';
|
||||
import { Main } from './Main.elm';
|
||||
import registerServiceWorker from './registerServiceWorker';
|
||||
import './main.css'
|
||||
import { Main } from './Main.elm'
|
||||
import registerServiceWorker from './registerServiceWorker'
|
||||
import registerMonitorPorts from './registerMonitor'
|
||||
|
||||
var app = Main.embed(document.getElementById('root'), sessionObject());
|
||||
// App startup.
|
||||
var app = Main.embed(document.getElementById('root'), sessionObject())
|
||||
|
||||
// Message monitor.
|
||||
registerMonitorPorts(app)
|
||||
|
||||
// Session storage.
|
||||
app.ports.storeSession.subscribe(function (session) {
|
||||
localStorage.session = JSON.stringify(session);
|
||||
});
|
||||
|
||||
app.ports.windowTitle.subscribe(function (title) {
|
||||
document.title = title;
|
||||
});
|
||||
localStorage.session = JSON.stringify(session)
|
||||
})
|
||||
|
||||
window.addEventListener("storage", function (event) {
|
||||
if (event.storageArea === localStorage && event.key === "session") {
|
||||
app.ports.onSessionChange.send(sessionObject());
|
||||
app.ports.onSessionChange.send(sessionObject())
|
||||
}
|
||||
}, false);
|
||||
}, false)
|
||||
|
||||
function sessionObject() {
|
||||
var s = localStorage.session;
|
||||
var s = localStorage.session
|
||||
try {
|
||||
if (s) {
|
||||
return JSON.parse(s);
|
||||
return JSON.parse(s)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error(error)
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
registerServiceWorker();
|
||||
// Window title.
|
||||
app.ports.windowTitle.subscribe(function (title) {
|
||||
document.title = title
|
||||
})
|
||||
|
||||
registerServiceWorker()
|
||||
|
||||
46
ui/src/registerMonitor.js
Normal file
46
ui/src/registerMonitor.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// Register the websocket listeners for the monitor API.
|
||||
export default function registerMonitorPorts(app) {
|
||||
var uri = '/api/v1/monitor/messages'
|
||||
var url = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + uri
|
||||
|
||||
// Current handler.
|
||||
var handler = null
|
||||
|
||||
app.ports.monitorCommand.subscribe(function (cmd) {
|
||||
if (handler != null) {
|
||||
handler.down()
|
||||
handler = null
|
||||
}
|
||||
if (cmd) {
|
||||
// Command is up.
|
||||
handler = websocketHandler(url, app.ports.monitorMessage)
|
||||
handler.up()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Creates a handler responsible for connecting, disconnecting from web socket.
|
||||
function websocketHandler(url, port) {
|
||||
var ws = null
|
||||
|
||||
return {
|
||||
up: () => {
|
||||
ws = new WebSocket(url)
|
||||
|
||||
ws.addEventListener('open', function (e) {
|
||||
port.send(true)
|
||||
})
|
||||
ws.addEventListener('close', function (e) {
|
||||
port.send(false)
|
||||
})
|
||||
ws.addEventListener('message', function (e) {
|
||||
var msg = JSON.parse(e.data)
|
||||
port.send(msg)
|
||||
})
|
||||
},
|
||||
|
||||
down: () => {
|
||||
ws.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user