Load global configuration into the frontend as a JS dict using a <script> inclusion

This commit is contained in:
Kailash Nadh 2018-11-02 23:33:00 +05:30
parent 09b7fc8d0c
commit ad8787cab3
7 changed files with 41 additions and 32 deletions

View File

@ -1,13 +1,43 @@
package main package main
import ( import (
"bytes"
"encoding/json"
"net/http" "net/http"
"github.com/labstack/echo" "github.com/labstack/echo"
) )
type configScript struct {
RootURL string `json:"rootURL"`
UploadURI string `json:"uploadURL"`
FromEmail string `json:"fromEmail"`
Messengers []string `json:"messengers"`
}
// handleGetStats returns a collection of general statistics. // handleGetStats returns a collection of general statistics.
func handleGetStats(c echo.Context) error { func handleGetStats(c echo.Context) error {
app := c.Get("app").(*App) app := c.Get("app").(*App)
return c.JSON(http.StatusOK, okResp{app.Runner.GetMessengerNames()}) return c.JSON(http.StatusOK, okResp{app.Runner.GetMessengerNames()})
} }
// handleGetConfigScript returns general configuration as a Javascript
// variable that can be included in an HTML page directly.
func handleGetConfigScript(c echo.Context) error {
var (
app = c.Get("app").(*App)
out = configScript{
RootURL: app.Constants.RootURL,
UploadURI: app.Constants.UploadURI,
FromEmail: app.Constants.FromEmail,
Messengers: app.Runner.GetMessengerNames(),
}
b = bytes.Buffer{}
j = json.NewEncoder(&b)
)
b.Write([]byte(`var CONFIG = `))
j.Encode(out)
return c.Blob(http.StatusOK, "application/javascript", b.Bytes())
}

View File

