Compare commits

...

4 commits

Author SHA1 Message Date
0d822e2a38
feat: disable editor
All checks were successful
/ pre-commit (push) Successful in 1m31s
Signed-off-by: Julien Riou <julien@riou.xyz>
2025-09-22 17:14:19 +02:00
d0115cdaf4
style: Simplify condition to load bootstrap
Signed-off-by: Julien Riou <julien@riou.xyz>
2025-09-22 17:14:18 +02:00
d91a386881
feat: replace Monaco by Ace
- Remove the Monaco Editor because it was to heavy and hard to integrate
- Use Ace instead
- Use the lowercase identifier for languages (ex: "Text" -> "text")
- Select automatically the default language in the drop down to create a note
  (like the expiration)
- Add `ace_directory` to serve assets from a local folder instead of a CDN
- "hcl" syntax highlighting has been removed
- "go" syntax highlighting has been renamed to "golang"

Signed-off-by: Julien Riou <julien@riou.xyz>
2025-09-22 17:14:14 +02:00
634326190c
feat: add expiration time in the note web view
All checks were successful
/ pre-commit (push) Successful in 1m7s
Fixes #35.

Signed-off-by: Julien Riou <julien@riou.xyz>
2025-09-22 17:13:44 +02:00
10 changed files with 131 additions and 74 deletions

6
package-lock.json generated
View file

