mirror of
https://github.com/ohwgiles/laminar.git
synced 2024-10-27 20:34:20 +00:00
Add the ability to customize index.html (#113)
User may provide a custom index.html template file to be used instead of the built-in version. Changes to this file are watched by laminard using inotify in order to load and compress the custom file for gzip delivery, reusing the existing method for serving static assets. This feature obviates the custom css feature, so remove references from the manual and add a deprecation warning if it is used. Add a section to the UserManual describing how to use this feature and including a link to an example using Semantic UI.
This commit is contained in:
parent
f981491a34
commit
2e54773e83
@ -616,11 +616,11 @@ DESCRIPTION=Anything here will appear on the job page in the frontend <em>unesca
|
|||||||
|
|
||||||
Change `LAMINAR_TITLE` in `/etc/laminar.conf` to your preferred page title. Laminar must be restarted for this change to take effect.
|
Change `LAMINAR_TITLE` in `/etc/laminar.conf` to your preferred page title. Laminar must be restarted for this change to take effect.
|
||||||
|
|
||||||
## Custom stylesheet
|
## Custom HTML template
|
||||||
|
|
||||||
If it exists, the file `/var/lib/laminar/custom/style.css` will be served by laminar and may be used to change the appearance of Laminar's WebUI.
|
If it exists, the file `/var/lib/laminar/custom/index.html` will be served by laminar instead of the default markup that is bundled into the Laminar binary. This file can be used to change any aspect of Laminar's WebUI, for example adding menu links or adding a custom stylesheet. Any required assets will need to be served directly from your [HTTP reverse proxy](#Service-configuration) or other HTTP server.
|
||||||
|
|
||||||
This directory is also a good place to add any extra assets needed for this customization, but note that in this case you will need to serve this directory directly from your [HTTP reverse proxy](#Service-configuration) (highly recommended).
|
An example customization can be found at [cweagans/semantic-laminar-theme](https://github.com/cweagans/semantic-laminar-theme).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -297,3 +297,8 @@ void Http::notifyLog(std::string job, uint run, std::string log_chunk, bool eot)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Http::setHtmlTemplate(std::string tmpl)
|
||||||
|
{
|
||||||
|
resources->setHtmlTemplate(tmpl);
|
||||||
|
}
|
||||||
|
@ -43,6 +43,9 @@ public:
|
|||||||
void notifyEvent(const char* data, std::string job = nullptr);
|
void notifyEvent(const char* data, std::string job = nullptr);
|
||||||
void notifyLog(std::string job, uint run, std::string log_chunk, bool eot);
|
void notifyLog(std::string job, uint run, std::string log_chunk, bool eot);
|
||||||
|
|
||||||
|
// Allows supplying a custom HTML template. Pass an empty string to use the default.
|
||||||
|
void setHtmlTemplate(std::string tmpl = std::string());
|
||||||
|
|
||||||
private:
|
private:
|
||||||
virtual kj::Promise<void> request(kj::HttpMethod method, kj::StringPtr url, const kj::HttpHeaders& headers,
|
virtual kj::Promise<void> request(kj::HttpMethod method, kj::StringPtr url, const kj::HttpHeaders& headers,
|
||||||
kj::AsyncInputStream& requestBody, Response& response) override;
|
kj::AsyncInputStream& requestBody, Response& response) override;
|
||||||
|
@ -113,6 +113,12 @@ Laminar::Laminar(Server &server, Settings settings) :
|
|||||||
.addPath((homePath/"cfg"/"jobs").toString(true).cStr())
|
.addPath((homePath/"cfg"/"jobs").toString(true).cStr())
|
||||||
.addPath((homePath/"cfg").toString(true).cStr()); // for groups.conf
|
.addPath((homePath/"cfg").toString(true).cStr()); // for groups.conf
|
||||||
|
|
||||||
|
loadCustomizations();
|
||||||
|
srv.watchPaths([this]{
|
||||||
|
LLOG(INFO, "Reloading customizations");
|
||||||
|
loadCustomizations();
|
||||||
|
}).addPath((homePath/"custom").toString(true).cStr());
|
||||||
|
|
||||||
srv.listenRpc(*rpc, settings.bind_rpc);
|
srv.listenRpc(*rpc, settings.bind_rpc);
|
||||||
srv.listenHttp(*http, settings.bind_http);
|
srv.listenHttp(*http, settings.bind_http);
|
||||||
|
|
||||||
@ -121,6 +127,14 @@ Laminar::Laminar(Server &server, Settings settings) :
|
|||||||
loadConfiguration();
|
loadConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Laminar::loadCustomizations() {
|
||||||
|
KJ_IF_MAYBE(templ, fsHome->tryOpenFile(kj::Path{"custom","index.html"})) {
|
||||||
|
http->setHtmlTemplate((*templ)->readAllText().cStr());
|
||||||
|
} else {
|
||||||
|
http->setHtmlTemplate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uint Laminar::latestRun(std::string job) {
|
uint Laminar::latestRun(std::string job) {
|
||||||
auto it = activeJobs.byJobName().equal_range(job);
|
auto it = activeJobs.byJobName().equal_range(job);
|
||||||
if(it.first == it.second) {
|
if(it.first == it.second) {
|
||||||
@ -789,8 +803,14 @@ R"x(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: deprecate
|
||||||
std::string Laminar::getCustomCss() {
|
std::string Laminar::getCustomCss() {
|
||||||
KJ_IF_MAYBE(cssFile, fsHome->tryOpenFile(kj::Path{"custom","style.css"})) {
|
KJ_IF_MAYBE(cssFile, fsHome->tryOpenFile(kj::Path{"custom","style.css"})) {
|
||||||
|
static bool warningShown = false;
|
||||||
|
if(!warningShown) {
|
||||||
|
LLOG(WARNING, "Custom CSS has been deprecated and will be removed from a future release. Use a custom HTML template instead.");
|
||||||
|
warningShown = true;
|
||||||
|
}
|
||||||
return (*cssFile)->readAllText().cStr();
|
return (*cssFile)->readAllText().cStr();
|
||||||
} else {
|
} else {
|
||||||
return std::string();
|
return std::string();
|
||||||
|
@ -103,6 +103,7 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
bool loadConfiguration();
|
bool loadConfiguration();
|
||||||
|
void loadCustomizations();
|
||||||
void assignNewJobs();
|
void assignNewJobs();
|
||||||
bool tryStartRun(std::shared_ptr<Run> run, int queueIndex);
|
bool tryStartRun(std::shared_ptr<Run> run, int queueIndex);
|
||||||
void handleRunFinished(Run*);
|
void handleRunFinished(Run*);
|
||||||
|
@ -37,7 +37,6 @@
|
|||||||
|
|
||||||
Resources::Resources()
|
Resources::Resources()
|
||||||
{
|
{
|
||||||
INIT_RESOURCE("/", index_html, CONTENT_TYPE_HTML);
|
|
||||||
INIT_RESOURCE("/favicon.ico", favicon_ico, CONTENT_TYPE_ICO);
|
INIT_RESOURCE("/favicon.ico", favicon_ico, CONTENT_TYPE_ICO);
|
||||||
INIT_RESOURCE("/favicon-152.png", favicon_152_png, CONTENT_TYPE_PNG);
|
INIT_RESOURCE("/favicon-152.png", favicon_152_png, CONTENT_TYPE_PNG);
|
||||||
INIT_RESOURCE("/icon.png", icon_png, CONTENT_TYPE_PNG);
|
INIT_RESOURCE("/icon.png", icon_png, CONTENT_TYPE_PNG);
|
||||||
@ -48,46 +47,70 @@ Resources::Resources()
|
|||||||
INIT_RESOURCE("/js/ansi_up.js", js_ansi_up_js, CONTENT_TYPE_JS);
|
INIT_RESOURCE("/js/ansi_up.js", js_ansi_up_js, CONTENT_TYPE_JS);
|
||||||
INIT_RESOURCE("/js/Chart.min.js", js_Chart_min_js, CONTENT_TYPE_JS);
|
INIT_RESOURCE("/js/Chart.min.js", js_Chart_min_js, CONTENT_TYPE_JS);
|
||||||
INIT_RESOURCE("/css/bootstrap.min.css", css_bootstrap_min_css, CONTENT_TYPE_CSS);
|
INIT_RESOURCE("/css/bootstrap.min.css", css_bootstrap_min_css, CONTENT_TYPE_CSS);
|
||||||
|
// Configure the default template
|
||||||
|
setHtmlTemplate(std::string());
|
||||||
|
}
|
||||||
|
|
||||||
if(const char* baseUrl = getenv("LAMINAR_BASE_URL")) {
|
void Resources::setHtmlTemplate(std::string tmpl) {
|
||||||
// The administrator needs to customize the <base href>. Unfortunately this seems
|
extern const char _binary_index_html_z_start[];
|
||||||
// to be the only thing that needs to be customizable but cannot be done via dynamic
|
extern const char _binary_index_html_z_end[];
|
||||||
// DOM manipulation without heavy compromises. So replace the static char array with
|
|
||||||
// a modified buffer accordingly.
|
z_stream strm;
|
||||||
z_stream strm;
|
memset(&strm, 0, sizeof(z_stream));
|
||||||
memset(&strm, 0, sizeof(z_stream));
|
|
||||||
std::string tmp;
|
if(!tmpl.empty()) {
|
||||||
tmp.resize(INDEX_HTML_UNCOMPRESSED_SIZE);
|
|
||||||
// inflate
|
|
||||||
inflateInit2(&strm, MAX_WBITS|GZIP_FORMAT);
|
|
||||||
strm.next_in = (unsigned char*) _binary_index_html_z_start;
|
|
||||||
strm.avail_in = _binary_index_html_z_end - _binary_index_html_z_start;
|
|
||||||
strm.next_out = (unsigned char*) tmp.data();
|
|
||||||
strm.avail_out = INDEX_HTML_UNCOMPRESSED_SIZE;
|
|
||||||
if(inflate(&strm, Z_FINISH) != Z_STREAM_END) {
|
|
||||||
LLOG(FATAL, "Failed to uncompress index_html");
|
|
||||||
}
|
|
||||||
// replace
|
|
||||||
// There's no validation on the replacement string, so you can completely mangle
|
|
||||||
// the html if you like. This isn't really an issue because if you can modify laminar's
|
|
||||||
// environment you already have elevated permissions
|
|
||||||
if(auto it = tmp.find("base href=\"/"))
|
|
||||||
tmp.replace(it+11, 1, baseUrl);
|
|
||||||
// deflate
|
// deflate
|
||||||
index_html.resize(tmp.size());
|
index_html.resize(tmpl.size());
|
||||||
deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS|GZIP_FORMAT, 8, Z_DEFAULT_STRATEGY);
|
deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS|GZIP_FORMAT, 8, Z_DEFAULT_STRATEGY);
|
||||||
strm.next_in = (unsigned char*) tmp.data();
|
strm.next_in = (unsigned char*) tmpl.data();
|
||||||
strm.avail_in = tmp.size();
|
strm.avail_in = tmpl.size();
|
||||||
strm.next_out = (unsigned char*) index_html.data();
|
strm.next_out = (unsigned char*) index_html.data();
|
||||||
strm.avail_out = tmp.size();
|
strm.avail_out = tmpl.size();
|
||||||
if(deflate(&strm, Z_FINISH) != Z_STREAM_END) {
|
if(deflate(&strm, Z_FINISH) != Z_STREAM_END) {
|
||||||
LLOG(FATAL, "Failed to compress index.html");
|
LLOG(FATAL, "Failed to compress index.html");
|
||||||
}
|
}
|
||||||
index_html.resize(strm.total_out);
|
index_html.resize(strm.total_out);
|
||||||
// update resource map
|
} else {
|
||||||
resources["/"].start = index_html.data();
|
// use the default template from compile-time asset
|
||||||
resources["/"].end = index_html.data() + index_html.size();
|
if(const char* baseUrl = getenv("LAMINAR_BASE_URL")) {
|
||||||
|
// The administrator needs to customize the <base href>. Unfortunately this seems
|
||||||
|
// to be the only thing that needs to be customizable but cannot be done via dynamic
|
||||||
|
// DOM manipulation without heavy compromises. So replace the static char array with
|
||||||
|
// a modified buffer accordingly.
|
||||||
|
std::string tmp;
|
||||||
|
tmp.resize(INDEX_HTML_UNCOMPRESSED_SIZE);
|
||||||
|
// inflate
|
||||||
|
inflateInit2(&strm, MAX_WBITS|GZIP_FORMAT);
|
||||||
|
strm.next_in = (unsigned char*) _binary_index_html_z_start;
|
||||||
|
strm.avail_in = _binary_index_html_z_end - _binary_index_html_z_start;
|
||||||
|
strm.next_out = (unsigned char*) tmp.data();
|
||||||
|
strm.avail_out = INDEX_HTML_UNCOMPRESSED_SIZE;
|
||||||
|
if(inflate(&strm, Z_FINISH) != Z_STREAM_END) {
|
||||||
|
LLOG(FATAL, "Failed to uncompress index_html");
|
||||||
|
}
|
||||||
|
// replace
|
||||||
|
// There's no validation on the replacement string, so you can completely mangle
|
||||||
|
// the html if you like. This isn't really an issue because if you can modify laminar's
|
||||||
|
// environment you already have elevated permissions
|
||||||
|
if(auto it = tmp.find("base href=\"/"))
|
||||||
|
tmp.replace(it+11, 1, baseUrl);
|
||||||
|
// deflate
|
||||||
|
index_html.resize(tmp.size());
|
||||||
|
deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS|GZIP_FORMAT, 8, Z_DEFAULT_STRATEGY);
|
||||||
|
strm.next_in = (unsigned char*) tmp.data();
|
||||||
|
strm.avail_in = tmp.size();
|
||||||
|
strm.next_out = (unsigned char*) index_html.data();
|
||||||
|
strm.avail_out = tmp.size();
|
||||||
|
if(deflate(&strm, Z_FINISH) != Z_STREAM_END) {
|
||||||
|
LLOG(FATAL, "Failed to compress index.html");
|
||||||
|
}
|
||||||
|
index_html.resize(strm.total_out);
|
||||||
|
} else {
|
||||||
|
index_html = std::string(_binary_index_html_z_start, _binary_index_html_z_end);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// update resource map
|
||||||
|
resources["/"] = Resource{index_html.data(), index_html.data() + index_html.size(), CONTENT_TYPE_HTML};
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool beginsWith(std::string haystack, const char* needle) {
|
inline bool beginsWith(std::string haystack, const char* needle) {
|
||||||
|
@ -34,6 +34,9 @@ public:
|
|||||||
// type. Function returns false if no resource for the given path exists
|
// type. Function returns false if no resource for the given path exists
|
||||||
bool handleRequest(std::string path, const char** start, const char** end, const char** content_type);
|
bool handleRequest(std::string path, const char** start, const char** end, const char** content_type);
|
||||||
|
|
||||||
|
// Allows providing a custom HTML template. Pass an empty string to use the default.
|
||||||
|
void setHtmlTemplate(std::string templ = std::string());
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Resource {
|
struct Resource {
|
||||||
const char* start;
|
const char* start;
|
||||||
|
Loading…
Reference in New Issue
Block a user