@ -401,12 +401,6 @@ func handleGetRunningCampaignStats(c echo.Context) error {
return c.JSON(http.StatusOK, okResp{out}) return c.JSON(http.StatusOK, okResp{out})
} }
// handleGetCampaignMessengers returns the list of registered messengers.
func handleGetCampaignMessengers(c echo.Context) error {
app := c.Get("app").(*App)
return c.JSON(http.StatusOK, okResp{app.Runner.GetMessengerNames()})
}
// handleTestCampaign handles the sending of a campaign message to // handleTestCampaign handles the sending of a campaign message to
// arbitrary subscribers for testing. // arbitrary subscribers for testing.
func handleTestCampaign(c echo.Context) error { func handleTestCampaign(c echo.Context) error {

View File

@ -3,8 +3,8 @@
# Interface and port where the app will run its webserver. # Interface and port where the app will run its webserver.
address = "0.0.0.0:9000" address = "0.0.0.0:9000"
# Root to the listmonk installation that'll be used in the e-mails for linking to # Public root URL of the listmonk installation that'll be used
# images, the unsubscribe URL etc. # in the messages for linking to images, unsubscribe page etc.
root = "http://listmonk.mysite.com" root = "http://listmonk.mysite.com"
# The default 'from' e-mail for outgoing e-mail campaigns. # The default 'from' e-mail for outgoing e-mail campaigns.

View File

@ -4,21 +4,9 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000"> <meta name="theme-color" content="#000000">
<!-- <script src="%PUBLIC_URL%/api/config.js" type="text/javascript"></script>
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="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title> <title>React App</title>
</head> </head>
<body> <body>

View File

@ -118,6 +118,7 @@ class App extends React.PureComponent {
<Layout modelRequest={ this.modelRequest } <Layout modelRequest={ this.modelRequest }
request={ this.request } request={ this.request }
reqStates={ this.state.reqStates } reqStates={ this.state.reqStates }
config={ window.CONFIG }
data={ this.state.data } /> data={ this.state.data } />
</BrowserRouter> </BrowserRouter>
) )

View File

@ -298,7 +298,7 @@ class TheFormDef extends React.PureComponent {
</Form.Item> </Form.Item>
<Form.Item {...formItemLayout} label="From address"> <Form.Item {...formItemLayout} label="From address">
{getFieldDecorator("from_email", { {getFieldDecorator("from_email", {
initialValue: record.from_email, initialValue: record.from_email ? record.from_email : this.props.config.fromEmail,
rules: [{ required: true }, { validator: this.validateEmail }] rules: [{ required: true }, { validator: this.validateEmail }]
})(<Input disabled={ this.props.formDisabled } placeholder="Company Name <email@company.com>" maxLength="200" />)} })(<Input disabled={ this.props.formDisabled } placeholder="Company Name <email@company.com>" maxLength="200" />)}
</Form.Item> </Form.Item>
@ -325,10 +325,10 @@ class TheFormDef extends React.PureComponent {
<Select disabled={ this.props.formDisabled } mode="tags"></Select> <Select disabled={ this.props.formDisabled } mode="tags"></Select>
)} )}
</Form.Item> </Form.Item>
<Form.Item {...formItemLayout} label="Messenger"> <Form.Item {...formItemLayout} label="Messenger" style={{ display: this.props.config.messengers.length === 1 ? "none" : "block" }}>
{getFieldDecorator("messenger", { initialValue: record.messenger ? record.messenger : "email" })( {getFieldDecorator("messenger", { initialValue: record.messenger ? record.messenger : "email" })(
<Radio.Group className="messengers"> <Radio.Group className="messengers">
{[...this.props.messengers].map((v, i) => {[...this.props.config.messengers].map((v, i) =>
<Radio disabled={ this.props.formDisabled } value={v} key={v}>{ v }</Radio> <Radio disabled={ this.props.formDisabled } value={v} key={v}>{ v }</Radio>
)} )}
</Radio.Group> </Radio.Group>
@ -395,7 +395,6 @@ class Campaign extends React.PureComponent {
campaignID: this.props.route.match.params ? parseInt(this.props.route.match.params.campaignID, 10) : 0, campaignID: this.props.route.match.params ? parseInt(this.props.route.match.params.campaignID, 10) : 0,
record: {}, record: {},
contentType: "richtext", contentType: "richtext",
messengers: [],
previewRecord: null, previewRecord: null,
body: "", body: "",
currentTab: "form", currentTab: "form",
@ -412,14 +411,11 @@ class Campaign extends React.PureComponent {
// Fetch templates. // Fetch templates.
this.props.modelRequest(cs.ModelTemplates, cs.Routes.GetTemplates, cs.MethodGet) this.props.modelRequest(cs.ModelTemplates, cs.Routes.GetTemplates, cs.MethodGet)
// Fetch messengers.
this.props.request(cs.Routes.GetCampaignMessengers, cs.MethodGet).then((r) => {
this.setState({ messengers: r.data.data, loading: false })
})
// Fetch campaign. // Fetch campaign.
if(this.state.campaignID) { if(this.state.campaignID) {
this.fetchRecord(this.state.campaignID) this.fetchRecord(this.state.campaignID)
} else {
this.setState({ loading: false })
} }
} }
@ -482,7 +478,6 @@ class Campaign extends React.PureComponent {
<TheForm { ...this.props } <TheForm { ...this.props }
record={ this.state.record } record={ this.state.record }
isSingle={ this.state.record.id ? true : false } isSingle={ this.state.record.id ? true : false }
messengers={ this.state.messengers }
body={ this.state.body ? this.state.body : this.state.record.body } body={ this.state.body ? this.state.body : this.state.record.body }
contentType={ this.state.contentType } contentType={ this.state.contentType }
formDisabled={ this.state.formDisabled } formDisabled={ this.state.formDisabled }

View File

@ -26,6 +26,7 @@ type constants struct {
RootURL string `mapstructure:"root"` RootURL string `mapstructure:"root"`
UploadPath string `mapstructure:"upload_path"` UploadPath string `mapstructure:"upload_path"`
UploadURI string `mapstructure:"upload_uri"` UploadURI string `mapstructure:"upload_uri"`
FromEmail string `mapstructure:"from_email"`
} }
// App contains the "global" components that are // App contains the "global" components that are
@ -77,6 +78,7 @@ func init() {
// registerHandlers registers HTTP handlers. // registerHandlers registers HTTP handlers.
func registerHandlers(e *echo.Echo) { func registerHandlers(e *echo.Echo) {
e.GET("/", handleIndexPage) e.GET("/", handleIndexPage)
e.GET("/api/config.js", handleGetConfigScript)
e.GET("/api/users", handleGetUsers) e.GET("/api/users", handleGetUsers)
e.POST("/api/users", handleCreateUser) e.POST("/api/users", handleCreateUser)
e.DELETE("/api/users/:id", handleDeleteUser) e.DELETE("/api/users/:id", handleDeleteUser)
@ -102,7 +104,6 @@ func registerHandlers(e *echo.Echo) {
e.GET("/api/campaigns", handleGetCampaigns) e.GET("/api/campaigns", handleGetCampaigns)
e.GET("/api/campaigns/running/stats", handleGetRunningCampaignStats) e.GET("/api/campaigns/running/stats", handleGetRunningCampaignStats)
e.GET("/api/campaigns/messengers", handleGetCampaignMessengers)
e.GET("/api/campaigns/:id", handleGetCampaigns) e.GET("/api/campaigns/:id", handleGetCampaigns)
e.GET("/api/campaigns/:id/preview", handlePreviewCampaign) e.GET("/api/campaigns/:id/preview", handlePreviewCampaign)
e.POST("/api/campaigns/:id/preview", handlePreviewCampaign) e.POST("/api/campaigns/:id/preview", handlePreviewCampaign)