@ -5,6 +5,7 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"ace-builds": "^1.43.3",
"bootstrap": "^5.3.8" "bootstrap": "^5.3.8"
} }
}, },
@ -18,6 +19,11 @@
"url": "https://opencollective.com/popperjs" "url": "https://opencollective.com/popperjs"
} }
}, },
"node_modules/ace-builds": {
"version": "1.43.3",
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.43.3.tgz",
"integrity": "sha512-MCl9rALmXwIty/4Qboijo/yNysx1r6hBTzG+6n/TiOm5LFhZpEvEIcIITPFiEOEFDfgBOEmxu+a4f54LEFM6Sg=="
},
"node_modules/bootstrap": { "node_modules/bootstrap": {
"version": "5.3.8", "version": "5.3.8",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",

View file

@ -1,5 +1,6 @@
{ {
"dependencies": { "dependencies": {
"ace-builds": "^1.43.3",
"bootstrap": "^5.3.8" "bootstrap": "^5.3.8"
} }
} }

View file

@ -31,12 +31,14 @@ The file format is **JSON**:
* **prometheus_route** (string): Route to expose Prometheus metrics (default "/metrics") * **prometheus_route** (string): Route to expose Prometheus metrics (default "/metrics")
* **prometheus_notes_metric** (string): Name of the notes count metric (default "collerd_notes") * **prometheus_notes_metric** (string): Name of the notes count metric (default "collerd_notes")
* **observation_internal** (int): Number of seconds to wait between two observations (default 60) * **observation_internal** (int): Number of seconds to wait between two observations (default 60)
* **languages** ([]string): List of supported [languages](https://github.com/microsoft/monaco-editor/tree/main/src/basic-languages) * **languages** ([]string): List of supported [languages](https://github.com/ajaxorg/ace/tree/master/src/mode)
* **language** (string): Default language (default "text") * **language** (string): Default language (default "text")
* **enable_upload_file_button** (bool): Display the upload file button in the UI (default true) * **enable_upload_file_button** (bool): Display the upload file button in the UI (default true)
* **tls_cert_file** (string): Path to TLS certificate file to enable HTTPS * **tls_cert_file** (string): Path to TLS certificate file to enable HTTPS
* **tls_key_file** (string): Path to TLS key file to enable HTTPS * **tls_key_file** (string): Path to TLS key file to enable HTTPS
* **ace_directory** (string): Serve [Ace](hhttps://ace.c9.io/) assets from this local directory (ex: "./node_modules/ace-builds"). See **Dependencies** for details.
* **bootstrap_directory** (string): Serve [Bootstrap](https://getbootstrap.com/) assets from this local directory (ex: "./node_modules/bootstrap/dist"). See **Dependencies** for details. * **bootstrap_directory** (string): Serve [Bootstrap](https://getbootstrap.com/) assets from this local directory (ex: "./node_modules/bootstrap/dist"). See **Dependencies** for details.
* **disable_editor** (bool): Disable Ace editor.
The configuration file is not required but the service might not be exposed to the public. The configuration file is not required but the service might not be exposed to the public.
@ -85,12 +87,12 @@ Errors return **500 Server Internal Error** with the **JSON** payload:
The web interface depends on: The web interface depends on:
- [Ace](https://ace.c9.io/)
- [Bootstrap](https://getbootstrap.com/) - [Bootstrap](https://getbootstrap.com/)
- [Monaco Editor](https://github.com/microsoft/monaco-editor/)
By default, those dependencies are fetched from **remote CDN** services by the client. By default, those dependencies are fetched from **remote CDN** services by the client.
If you would like to download them to serve them locally: If you would like to download and serve them locally:
``` ```
npm install npm install
@ -106,8 +108,7 @@ Then configure the local directories:
```json ```json
{ {
"ace_directory": "./node_modules/ace-builds",
"bootstrap_directory": "./node_modules/bootstrap/dist" "bootstrap_directory": "./node_modules/bootstrap/dist"
} }
``` ```
Downloading Monaco Editor is not supported yet.

View file

@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"time"
) )
func ReadConfig(file string, config interface{}) error { func ReadConfig(file string, config interface{}) error {
@ -102,6 +103,15 @@ func HumanDuration(i int) string {
return fmt.Sprintf("%d %s", i, w) return fmt.Sprintf("%d %s", i, w)
} }
// TimeDiff to return the number of seconds between this time and now
func TimeDiff(ts time.Time) int {
diff := int(time.Since(ts).Seconds())
if diff < 0 {
return diff * -1
}
return diff
}
func ReturnError(logger *slog.Logger, message string, err error) int { func ReturnError(logger *slog.Logger, message string, err error) int {
if err != nil { if err != nil {
logger.Error(message, slog.Any("error", err)) logger.Error(message, slog.Any("error", err))

View file

@ -28,7 +28,9 @@ type Config struct {
EnableUploadFileButton bool `json:"enable_upload_file_button"` EnableUploadFileButton bool `json:"enable_upload_file_button"`
TLSCertFile string `json:"tls_cert_file"` TLSCertFile string `json:"tls_cert_file"`
TLSKeyFile string `json:"tls_key_file"` TLSKeyFile string `json:"tls_key_file"`
AceDirectory string `json:"ace_directory"`
BootstrapDirectory string `json:"bootstrap_directory"` BootstrapDirectory string `json:"bootstrap_directory"`
DisableEditor bool `json:"disable_editor"`
} }
func NewConfig() *Config { func NewConfig() *Config {
@ -56,22 +58,21 @@ func NewConfig() *Config {
PrometheusNotesMetric: "collerd_notes", PrometheusNotesMetric: "collerd_notes",
ObservationInterval: 60, ObservationInterval: 60,
Languages: []string{ Languages: []string{
"Text", "css",
"CSS", "dockerfile",
"Dockerfile", "golang",
"Go", "html",
"HCL", "javascript",
"HTML", "json",
"Javascript", "markdown",
"JSON", "perl",
"Markdown", "python",
"Perl", "ruby",
"Python", "rust",
"Ruby", "sh",
"Rust", "sql",
"Shell", "text",
"SQL", "yaml",
"YAML",
}, },
Language: "text", Language: "text",
EnableUploadFileButton: true, EnableUploadFileButton: true,

View file

@ -21,12 +21,15 @@ type PageData struct {
Version string Version string
Expirations []int Expirations []int
Expiration int Expiration int
Language string
Languages []string Languages []string
Err error Err error
URL string URL string
Note *Note Note *Note
EnableUploadFileButton bool EnableUploadFileButton bool
AceDirectory string
BootstrapDirectory string BootstrapDirectory string
DisableEditor bool
} }
type HomeHandler struct { type HomeHandler struct {

View file

@ -106,6 +106,7 @@ func (s *Server) Start() error {
// Web pages // Web pages
funcs := template.FuncMap{ funcs := template.FuncMap{
"HumanDuration": internal.HumanDuration, "HumanDuration": internal.HumanDuration,
"TimeDiff": internal.TimeDiff,
"lower": strings.ToLower, "lower": strings.ToLower,
"string": func(b []byte) string { return string(b) }, "string": func(b []byte) string { return string(b) },
} }
@ -113,8 +114,11 @@ func (s *Server) Start() error {
Title: s.config.Title, Title: s.config.Title,
Expirations: s.config.Expirations, Expirations: s.config.Expirations,
Expiration: s.config.Expiration, Expiration: s.config.Expiration,
Language: s.config.Language,
Languages: s.config.Languages, Languages: s.config.Languages,
AceDirectory: s.config.AceDirectory,
BootstrapDirectory: s.config.BootstrapDirectory, BootstrapDirectory: s.config.BootstrapDirectory,
DisableEditor: s.config.DisableEditor,
} }
if s.config.ShowVersion { if s.config.ShowVersion {
@ -159,6 +163,10 @@ func (s *Server) Start() error {
} }
r.Path("/{id:[a-zA-Z0-9]+}.html").Handler(webNoteHandler).Methods("GET") r.Path("/{id:[a-zA-Z0-9]+}.html").Handler(webNoteHandler).Methods("GET")
if s.config.AceDirectory != "" {
r.PathPrefix("/static/ace-builds/").Handler(http.StripPrefix("/static/ace-builds/", http.FileServer(http.Dir(s.config.AceDirectory))))
}
if s.config.BootstrapDirectory != "" { if s.config.BootstrapDirectory != "" {
r.PathPrefix("/static/bootstrap/").Handler(http.StripPrefix("/static/bootstrap/", http.FileServer(http.Dir(s.config.BootstrapDirectory)))) r.PathPrefix("/static/bootstrap/").Handler(http.StripPrefix("/static/bootstrap/", http.FileServer(http.Dir(s.config.BootstrapDirectory))))
} }
@ -168,10 +176,10 @@ func (s *Server) Start() error {
addr := fmt.Sprintf("%s:%d", s.config.ListenAddress, s.config.ListenPort) addr := fmt.Sprintf("%s:%d", s.config.ListenAddress, s.config.ListenPort)
if s.config.HasTLS() { if s.config.HasTLS() {
s.logger.Info(fmt.Sprintf("listening to %s:%d (https)", s.config.ListenAddress, s.config.ListenPort)) s.logger.Info(fmt.Sprintf("listening to %s:%d", s.config.ListenAddress, s.config.ListenPort), slog.Any("scheme", "http"))
return http.ListenAndServeTLS(addr, s.config.TLSCertFile, s.config.TLSKeyFile, r) return http.ListenAndServeTLS(addr, s.config.TLSCertFile, s.config.TLSKeyFile, r)
} else { } else {
s.logger.Info(fmt.Sprintf("listening to %s:%d (http)", s.config.ListenAddress, s.config.ListenPort)) s.logger.Info(fmt.Sprintf("listening to %s:%d", s.config.ListenAddress, s.config.ListenPort), slog.Any("scheme", "https"))
return http.ListenAndServe(addr, r) return http.ListenAndServe(addr, r)
} }
} }

View file

@ -4,6 +4,8 @@
<title>{{.Title}}</title> <title>{{.Title}}</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link href="{{if ne .BootstrapDirectory ``}}/static/bootstrap/css/bootstrap.min.css{{else}}https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css{{end}}" rel="stylesheet"> <link
href="{{if .BootstrapDirectory}}/static/bootstrap/css/bootstrap.min.css{{else}}https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css{{end}}"
rel="stylesheet">
</head> </head>
{{end}} {{end}}

View file

@ -40,15 +40,17 @@
<select class="form-select" aria-label="Expiration" id="expiration" name="expiration"> <select class="form-select" aria-label="Expiration" id="expiration" name="expiration">
<option disabled>Expiration</option> <option disabled>Expiration</option>
{{range $exp := .Expirations}} {{range $exp := .Expirations}}
<option {{ if eq $exp $.Expiration }}selected="selected"{{end}} value="{{$exp}}">{{HumanDuration $exp}}</option> <option {{ if eq $exp $.Expiration }}selected="selected" {{end}} value="{{$exp}}">
{{HumanDuration $exp}}</option>
{{end}} {{end}}
</select> </select>
</div> </div>
<div class="col"> <div class="col">
<select class="form-select" aria-label="Language" id="language" name="language"> <select class="form-select" aria-label="Language" id="language" name="language">
<option selected="selected" value="" disabled>Language</option> <option disabled>Language</option>
{{range .Languages}} {{range $language := .Languages}}
<option value="{{lower .}}">{{.}}</option> <option {{ if eq $language $.Language }}selected="selected" {{end}}value="{{lower .}}">
{{$language}}</option>
{{end}} {{end}}
</select> </select>
</div> </div>
@ -56,11 +58,15 @@
</div> </div>
<div class="container mb-4"> <div class="container mb-4">
{{if .DisableEditor}}
<textarea class="form-control" id="content" name="content" cols="40" rows="12"></textarea>
{{else}}
<div class="row"> <div class="row">
<div id="editor" name="editor" class="form-control" <div id="editor" name="editor" class="form-control"
style="height: 300px; resize: vertical; overflow: auto;"></div> style="height: 300px; resize: vertical; overflow: auto;"></div>
<input type="hidden" id="content" /> <input type="hidden" id="content" />
</div> </div>
{{end}}
</div> </div>
<div class="container mb-4"> <div class="container mb-4">
<div class="row text-center justify-content-center"> <div class="row text-center justify-content-center">
@ -69,38 +75,40 @@
</div> </div>
</div> </div>
</div> </div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs/loader.min.js"></script> {{if eq false .DisableEditor}}
<script
src="{{if .AceDirectory}}/static/ace-builds{{else}}https://cdn.jsdelivr.net/npm/ace-builds@1.43.3{{end}}/src-noconflict/ace.js"
type="text/javascript" charset="utf-8"></script>
<script> <script>
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs' } }); var editor = ace.edit("editor");
if (document.documentElement.getAttribute('data-bs-theme') == 'light') {
editor.setTheme("ace/theme/github_light_default");
} else {
editor.setTheme("ace/theme/github_dark");
}
require(['vs/editor/editor.main'], function () { // Syntax highlighting
var editor = monaco.editor.create(document.getElementById('editor'), { document.getElementById("language").addEventListener("change", (e) => {
theme: document.documentElement.getAttribute('data-bs-theme') == 'light' ? "vs" : "vs-dark", if (e.target.value != "") {
language: document.getElementById("language").value, editor.getSession().setMode("ace/mode/" + e.target.value);
}); }
});
// Syntax highlighting // Dark mode
document.getElementById("language").addEventListener("change", (e) => { document.getElementById("lightSwitch").addEventListener("click", () => {
if (e.target.value != "") { if (document.documentElement.getAttribute('data-bs-theme') == 'light') {
monaco.editor.setModelLanguage(editor.getModel(), e.target.value); editor.setTheme("ace/theme/github_light_default")
} } else if (document.documentElement.getAttribute('data-bs-theme') == 'dark') {
}); editor.setTheme("ace/theme/github_dark")
}
});
// Dark mode // Copy content on submit
document.getElementById("lightSwitch").addEventListener("click", () => { document.getElementById("form").addEventListener("formdata", (e) => {
if (document.documentElement.getAttribute('data-bs-theme') == 'light') { e.formData.append('content', editor.getValue());
monaco.editor.setTheme("vs")
} else if (document.documentElement.getAttribute('data-bs-theme') == 'dark') {
monaco.editor.setTheme("vs-dark")
}
});
// Copy content on submit
document.getElementById("form").addEventListener("formdata", (e) => {
e.formData.append('content', editor.getModel().getValue());
});
}); });
</script> </script>
{{end}}
</form> </form>
{{block "footer" .}}{{end}} {{block "footer" .}}{{end}}

View file

@ -19,41 +19,58 @@
<div class="d-flex flex-wrap py-2"> <div class="d-flex flex-wrap py-2">
<span class="fs-4 d-flex mb-3 mb-md-0 me-md-auto text-decoration-none">Note {{.Note.ID}}</span> <span class="fs-4 d-flex mb-3 mb-md-0 me-md-auto text-decoration-none">Note {{.Note.ID}}</span>
<ul class="nav nav-pills align-items-center"> <ul class="nav nav-pills align-items-center">
<li class="nav-item"> <li class="nav-item px-2">
<a href="" id="rawURL">raw</a> <a href="" id="rawURL">raw</a>
<script>document.getElementById("rawURL").href = window.location.href.replace(".html", "");</script> <script>document.getElementById("rawURL").href = window.location.href.replace(".html", "");</script>
</li> </li>
<li class="nav-item px-2"> <li class="nav-item px-2">
{{.Note.Language}} {{.Note.Language}}
</li> </li>
{{if eq .Note.DeleteAfterRead false}}
<li class="nav-item px-2">
expires in {{HumanDuration (TimeDiff .Note.ExpiresAt)}}
</li>
{{end}}
</ul> </ul>
</div> </div>
{{if .DisableEditor}}
<div class="row">
<pre class="border px-3 pt-3" style="min-height: 300px; resize: vertical; overflow: auto;">
{{string .Note.Content}}
</pre>
</div>
{{else}}
<div class="row"> <div class="row">
<div id="editor" name="editor" class="form-control" <div id="editor" name="editor" class="form-control"
style="height: 300px; resize: vertical; overflow: auto;"></div> style="min-height: 300px; resize: vertical; overflow: auto;"></div>
</div> </div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs/loader.min.js"></script> <script
src="{{if .AceDirectory}}/static/ace-builds{{else}}https://cdn.jsdelivr.net/npm/ace-builds@1.43.3{{end}}/src-noconflict/ace.js"
type="text/javascript" charset="utf-8"></script>
<script> <script>
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs' } }); var editor = ace.edit("editor");
editor.setValue("{{string .Note.Content}}");
editor.setReadOnly(true);
editor.getSession().setMode("ace/mode/{{.Note.Language}}");
editor.getSession().selection.clearSelection();
editor.setOptions({maxLines: Infinity});
require(['vs/editor/editor.main'], function () { if (document.documentElement.getAttribute('data-bs-theme') == 'light') {
var editor = monaco.editor.create(document.getElementById('editor'), { editor.setTheme("ace/theme/github_light_default");
theme: document.documentElement.getAttribute('data-bs-theme') == 'light' ? "vs" : "vs-dark", } else {
language: "{{.Note.Language}}", editor.setTheme("ace/theme/github_dark");
readOnly: true, }
value: "{{string .Note.Content}}"
});
// Dark mode // Dark mode
document.getElementById("lightSwitch").addEventListener("click", () => { document.getElementById("lightSwitch").addEventListener("click", () => {
if (document.documentElement.getAttribute('data-bs-theme') == 'light') { if (document.documentElement.getAttribute('data-bs-theme') == 'light') {
monaco.editor.setTheme("vs") editor.setTheme("ace/theme/github_light_default")
} else if (document.documentElement.getAttribute('data-bs-theme') == 'dark') { } else if (document.documentElement.getAttribute('data-bs-theme') == 'dark') {
monaco.editor.setTheme("vs-dark") editor.setTheme("ace/theme/github_dark")
} }
});
}); });
</script> </script>
{{end}}
</div> </div>
{{end}} {{end}}