mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-23 00:46:30 +00:00
extract commit update js into a separate file
This commit is contained in:
parent
4689d53305
commit
b2965bf432
8 changed files with 337 additions and 104 deletions
5
Makefile
5
Makefile
|
@ -26,10 +26,13 @@ deps:
|
||||||
go get github.com/mattn/go-sqlite3
|
go get github.com/mattn/go-sqlite3
|
||||||
go get github.com/russross/meddler
|
go get github.com/russross/meddler
|
||||||
|
|
||||||
embed:
|
embed: js
|
||||||
cd cmd/droned && rice embed
|
cd cmd/droned && rice embed
|
||||||
cd pkg/template && rice embed
|
cd pkg/template && rice embed
|
||||||
|
|
||||||
|
js:
|
||||||
|
cd cmd/droned/assets && find js -name "*.js" ! -name '.*' ! -name "main.js" -exec cat {} \; > js/main.js
|
||||||
|
|
||||||
build:
|
build:
|
||||||
cd cmd/drone && go build -o ../../bin/drone
|
cd cmd/drone && go build -o ../../bin/drone
|
||||||
cd cmd/droned && go build -o ../../bin/droned
|
cd cmd/droned && go build -o ../../bin/droned
|
||||||
|
|
83
cmd/droned/assets/js/commit_updates.js
Normal file
83
cmd/droned/assets/js/commit_updates.js
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
;// Live commit updates
|
||||||
|
|
||||||
|
if(typeof(Drone) === 'undefined') { Drone = {}; }
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
Drone.CommitUpdates = function(socket) {
|
||||||
|
if(typeof(socket) === "string") {
|
||||||
|
var url = [(window.location.protocol == 'https:' ? 'wss' : 'ws'),
|
||||||
|
'://',
|
||||||
|
window.location.host,
|
||||||
|
'/',
|
||||||
|
socket].join('')
|
||||||
|
this.socket = new WebSocket(url);
|
||||||
|
} else {
|
||||||
|
this.socket = socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lineFormatter = new Drone.LineFormatter();
|
||||||
|
this.attach();
|
||||||
|
}
|
||||||
|
|
||||||
|
Drone.CommitUpdates.prototype = {
|
||||||
|
lineBuffer: "",
|
||||||
|
autoFollow: false,
|
||||||
|
|
||||||
|
startOutput: function(el) {
|
||||||
|
if(typeof(el) === 'string') {
|
||||||
|
this.el = document.getElementById(el);
|
||||||
|
} else {
|
||||||
|
this.el = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateScreen();
|
||||||
|
},
|
||||||
|
|
||||||
|
attach: function() {
|
||||||
|
this.socket.onopen = this.onOpen;
|
||||||
|
this.socket.onerror = this.onError;
|
||||||
|
this.socket.onmessage = this.onMessage.bind(this);
|
||||||
|
this.socket.onclose = this.onClose;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateScreen: function() {
|
||||||
|
if(this.lineBuffer.length > 0) {
|
||||||
|
this.el.innerHTML += this.lineBuffer;
|
||||||
|
this.lineBuffer = '';
|
||||||
|
|
||||||
|
if (this.autoFollow) {
|
||||||
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(this.updateScreen.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
onOpen: function() {
|
||||||
|
console.log('output websocket open');
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: function(e) {
|
||||||
|
console.log('websocket error: ' + e);
|
||||||
|
},
|
||||||
|
|
||||||
|
onMessage: function(e) {
|
||||||
|
this.lineBuffer += this.lineFormatter.format(e.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
onClose: function(e) {
|
||||||
|
console.log('output websocket closed: ' + JSON.stringify(e));
|
||||||
|
//window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Polyfill rAF for older browsers
|
||||||
|
window.requestAnimationFrame = window.requestAnimationFrame ||
|
||||||
|
window.webkitRequestAnimationFrame ||
|
||||||
|
function(callback, element) {
|
||||||
|
return window.setTimeout(function() {
|
||||||
|
callback(+new Date());
|
||||||
|
}, 1000 / 60);
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
66
cmd/droned/assets/js/line_formatter.js
Normal file
66
cmd/droned/assets/js/line_formatter.js
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
;// Format ANSI to HTML
|
||||||
|
|
||||||
|
if(typeof(Drone) === 'undefined') { Drone = {}; }
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
Drone.LineFormatter = function() {};
|
||||||
|
|
||||||
|
Drone.LineFormatter.prototype = {
|
||||||
|
regex: /\u001B\[([0-9]+;?)*[Km]/g,
|
||||||
|
styles: [],
|
||||||
|
|
||||||
|
format: function(s) {
|
||||||
|
// Check for newline and early exit?
|
||||||
|
s = s.replace(/</g, "<");
|
||||||
|
s = s.replace(/>/g, ">");
|
||||||
|
|
||||||
|
var output = "";
|
||||||
|
var current = 0;
|
||||||
|
while (m = this.regex.exec(s)) {
|
||||||
|
var part = s.substring(current, m.index+1);
|
||||||
|
current = this.regex.lastIndex;
|
||||||
|
|
||||||
|
var token = s.substr(m.index, this.regex.lastIndex - m.index);
|
||||||
|
var code = token.substr(2, token.length-2);
|
||||||
|
|
||||||
|
var pre = "";
|
||||||
|
var post = "";
|
||||||
|
|
||||||
|
switch (code) {
|
||||||
|
case 'm':
|
||||||
|
case '0m':
|
||||||
|
var len = styles.length;
|
||||||
|
for (var i=0; i < len; i++) {
|
||||||
|
styles.pop();
|
||||||
|
post += "</span>"
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '30;42m': pre = '<span style="color:black;background:lime">'; break;
|
||||||
|
case '36m':
|
||||||
|
case '36;1m': pre = '<span style="color:cyan;">'; break;
|
||||||
|
case '31m':
|
||||||
|
case '31;31m': pre = '<span style="color:red;">'; break;
|
||||||
|
case '33m':
|
||||||
|
case '33;33m': pre = '<span style="color:yellow;">'; break;
|
||||||
|
case '32m':
|
||||||
|
case '0;32m': pre = '<span style="color:lime;">'; break;
|
||||||
|
case '90m': pre = '<span style="color:gray;">'; break;
|
||||||
|
case 'K':
|
||||||
|
case '0K':
|
||||||
|
case '1K':
|
||||||
|
case '2K': break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pre !== "") {
|
||||||
|
styles.push(pre);
|
||||||
|
}
|
||||||
|
|
||||||
|
output += part + pre + post;
|
||||||
|
}
|
||||||
|
|
||||||
|
var part = s.substring(current, s.length);
|
||||||
|
output += part;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
149
cmd/droned/assets/js/main.js
Normal file
149
cmd/droned/assets/js/main.js
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
;// Live commit updates
|
||||||
|
|
||||||
|
if(typeof(Drone) === 'undefined') { Drone = {}; }
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
Drone.CommitUpdates = function(socket) {
|
||||||
|
if(typeof(socket) === "string") {
|
||||||
|
var url = [(window.location.protocol == 'https:' ? 'wss' : 'ws'),
|
||||||
|
'://',
|
||||||
|
window.location.host,
|
||||||
|
'/',
|
||||||
|
socket].join('')
|
||||||
|
this.socket = new WebSocket(url);
|
||||||
|
} else {
|
||||||
|
this.socket = socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lineFormatter = new Drone.LineFormatter();
|
||||||
|
this.attach();
|
||||||
|
}
|
||||||
|
|
||||||
|
Drone.CommitUpdates.prototype = {
|
||||||
|
lineBuffer: "",
|
||||||
|
autoFollow: false,
|
||||||
|
|
||||||
|
startOutput: function(el) {
|
||||||
|
if(typeof(el) === 'string') {
|
||||||
|
this.el = document.getElementById(el);
|
||||||
|
} else {
|
||||||
|
this.el = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateScreen();
|
||||||
|
},
|
||||||
|
|
||||||
|
attach: function() {
|
||||||
|
this.socket.onopen = this.onOpen;
|
||||||
|
this.socket.onerror = this.onError;
|
||||||
|
this.socket.onmessage = this.onMessage.bind(this);
|
||||||
|
this.socket.onclose = this.onClose;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateScreen: function() {
|
||||||
|
if(this.lineBuffer.length > 0) {
|
||||||
|
this.el.innerHTML += this.lineBuffer;
|
||||||
|
this.lineBuffer = '';
|
||||||
|
|
||||||
|
if (this.autoFollow) {
|
||||||
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(this.updateScreen.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
onOpen: function() {
|
||||||
|
console.log('output websocket open');
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: function(e) {
|
||||||
|
console.log('websocket error: ' + e);
|
||||||
|
},
|
||||||
|
|
||||||
|
onMessage: function(e) {
|
||||||
|
this.lineBuffer += this.lineFormatter.format(e.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
onClose: function(e) {
|
||||||
|
console.log('output websocket closed: ' + JSON.stringify(e));
|
||||||
|
//window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Polyfill rAF for older browsers
|
||||||
|
window.requestAnimationFrame = window.requestAnimationFrame ||
|
||||||
|
window.webkitRequestAnimationFrame ||
|
||||||
|
function(callback, element) {
|
||||||
|
return window.setTimeout(function() {
|
||||||
|
callback(+new Date());
|
||||||
|
}, 1000 / 60);
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
||||||
|
;// Format ANSI to HTML
|
||||||
|
|
||||||
|
if(typeof(Drone) === 'undefined') { Drone = {}; }
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
Drone.LineFormatter = function() {};
|
||||||
|
|
||||||
|
Drone.LineFormatter.prototype = {
|
||||||
|
regex: /\u001B\[([0-9]+;?)*[Km]/g,
|
||||||
|
styles: [],
|
||||||
|
|
||||||
|
format: function(s) {
|
||||||
|
// Check for newline and early exit?
|
||||||
|
s = s.replace(/</g, "<");
|
||||||
|
s = s.replace(/>/g, ">");
|
||||||
|
|
||||||
|
var output = "";
|
||||||
|
var current = 0;
|
||||||
|
while (m = this.regex.exec(s)) {
|
||||||
|
var part = s.substring(current, m.index+1);
|
||||||
|
current = this.regex.lastIndex;
|
||||||
|
|
||||||
|
var token = s.substr(m.index, this.regex.lastIndex - m.index);
|
||||||
|
var code = token.substr(2, token.length-2);
|
||||||
|
|
||||||
|
var pre = "";
|
||||||
|
var post = "";
|
||||||
|
|
||||||
|
switch (code) {
|
||||||
|
case 'm':
|
||||||
|
case '0m':
|
||||||
|
var len = styles.length;
|
||||||
|
for (var i=0; i < len; i++) {
|
||||||
|
styles.pop();
|
||||||
|
post += "</span>"
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '30;42m': pre = '<span style="color:black;background:lime">'; break;
|
||||||
|
case '36m':
|
||||||
|
case '36;1m': pre = '<span style="color:cyan;">'; break;
|
||||||
|
case '31m':
|
||||||
|
case '31;31m': pre = '<span style="color:red;">'; break;
|
||||||
|
case '33m':
|
||||||
|
case '33;33m': pre = '<span style="color:yellow;">'; break;
|
||||||
|
case '32m':
|
||||||
|
case '0;32m': pre = '<span style="color:lime;">'; break;
|
||||||
|
case '90m': pre = '<span style="color:gray;">'; break;
|
||||||
|
case 'K':
|
||||||
|
case '0K':
|
||||||
|
case '1K':
|
||||||
|
case '2K': break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pre !== "") {
|
||||||
|
styles.push(pre);
|
||||||
|
}
|
||||||
|
|
||||||
|
output += part + pre + post;
|
||||||
|
}
|
||||||
|
|
||||||
|
var part = s.substring(current, s.length);
|
||||||
|
output += part;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
|
@ -111,6 +111,7 @@ func setupDatabase() {
|
||||||
func setupStatic() {
|
func setupStatic() {
|
||||||
box := rice.MustFindBox("assets")
|
box := rice.MustFindBox("assets")
|
||||||
http.Handle("/css/", http.FileServer(box.HTTPBox()))
|
http.Handle("/css/", http.FileServer(box.HTTPBox()))
|
||||||
|
http.Handle("/js/", http.FileServer(box.HTTPBox()))
|
||||||
|
|
||||||
// we need to intercept all attempts to serve images
|
// we need to intercept all attempts to serve images
|
||||||
// so that we can add a cache-control settings
|
// so that we can add a cache-control settings
|
||||||
|
|
|
@ -13,13 +13,14 @@
|
||||||
|
|
||||||
<!-- drone bootstrap theme -->
|
<!-- drone bootstrap theme -->
|
||||||
<link href="/css/drone.css" rel="stylesheet" type="text/css" />
|
<link href="/css/drone.css" rel="stylesheet" type="text/css" />
|
||||||
|
|
||||||
<!-- fonts -->
|
<!-- fonts -->
|
||||||
<link href="//fonts.googleapis.com/css?family=Orbitron" rel="stylesheet" />
|
<link href="//fonts.googleapis.com/css?family=Orbitron" rel="stylesheet" />
|
||||||
<link href="//fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" />
|
<link href="//fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" />
|
||||||
<link href="//fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet" />
|
<link href="//fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet" />
|
||||||
<link href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.min.css" rel="stylesheet" />
|
<link href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<script src="/js/main.js" type="text/javascript"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
@ -59,4 +60,4 @@
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script>
|
||||||
{{ template "script" . }}
|
{{ template "script" . }}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -52,120 +52,36 @@
|
||||||
{{ define "script" }}
|
{{ define "script" }}
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-timeago/1.1.0/jquery.timeago.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-timeago/1.1.0/jquery.timeago.js"></script>
|
||||||
<script>
|
<script>
|
||||||
window.autofollow = false;
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$(".timeago").timeago();
|
$(".timeago").timeago();
|
||||||
$("#follow").bind("click", function(ev) {
|
|
||||||
if (window.autofollow) {
|
|
||||||
window.autofollow = false;
|
|
||||||
$("#follow").text("Follow");
|
|
||||||
} else {
|
|
||||||
window.autofollow = true;
|
|
||||||
$("#follow").text("Stop following");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
|
||||||
var re = /\u001B\[([0-9]+;?)*[Km]/g;
|
|
||||||
|
|
||||||
var styles = new Array();
|
|
||||||
var formatLine = function(s) {
|
|
||||||
// Check for newline and early exit?
|
|
||||||
s = s.replace(/</g, "<");
|
|
||||||
s = s.replace(/>/g, ">");
|
|
||||||
|
|
||||||
var final = "";
|
|
||||||
var current = 0;
|
|
||||||
while (m = re.exec(s)) {
|
|
||||||
var part = s.substring(current, m.index+1);
|
|
||||||
current = re.lastIndex;
|
|
||||||
|
|
||||||
var token = s.substr(m.index, re.lastIndex - m.index);
|
|
||||||
var code = token.substr(2, token.length-2);
|
|
||||||
|
|
||||||
var pre = "";
|
|
||||||
var post = "";
|
|
||||||
|
|
||||||
switch (code) {
|
|
||||||
case 'm':
|
|
||||||
case '0m':
|
|
||||||
var len = styles.length;
|
|
||||||
for (var i=0; i < len; i++) {
|
|
||||||
styles.pop();
|
|
||||||
post += "</span>"
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case '30;42m': pre = '<span style="color:black;background:lime">'; break;
|
|
||||||
case '36m':
|
|
||||||
case '36;1m': pre = '<span style="color:cyan;">'; break;
|
|
||||||
case '31m':
|
|
||||||
case '31;31m': pre = '<span style="color:red;">'; break;
|
|
||||||
case '33m':
|
|
||||||
case '33;33m': pre = '<span style="color:yellow;">'; break;
|
|
||||||
case '32m':
|
|
||||||
case '0;32m': pre = '<span style="color:lime;">'; break;
|
|
||||||
case '90m': pre = '<span style="color:gray;">'; break;
|
|
||||||
case 'K':
|
|
||||||
case '0K':
|
|
||||||
case '1K':
|
|
||||||
case '2K': break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pre !== "") {
|
|
||||||
styles.push(pre);
|
|
||||||
}
|
|
||||||
|
|
||||||
final += part + pre + post;
|
|
||||||
}
|
|
||||||
|
|
||||||
var part = s.substring(current, s.length);
|
|
||||||
final += part;
|
|
||||||
return final;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
{{ if .Build.IsRunning }}
|
{{ if .Build.IsRunning }}
|
||||||
var outputBox = document.getElementById('stdout');
|
$(document).ready(function() {
|
||||||
var outputWS = new WebSocket((window.location.protocol=='http:'?'ws':'wss')+'://'+window.location.host+'/feed?token='+{{ .Token }});
|
var commitUpdates = new Drone.CommitUpdates('feed?token='+{{ .Token }});
|
||||||
outputWS.onopen = function () { console.log('output websocket open'); };
|
var outputBox = document.getElementById('stdout');
|
||||||
outputWS.onerror = function (e) { console.log('websocket error: ' + e); };
|
commitUpdates.startOutput(outputBox);
|
||||||
outputWS.onclose = function (e) { window.location.reload(); };
|
|
||||||
|
|
||||||
window.requestAnimationFrame = window.requestAnimationFrame ||
|
$("#follow").on("click", function(e) {
|
||||||
window.webkitRequestAnimationFrame ||
|
e.preventDefault();
|
||||||
function(callback, element) {
|
|
||||||
return window.setTimeout(function() {
|
|
||||||
callback(+new Date());
|
|
||||||
}, 1000 / 60);
|
|
||||||
};
|
|
||||||
|
|
||||||
var lineBuffer = "";
|
if(commitUpdates.autoFollow) {
|
||||||
|
commitUpdates.autoFollow = false;
|
||||||
outputWS.onmessage = function (e) {
|
$(this).text("Follow");
|
||||||
lineBuffer += formatLine(e.data);
|
} else {
|
||||||
};
|
commitUpdates.autoFollow = true;
|
||||||
|
$(this).text("Stop following");
|
||||||
function updateScreen() {
|
|
||||||
if(lineBuffer.length > 0) {
|
|
||||||
outputBox.innerHTML += lineBuffer;
|
|
||||||
lineBuffer = '';
|
|
||||||
|
|
||||||
if (window.autofollow) {
|
|
||||||
window.scrollTo(0, document.body.scrollHeight);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
requestAnimationFrame(updateScreen);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
requestAnimationFrame(updateScreen);
|
|
||||||
|
|
||||||
{{ else }}
|
{{ else }}
|
||||||
$.get("/{{ .Repo.Slug }}/commit/{{ .Commit.Hash }}/build/{{ .Build.Slug }}/out.txt", function( data ) {
|
$.get("/{{ .Repo.Slug }}/commit/{{ .Commit.Hash }}/build/{{ .Build.Slug }}/out.txt", function( data ) {
|
||||||
$( "#stdout" ).html(formatLine(data));
|
var lineFormatter = new Drone.LineFormatter();
|
||||||
|
$( "#stdout" ).html(lineFormatter.format(data));
|
||||||
});
|
});
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</script>
|
</script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -4,6 +4,9 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
|
"crypto/md5"
|
||||||
|
"strings"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/GeertJohan/go.rice"
|
"github.com/GeertJohan/go.rice"
|
||||||
)
|
)
|
||||||
|
@ -87,6 +90,17 @@ func init() {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assets := rice.MustFindBox("../../cmd/droned/assets")
|
||||||
|
mainjs, err := assets.String("js/main.js")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := md5.New()
|
||||||
|
io.WriteString(h, mainjs)
|
||||||
|
jshash := fmt.Sprintf("%x", h.Sum(nil))
|
||||||
|
base = strings.Replace(base, "main.js", "main.js?h=" + jshash, 1)
|
||||||
|
|
||||||
// extract the base form template as a string
|
// extract the base form template as a string
|
||||||
form, err := box.String("form.html")
|
form, err := box.String("form.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in a new issue