Skip to content

Commit b950d2f

Browse files
committed
Refactor fetching of server config and settings.
The earlier approach of loading `/api/config.js` as a script on initial page load with the necessary variables to init the UI is ditched. Instead, it's now `/api/config` and `/api/settings` like all other API calls. On load of the frontend, these two resources are fetched and the frontend is initialised.
1 parent b6dcf2c commit b950d2f

File tree

16 files changed

+142
-166
lines changed

16 files changed

+142
-166
lines changed

cmd/admin.go

Lines changed: 12 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package main
22

33
import (
4-
"bytes"
5-
"encoding/json"
64
"fmt"
75
"net/http"
86
"sort"
@@ -13,41 +11,29 @@ import (
1311
"github.com/labstack/echo"
1412
)
1513

16-
type configScript struct {
17-
RootURL string `json:"rootURL"`
18-
FromEmail string `json:"fromEmail"`
19-
Messengers []string `json:"messengers"`
20-
MediaProvider string `json:"mediaProvider"`
21-
NeedsRestart bool `json:"needsRestart"`
22-
Update *AppUpdate `json:"update"`
23-
Langs []i18nLang `json:"langs"`
24-
EnablePublicSubPage bool `json:"enablePublicSubscriptionPage"`
25-
Lang json.RawMessage `json:"lang"`
14+
type serverConfig struct {
15+
Messengers []string `json:"messengers"`
16+
Langs []i18nLang `json:"langs"`
17+
Lang string `json:"lang"`
18+
Update *AppUpdate `json:"update"`
19+
NeedsRestart bool `json:"needs_restart"`
2620
}
2721

28-
// handleGetConfigScript returns general configuration as a Javascript
29-
// variable that can be included in an HTML page directly.
30-
func handleGetConfigScript(c echo.Context) error {
22+
// handleGetServerConfig returns general server config.
23+
func handleGetServerConfig(c echo.Context) error {
3124
var (
3225
app = c.Get("app").(*App)
33-
out = configScript{
34-
RootURL: app.constants.RootURL,
35-
FromEmail: app.constants.FromEmail,
36-
MediaProvider: app.constants.MediaProvider,
37-
EnablePublicSubPage: app.constants.EnablePublicSubPage,
38-
}
26+
out = serverConfig{}
3927
)
4028

4129
// Language list.
42-
langList, err := geti18nLangList(app.constants.Lang, app)
30+
langList, err := getI18nLangList(app.constants.Lang, app)
4331
if err != nil {
4432
return echo.NewHTTPError(http.StatusInternalServerError,
4533
fmt.Sprintf("Error loading language list: %v", err))
4634
}
4735
out.Langs = langList
48-
49-
// Current language.
50-
out.Lang = json.RawMessage(app.i18n.JSON())
36+
out.Lang = app.constants.Lang
5137

5238
// Sort messenger names with `email` always as the first item.
5339
var names []string
@@ -66,17 +52,7 @@ func handleGetConfigScript(c echo.Context) error {
6652
out.Update = app.update
6753
app.Unlock()
6854

69-
// Write the Javascript variable opening;
70-
b := bytes.Buffer{}
71-
b.Write([]byte(`var CONFIG = `))
72-
73-
// Encode the config payload as JSON and write as the variable's value assignment.
74-
j := json.NewEncoder(&b)
75-
if err := j.Encode(out); err != nil {
76-
return echo.NewHTTPError(http.StatusInternalServerError,
77-
app.i18n.Ts("admin.errorMarshallingConfig", "error", err.Error()))
78-
}
79-
return c.Blob(http.StatusOK, "application/javascript; charset=utf-8", b.Bytes())
55+
return c.JSON(http.StatusOK, okResp{out})
8056
}
8157

8258
// handleGetDashboardCharts returns chart data points to render ont he dashboard.

cmd/handlers.go

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package main
22

33
import (
44
"crypto/subtle"
5-
"encoding/json"
6-
"fmt"
75
"net/http"
86
"net/url"
97
"regexp"
@@ -44,8 +42,8 @@ func registerHTTPHandlers(e *echo.Echo) {
4442
g := e.Group("", middleware.BasicAuth(basicAuth))
4543
g.GET("/", handleIndexPage)
4644
g.GET("/api/health", handleHealthCheck)
47-
g.GET("/api/config.js", handleGetConfigScript)
48-
g.GET("/api/lang/:lang", handleLoadLanguage)
45+
g.GET("/api/config", handleGetServerConfig)
46+
g.GET("/api/lang/:lang", handleGetI18nLang)
4947
g.GET("/api/dashboard/charts", handleGetDashboardCharts)
5048
g.GET("/api/dashboard/counts", handleGetDashboardCounts)
5149

@@ -164,23 +162,6 @@ func handleHealthCheck(c echo.Context) error {
164162
return c.JSON(http.StatusOK, okResp{true})
165163
}
166164

167-
// handleLoadLanguage returns the JSON language pack given the language code.
168-
func handleLoadLanguage(c echo.Context) error {
169-
app := c.Get("app").(*App)
170-
171-
lang := c.Param("lang")
172-
if len(lang) > 6 || reLangCode.MatchString(lang) {
173-
return echo.NewHTTPError(http.StatusBadRequest, "Invalid language code.")
174-
}
175-
176-
b, err := app.fs.Read(fmt.Sprintf("/lang/%s.json", lang))
177-
if err != nil {
178-
return echo.NewHTTPError(http.StatusBadRequest, "Unknown language.")
179-
}
180-
181-
return c.JSON(http.StatusOK, okResp{json.RawMessage(b)})
182-
}
183-
184165
// basicAuth middleware does an HTTP BasicAuth authentication for admin handlers.
185166
func basicAuth(username, password string, c echo.Context) (bool, error) {
186167
app := c.Get("app").(*App)

cmd/i18n.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ package main
33
import (
44
"encoding/json"
55
"fmt"
6+
"net/http"
7+
8+
"github.com/knadh/listmonk/internal/i18n"
9+
"github.com/knadh/stuffbin"
10+
"github.com/labstack/echo"
611
)
712

813
type i18nLang struct {
@@ -15,8 +20,25 @@ type i18nLangRaw struct {
1520
Name string `json:"_.name"`
1621
}
1722

18-
// geti18nLangList returns the list of available i18n languages.
19-
func geti18nLangList(lang string, app *App) ([]i18nLang, error) {
23+
// handleGetI18nLang returns the JSON language pack given the language code.
24+
func handleGetI18nLang(c echo.Context) error {
25+
app := c.Get("app").(*App)
26+
27+
lang := c.Param("lang")
28+
if len(lang) > 6 || reLangCode.MatchString(lang) {
29+
return echo.NewHTTPError(http.StatusBadRequest, "Invalid language code.")
30+
}
31+
32+
i, err := getI18nLang(lang, app.fs)
33+
if err != nil {
34+
return echo.NewHTTPError(http.StatusBadRequest, "Unknown language.")
35+
}
36+
37+
return c.JSON(http.StatusOK, okResp{json.RawMessage(i.JSON())})
38+
}
39+
40+
// getI18nLangList returns the list of available i18n languages.
41+
func getI18nLangList(lang string, app *App) ([]i18nLang, error) {
2042
list, err := app.fs.Glob("/i18n/*.json")
2143
if err != nil {
2244
return nil, err
@@ -42,3 +64,30 @@ func geti18nLangList(lang string, app *App) ([]i18nLang, error) {
4264

4365
return out, nil
4466
}
67+
68+
func getI18nLang(lang string, fs stuffbin.FileSystem) (*i18n.I18n, error) {
69+
const def = "en"
70+
71+
b, err := fs.Read(fmt.Sprintf("/i18n/%s.json", def))
72+
if err != nil {
73+
return nil, fmt.Errorf("error reading default i18n language file: %s: %v", def, err)
74+
}
75+
76+
// Initialize with the default language.
77+
i, err := i18n.New(b)
78+
if err != nil {
79+
return nil, fmt.Errorf("error unmarshalling i18n language: %v", err)
80+
}
81+
82+
// Load the selected language on top of it.
83+
b, err = fs.Read(fmt.Sprintf("/i18n/%s.json", lang))
84+
if err != nil {
85+
return nil, fmt.Errorf("error reading i18n language file: %v", err)
86+
}
87+
88+
if err := i.Load(b); err != nil {
89+
return nil, fmt.Errorf("error loading i18n language file: %v", err)
90+
}
91+
92+
return i, nil
93+
}

cmd/init.go

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -262,28 +262,10 @@ func initConstants() *constants {
262262
// and then the selected language is loaded on top of it so that if there are
263263
// missing translations in it, the default English translations show up.
264264
func initI18n(lang string, fs stuffbin.FileSystem) *i18n.I18n {
265-
const def = "en"
266-
267-
b, err := fs.Read(fmt.Sprintf("/i18n/%s.json", def))
265+
i, err := getI18nLang(lang, fs)
268266
if err != nil {
269-
lo.Fatalf("error reading default i18n language file: %s: %v", def, err)
267+
lo.Fatal(err)
270268
}
271-
272-
// Initialize with the default language.
273-
i, err := i18n.New(b)
274-
if err != nil {
275-
lo.Fatalf("error unmarshalling i18n language: %v", err)
276-
}
277-
278-
// Load the selected language on top of it.
279-
b, err = fs.Read(fmt.Sprintf("/i18n/%s.json", lang))
280-
if err != nil {
281-
lo.Fatalf("error reading i18n language file: %v", err)
282-
}
283-
if err := i.Load(b); err != nil {
284-
lo.Fatalf("error loading i18n language file: %v", err)
285-
}
286-
287269
return i
288270
}
289271

frontend/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ It's best if the `listmonk/frontend` directory is opened in an IDE as a separate
55
For developer setup instructions, refer to the main project's README.
66

77
## Globals
8-
`main.js` is where Buefy is injected globally into Vue. In addition two controllers, `$api` (collection of API calls from `api/index.js`), `$utils` (util functions from `util.js`), `$serverConfig` (loaded form /api/config.js) are also attached globaly to Vue. They are accessible within Vue as `this.$api` and `this.$utils`.
8+
In `main.js`, Buefy and vue-i18n are attached globally. In addition:
9+
10+
- `$api` (collection of API calls from `api/index.js`)
11+
- `$utils` (util functions from `util.js`). They are accessible within Vue as `this.$api` and `this.$utils`.
912

1013
Some constants are defined in `constants.js`.
1114

@@ -14,7 +17,7 @@ The project uses a global `vuex` state to centrally store the responses to prett
1417

1518
There is a global state `loading` (eg: loading.campaigns, loading.lists) that indicates whether an API call for that particular "model" is running. This can be used anywhere in the project to show loading spinners for instance. All the API definitions are in `api/index.js`. It also describes how each API call sets the global `loading` status alongside storing the API responses.
1619

17-
*IMPORTANT*: All JSON field names in GET API responses are automatically camel-cased when they're pulled for the sake of consistentcy in the frontend code and for complying with the linter spec in the project (Vue/AirBnB schema). For example, `content_type` becomes `contentType`. When sending responses to the backend, however, they should be snake-cased manually.
20+
*IMPORTANT*: All JSON field names in GET API responses are automatically camel-cased when they're pulled for the sake of consistentcy in the frontend code and for complying with the linter spec in the project (Vue/AirBnB schema). For example, `content_type` becomes `contentType`. When sending responses to the backend, however, they should be snake-cased manually. This is overridden for certain calls such as `/api/config` and `/api/settings` using the `preserveCase: true` param in `api/index.js`.
1821

1922

2023
## Icon pack

frontend/public/index.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
<link rel="icon" href="<%= BASE_URL %>frontend/favicon.png" />
88
<link href="https://fonts.googleapis.com/css?family=Inter:400,600" rel="stylesheet" />
99
<title><%= htmlWebpackPlugin.options.title %></title>
10-
<script src="<%= BASE_URL %>api/config.js" id="server-config"></script>
1110
</head>
1211
<body>
1312
<noscript>

frontend/src/App.vue

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div id="app">
3-
<b-navbar :fixed-top="true">
3+
<b-navbar :fixed-top="true" v-if="$root.isLoaded">
44
<template slot="brand">
55
<div class="logo">
66
<router-link :to="{name: 'dashboard'}">
@@ -14,7 +14,7 @@
1414
</template>
1515
</b-navbar>
1616

17-
<div class="wrapper">
17+
<div class="wrapper" v-if="$root.isLoaded">
1818
<section class="sidebar">
1919
<b-sidebar
2020
position="static"
@@ -100,18 +100,17 @@
100100

101101
<!-- body //-->
102102
<div class="main">
103-
<div class="global-notices" v-if="serverConfig.needsRestart || serverConfig.update">
104-
<div v-if="serverConfig.needsRestart" class="notification is-danger">
105-
Settings have changed. Pause all running campaigns and restart the app
103+
<div class="global-notices" v-if="serverConfig.needs_restart || serverConfig.update">
104+
<div v-if="serverConfig.needs_restart" class="notification is-danger">
105+
{{ $t('settings.needsRestart') }}
106106
&mdash;
107107
<b-button class="is-primary" size="is-small"
108-
@click="$utils.confirm(
109-
'Ensure running campaigns are paused. Restart?', reloadApp)">
110-
Restart
108+
@click="$utils.confirm($t('settings.confirmRestart'), reloadApp)">
109+
{{ $t('settings.restart') }}
111110
</b-button>
112111
</div>
113112
<div v-if="serverConfig.update" class="notification is-success">
114-
A new update ({{ serverConfig.update.version }}) is available.
113+
{{ $t('settings.updateAvailable', { version: serverConfig.update.version }) }}
115114
<a :href="serverConfig.update.url" target="_blank">View</a>
116115
</div>
117116
</div>
@@ -120,15 +119,7 @@
120119
</div>
121120
</div>
122121

123-
<b-loading v-if="!isLoaded" active>
124-
<div class="has-text-centered">
125-
<h1 class="title">Oops</h1>
126-
<p>
127-
Can't connect to the backend.<br />
128-
Make sure the server is running and refresh this page.
129-
</p>
130-
</div>
131-
</b-loading>
122+
<b-loading v-if="!$root.isLoaded" active />
132123
</div>
133124
</template>
134125

@@ -143,7 +134,6 @@ export default Vue.extend({
143134
return {
144135
activeItem: {},
145136
activeGroup: {},
146-
isLoaded: window.CONFIG,
147137
};
148138
},
149139

frontend/src/api/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,17 @@ export const deleteTemplate = async (id) => http.delete(`/api/templates/${id}`,
195195
{ loading: models.templates });
196196

197197
// Settings.
198+
export const getServerConfig = async () => http.get('/api/config',
199+
{ loading: models.serverConfig, store: models.serverConfig, preserveCase: true });
200+
198201
export const getSettings = async () => http.get('/api/settings',
199-
{ loading: models.settings, preserveCase: true });
202+
{ loading: models.settings, store: models.settings, preserveCase: true });
200203

201204
export const updateSettings = async (data) => http.put('/api/settings', data,
202205
{ loading: models.settings });
203206

204207
export const getLogs = async () => http.get('/api/logs',
205208
{ loading: models.logs });
209+
210+
export const getLang = async (lang) => http.get(`/api/lang/${lang}`,
211+
{ loading: models.lang, preserveCase: true });

frontend/src/constants.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
export const models = Object.freeze({
2-
// This is the config loaded from /api/config.js directly onto the page
3-
// via a <script> tag.
42
serverConfig: 'serverConfig',
5-
3+
lang: 'lang',
64
dashboard: 'dashboard',
75
lists: 'lists',
86
subscribers: 'subscribers',

0 commit comments

Comments
 (0)