1
0
mirror of https://github.com/axllent/mailpit.git synced 2025-04-17 12:06:22 +02:00

UI: Add about app modal with version update notification

This commit is contained in:
Ralph Slooten 2022-10-08 23:23:30 +13:00
parent 675704ca91
commit a31a7c3d2c
10 changed files with 146 additions and 67 deletions

View File

@ -42,4 +42,4 @@ jobs:
asset_name: mailpit-${{ matrix.goos }}-${{ matrix.goarch }}
extra_files: LICENSE README.md
md5sum: false
ldflags: -w -X "github.com/axllent/mailpit/cmd.Version=${{ steps.tag.outputs.tag }}"
ldflags: -w -X "github.com/axllent/mailpit/config.Version=${{ steps.tag.outputs.tag }}"

View File

@ -8,7 +8,7 @@ WORKDIR /app
RUN apk add --no-cache git npm && \
npm install && npm run package && \
CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/axllent/mailpit/cmd.Version=${VERSION}" -o /mailpit
CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/axllent/mailpit/config.Version=${VERSION}" -o /mailpit
FROM alpine:latest

View File

@ -5,21 +5,11 @@ import (
"os"
"runtime"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/updater"
"github.com/spf13/cobra"
)
var (
// Version is the default application version, updated on release
Version = "dev"
// Repo on Github for updater
Repo = "axllent/mailpit"
// RepoBinaryName on Github for updater
RepoBinaryName = "mailpit"
)
// versionCmd represents the version command
var versionCmd = &cobra.Command{
Use: "version",
@ -36,10 +26,10 @@ var versionCmd = &cobra.Command{
}
fmt.Printf("%s %s compiled with %s on %s/%s\n",
os.Args[0], Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
os.Args[0], config.Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
latest, _, _, err := updater.GithubLatest(Repo, RepoBinaryName)
if err == nil && updater.GreaterThan(latest, Version) {
latest, _, _, err := updater.GithubLatest(config.Repo, config.RepoBinaryName)
if err == nil && updater.GreaterThan(latest, config.Version) {
fmt.Printf(
"\nUpdate available: %s\nRun `%s version -u` to update (requires read/write access to install directory).\n",
latest,
@ -59,7 +49,7 @@ func init() {
}
func updateApp() error {
rel, err := updater.GithubUpdate(Repo, RepoBinaryName, Version)
rel, err := updater.GithubUpdate(config.Repo, config.RepoBinaryName, config.Version)
if err != nil {
return err
}

View File

@ -58,6 +58,15 @@ var (
// ContentSecurityPolicy for HTTP server
ContentSecurityPolicy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self'; img-src * data: blob:; font-src 'self' data:; media-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';"
// Version is the default application version, updated on release
Version = "dev"
// Repo on Github for updater
Repo = "axllent/mailpit"
// RepoBinaryName on Github for updater
RepoBinaryName = "mailpit"
)
// VerifyConfig wil do some basic checking

View File

@ -22,15 +22,6 @@ type MessagesResult struct {
Messages []data.Summary `json:"messages"`
}
// // Mailbox returns an message overview (stats)
// func Mailbox(w http.ResponseWriter, _ *http.Request) {
// res := storage.StatsGet()
// bytes, _ := json.Marshal(res)
// w.Header().Add("Content-Type", "application/json")
// _, _ = w.Write(bytes)
// }
// Messages returns a paginated list of messages
func Messages(w http.ResponseWriter, r *http.Request) {
start, limit := getStartLimit(r)
@ -171,34 +162,6 @@ func DeleteMessages(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("ok"))
}
// // DeleteMessage (method: DELETE) deletes a single message
// func DeleteMessage(w http.ResponseWriter, r *http.Request) {
// vars := mux.Vars(r)
// id := vars["id"]
// err := storage.DeleteOneMessage(id)
// if err != nil {
// httpError(w, err.Error())
// return
// }
// w.Header().Add("Content-Type", "text/plain")
// _, _ = w.Write([]byte("ok"))
// }
// SetAllRead (GET) will update all messages as read
// func SetAllRead(w http.ResponseWriter, r *http.Request) {
// err := storage.MarkAllRead()
// if err != nil {
// httpError(w, err.Error())
// return
// }
// w.Header().Add("Content-Type", "text/plain")
// _, _ = w.Write([]byte("ok"))
// }
// SetReadStatus (method: PUT) will update the status to Read/Unread for all provided IDs
func SetReadStatus(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)

52
server/apiv1/info.go Normal file
View File

@ -0,0 +1,52 @@
package apiv1
import (
"encoding/json"
"net/http"
"os"
"runtime"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/updater"
)
type appVersion struct {
Version string
LatestVersion string
Database string
DatabaseSize int64
Messages int
Memory uint64
}
// AppInfo returns some basic details about the running app, and latest release.
func AppInfo(w http.ResponseWriter, r *http.Request) {
info := appVersion{}
info.Version = config.Version
latest, _, _, err := updater.GithubLatest(config.Repo, config.RepoBinaryName)
if err == nil {
info.LatestVersion = latest
}
info.Database = config.DataFile
db, err := os.Stat(info.Database)
if err == nil {
info.DatabaseSize = db.Size()
}
info.Messages = storage.CountTotal()
var m runtime.MemStats
runtime.ReadMemStats(&m)
info.Memory = m.Sys - m.HeapReleased
bytes, _ := json.Marshal(info)
w.Header().Add("Content-Type", "application/json")
_, _ = w.Write(bytes)
}

View File

@ -66,6 +66,7 @@ func defaultRoutes() *mux.Router {
r.HandleFunc("/api/v1/message/{id}/part/{partID}", middleWareFunc(apiv1.DownloadAttachment)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}/part/{partID}/thumb", middleWareFunc(apiv1.Thumbnail)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}", middleWareFunc(apiv1.Message)).Methods("GET")
r.HandleFunc("/api/v1/info", middleWareFunc(apiv1.AppInfo)).Methods("GET")
return r
}

View File

@ -28,7 +28,8 @@ export default {
notificationsSupported: false,
notificationsEnabled: false,
selected: [],
tcStatus: 0
tcStatus: 0,
appInfo : false,
}
},
watch: {
@ -421,7 +422,7 @@ export default {
else if (Notification.permission !== "denied") {
let self = this;
Notification.requestPermission().then(function (permission) {
// If the user accepts, let's create a notification
// if the user accepts, let's create a notification
if (permission === "granted") {
self.browserNotify("Notifications enabled", "You will receive notifications when new mails are received.");
self.notificationsEnabled = true;
@ -479,6 +480,14 @@ export default {
isSelected: function(id) {
return this.selected.indexOf(id) != -1;
},
loadInfo: function() {
let self = this;
self.get('api/v1/info', false, function(response) {
self.appInfo = response.data;
self.modal('AppInfoModal').show();
});
}
}
}
@ -625,13 +634,9 @@ export default {
</a>
</li>
<li class="mt-5 position-fixed bottom-0 bg-white py-2 text-muted">
<a href="https://github.com/axllent/mailpit" target="_blank" class="text-muted me-1">
<i class="bi bi-github"></i>
GitHub
</a>
/
<a href="https://github.com/axllent/mailpit/wiki" target="_blank" class="text-muted ms-1">
Docs
<a href="#" class="text-muted" v-on:click="loadInfo">
<i class="bi bi-info-circle-fill"></i>
About
</a>
</li>
</ul>
@ -756,4 +761,59 @@ export default {
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="AppInfoModal" tabindex="-1" aria-labelledby="AppInfoModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header" v-if="appInfo">
<h5 class="modal-title" id="AppInfoModalLabel">
Mailpit
<code>({{ appInfo.Version }})</code>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<a class="btn btn-warning d-block mb-3" v-if="appInfo.Version != appInfo.LatestVersion"
:href="'https://github.com/axllent/mailpit/releases/tag/'+appInfo.LatestVersion">
A new version of Mailpit ({{ appInfo.LatestVersion }}) is available.
</a>
<div class="row g-3">
<div class="col-sm-6">
<a class="btn btn-primary w-100" href="https://github.com/axllent/mailpit" target="_blank">
<i class="bi bi-github"></i>
Github
<i class="bi bi-box-arrow-up-right"></i>
</a>
</div>
<div class="col-sm-6">
<a class="btn btn-primary w-100" href="https://github.com/axllent/mailpit/wiki" target="_blank">
Documentation
<i class="bi bi-box-arrow-up-right"></i>
</a>
</div>
<div class="col-sm-6">
<div class="card border-secondary text-center">
<div class="card-header">Database size</div>
<div class="card-body text-secondary">
<h5 class="card-title">{{ getFileSize(appInfo.DatabaseSize) }} </h5>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="card border-secondary text-center">
<div class="card-header">RAM usage</div>
<div class="card-body text-secondary">
<h5 class="card-title">{{ getFileSize(appInfo.Memory) }} </h5>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</template>

View File

@ -1,4 +1,6 @@
import axios from 'axios'
import axios from 'axios';
import { Modal } from 'bootstrap';
// FakeModal is used to return a fake Bootstrap modal
// if the ID returns nothing
@ -31,7 +33,7 @@ const commonMixins = {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
if (error.response.data.Error) {
alert(error.response.data.Error)
alert(error.response.data.Error);
} else {
alert(error.response.data);
}
@ -50,7 +52,7 @@ const commonMixins = {
modal: function (id) {
let e = document.getElementById(id);
if (e) {
return bootstrap.Modal.getOrCreateInstance(e);
return Modal.getOrCreateInstance(e);
}
// in case there are open/close actions
return new FakeModal();
@ -209,4 +211,4 @@ const commonMixins = {
}
export default commonMixins
export default commonMixins

View File

@ -95,6 +95,8 @@ func InitDB() error {
p = filepath.Clean(p)
}
config.DataFile = p
logger.Log().Debugf("[db] opening database %s", p)
var err error