mirror of https://github.com/Aidaho12/haproxy-wi
445 lines
17 KiB
HTML
445 lines
17 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{lang.menu_links.config.h2}} {{service_desc.service}}{% endblock %}
|
|
{% block h2 %}{{lang.menu_links.config.h2}} {{service_desc.service}}{% endblock %}
|
|
{% block content %}
|
|
<link rel="stylesheet" href="/static/js/codemirror/lib/codemirror.css">
|
|
<link rel="stylesheet" href="/static/js/codemirror/addon/dialog/dialog.css">
|
|
<link rel="stylesheet" href="/static/js/codemirror/addon/fold/foldgutter.css">
|
|
<script src="/static/js/codemirror/lib/codemirror.js"></script>
|
|
<script src="/static/js/codemirror/addon/search/search.js"></script>
|
|
<script src="/static/js/codemirror/addon/search/searchcursor.js"></script>
|
|
<script src="/static/js/codemirror/addon/search/jump-to-line.js"></script>
|
|
<script src="/static/js/codemirror/addon/search/matchesonscrollbar.js"></script>
|
|
<script src="/static/js/codemirror/addon/search/match-highlighter.js"></script>
|
|
<script src="/static/js/codemirror/addon/dialog/dialog.js"></script>
|
|
<script src="/static/js/codemirror/addon/edit/matchbrackets.js"></script>
|
|
<script src="/static/js/codemirror/addon/edit/closebrackets.js"></script>
|
|
<script src="/static/js/codemirror/addon/comment/comment.js"></script>
|
|
<script src="/static/js/codemirror/addon/wrap/hardwrap.js"></script>
|
|
<script src="/static/js/codemirror/addon/fold/foldcode.js"></script>
|
|
<script src="/static/js/codemirror/addon/fold/foldgutter.js"></script>
|
|
<script src="/static/js/codemirror/addon/fold/brace-fold.js"></script>
|
|
<script src="/static/js/codemirror/addon/fold/comment-fold.js"></script>
|
|
<script src="/static/js/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
|
<script src="/static/js/codemirror/mode/nginx.js"></script>
|
|
<script src="/static/js/codemirror/mode/haproxy.js"></script>
|
|
<script src="/static/js/codemirror/keymap/sublime.js"></script>
|
|
<link rel="stylesheet" href="/static/js/diff2html/diff2html.min.css">
|
|
<script src="/static/js/diff2html/diff2html.min.js"></script>
|
|
<script src="/static/js/diff2html/diff2html-ui.min.js"></script>
|
|
<script src="/static/js/configshow.js"></script>
|
|
<script src="/static/js/add.js"></script>
|
|
<script src="/static/js/add_nginx.js"></script>
|
|
<script src="/static/js/edit_config.js"></script>
|
|
|
|
{% if is_serv_protected and g.user_params['role'] > 2 %}
|
|
<meta http-equiv="refresh" content="0; url=/service">
|
|
{% else %}
|
|
{% if g.user_params['servers']|length == 0 %}
|
|
{% include 'include/getstarted.html' %}
|
|
{% else %}
|
|
<p>
|
|
<form action="{{ action }}" method="post" class="left-space">
|
|
<input type="hidden" id="service" value="{{service|default('haproxy', true)}}" />
|
|
{{ select('serv', values=g.user_params['servers'], is_servers='true', selected=serv) }}
|
|
{% if service == 'nginx' or service == 'apache' %}
|
|
<a class="ui-button ui-widget ui-corner-all" title="{{lang.words.show|title()}} {{lang.words.running}} {{lang.words.config}}" onclick="showConfigFiles()">{{lang.words.open|title()}}</a>
|
|
{% else %}
|
|
<a class="ui-button ui-widget ui-corner-all" title="{{lang.words.show|title()}} {{lang.words.running}} {{lang.words.config}}" onclick="showConfig()">{{lang.words.open|title()}}</a>
|
|
{% endif %}
|
|
{% if service != 'keepalived' and service != 'apache' %}
|
|
<a class="ui-button ui-widget ui-corner-all" title="{{lang.words.view|title()}} {{lang.words.stat}}" onclick="openStats()">{{lang.menu_links.stats.link}}</a>
|
|
{% endif %}
|
|
{% if service != 'keepalived' and service != 'nginx' and service != 'apache' %}
|
|
<a class="ui-button ui-widget ui-corner-all" title="{{lang.words.show|title()}} {{lang.words.map}}" onclick="showMap()">{{lang.words.map|title()}}</a>
|
|
{% endif %}
|
|
<a class="ui-button ui-widget ui-corner-all" title="{{lang.words.compare|title()}} {{lang.words.configs}}" onclick="showCompareConfigs()">{{lang.words.compare|title()}}</a>
|
|
{% if g.user_params['role'] <= 3 %}
|
|
<a class="ui-button ui-widget ui-corner-all" title="{{lang.words.show|title()}} {{lang.words.versions}}" onclick="openVersions()">{{lang.menu_links.versions.link}}</a>
|
|
{% endif %}
|
|
{% if g.user_params['role'] <= 2 %}
|
|
<a href="/admin#backup" class="ui-button ui-widget ui-corner-all" title="Git">Git</a>
|
|
{% endif %}
|
|
</form>
|
|
<div id="ajax-config_file_name"></div>
|
|
{% endif %}
|
|
|
|
{% if stderr or error %}
|
|
{% include 'include/errors.html' %}
|
|
{% endif %}
|
|
|
|
{% if config %}
|
|
{% if g.user_params['role'] <= 3 %}
|
|
<h4 class="left-space">{{lang.words.config|title()}} {% if config_file_name and config_file_name != 'undefined' %}{{config_file_name.replace('92', '/')}}{%endif%} {{lang.words.from}} {{ serv }}</h4>
|
|
<form action="/config/{{service}}/{{serv}}" name="saveconfig" id="saveconfig" method="post" class="left-space">
|
|
<input type="hidden" value="{{ cfg }}.old" name="config_local_path">
|
|
<input type="hidden" value="{{ service }}" name="service">
|
|
<input type="hidden" value="{{ config_file_name }}" name="file_path">
|
|
<div>
|
|
<textarea name="config" id="config_text_area" class="config" rows="35" cols="100">{{ config }}</textarea>
|
|
</div>
|
|
<p>
|
|
<a href="/config/{{service}}/{{serv}}/show" class="ui-button ui-widget ui-corner-all" title="{{lang.phrases.return_to_config}}">{{lang.words.back|title()}}</a>
|
|
<button type="submit" value="test" name="save" class="btn btn-default" title="{{lang.words.check|title()}} {{lang.words.config}} {{lang.words.without}} {{lang.words.saving}}">{{lang.phrases.check_config}}</button>
|
|
<button type="submit" value="save" name="save" class="btn btn-default" title="{{lang.phrases.save_title}}">{{lang.words.save|title()}}</button>
|
|
{% if is_restart|int == 0 %}
|
|
<button type="submit" value="restart" name="" class="btn btn-default">{{lang.phrases.save_and_restart}}</button>
|
|
{% endif %}
|
|
<button type="submit" value="reload" name="save" class="btn btn-default">{{lang.phrases.save_and_reload}}</button>
|
|
{% if service != 'keepalived' %}
|
|
<div class="alert alert-info" style="margin-left: -0px;"><b>{{lang.words.note|title()}}:</b> {{lang.phrases.master_slave}}</div>
|
|
{% endif %}
|
|
</p>
|
|
</form>
|
|
{% endif %}
|
|
{% endif %}
|
|
<script>
|
|
var cur_url = window.location.href.split('/');
|
|
if (cur_url[4] === 'map') {
|
|
showMap();
|
|
}
|
|
if (cur_url[4] === 'compare') {
|
|
showCompareConfigs();
|
|
}
|
|
if (cur_url[6] === 'show') {
|
|
if (cur_url[4] === 'nginx') {
|
|
showConfigFiles(false, cur_url[7]);
|
|
}
|
|
showConfig();
|
|
}
|
|
if (cur_url[6] === 'show-files') {
|
|
showConfigFiles();
|
|
}
|
|
if (cur_url[6] === 'findInConfig') {
|
|
let words = findGetParameter('findInConfig');
|
|
waitForElm('#finding_words_from').then((elm) => {
|
|
$('#find_p').show();
|
|
$('#words').val(words);
|
|
findInConfig(words);
|
|
});
|
|
}
|
|
if (cur_url[4] === 'config_file_name') {
|
|
showConfigFilesForEditing();
|
|
}
|
|
if (cur_url[4] === 'haproxy' && cur_url[6] === 'edit') {
|
|
var myCodeMirror = CodeMirror.fromTextArea(document.getElementById("config_text_area"),
|
|
{
|
|
mode: "haproxy",
|
|
lineNumbers: true,
|
|
lineWrapping: true,
|
|
autocapitalize: true,
|
|
autocorrect: true,
|
|
spellcheck: true,
|
|
autoCloseBrackets: true,
|
|
keyMap: "sublime",
|
|
matchBrackets: true,
|
|
foldGutter: true,
|
|
showCursorWhenSelecting: true,
|
|
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "breakpoints"],
|
|
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: true}
|
|
});
|
|
} else if (cur_url[6] === 'edit') {
|
|
var myCodeMirror = CodeMirror.fromTextArea(document.getElementById("config_text_area"),
|
|
{
|
|
mode: "nginx",
|
|
lineNumbers: true,
|
|
lineWrapping: true,
|
|
autocapitalize: true,
|
|
autocorrect: true,
|
|
spellcheck: true,
|
|
autoCloseBrackets: true,
|
|
keyMap: "sublime",
|
|
matchBrackets: true,
|
|
foldGutter: true,
|
|
showCursorWhenSelecting: true,
|
|
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "breakpoints"],
|
|
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: true}
|
|
});
|
|
}
|
|
if (cur_url[6] === 'edit') {
|
|
myCodeMirror.on("gutterClick", function (cm, n) {
|
|
let info = cm.lineInfo(n);
|
|
cm.setGutterMarker(n, "breakpoints", info.gutterMarkers ? null : makeMarker());
|
|
});
|
|
myCodeMirror.on("beforeChange", function (cm, change) {
|
|
$(window).bind('beforeunload', function () {
|
|
return 'Are you sure you want to leave?';
|
|
});
|
|
});
|
|
}
|
|
function makeMarker() {
|
|
var marker = document.createElement("div");
|
|
marker.style.color = "#822";
|
|
marker.innerHTML = "●";
|
|
return marker;
|
|
}
|
|
</script>
|
|
<style>
|
|
.CodeMirror {
|
|
line-height: 1.2em;
|
|
height: 70%;
|
|
}
|
|
</style>
|
|
<script>
|
|
if (cur_url[6] === 'edit') {
|
|
myCodeMirror.refresh();
|
|
}
|
|
$(document).bind('keydown', 'ctrl+s', function (){
|
|
$("[type='submit'][value='save']").click();
|
|
});
|
|
$(document).bind('keydown', 'ctrl+d', function (){
|
|
$("[type='submit'][value='test']").click();
|
|
});
|
|
$(document).bind('keydown', 'ctrl+e', function (){
|
|
$("[type='submit'][value='reload']").click();
|
|
});
|
|
{% if is_restart|int == 0 %}
|
|
$(document).bind('keydown', 'ctrl+r', function (){
|
|
$("[type='submit'][value='restart']").click();
|
|
});
|
|
{% endif %}
|
|
</script>
|
|
{% if user_subscription.user_status != 0 %}
|
|
<script>
|
|
function explainSelected(mode) {
|
|
const selectedText = myCodeMirror.getSelection();
|
|
const from = myCodeMirror.getCursor("from");
|
|
const to = myCodeMirror.getCursor("to");
|
|
|
|
if (!selectedText.trim()) {
|
|
alert("{{ lang.phrases.select_part }}");
|
|
return;
|
|
}
|
|
|
|
let prompt = "";
|
|
let previewMode = "replace"; // default
|
|
|
|
if (mode === "explain") {
|
|
prompt = `{{ lang.phrases.explain_config }} {{ service }}. {{ lang.phrases.just_explain }}`;
|
|
previewMode = "explanation"; // не заменяем текст
|
|
} else if (mode === "optimize") {
|
|
prompt = `{{ lang.phrases.optimize_config }}`;
|
|
} else if (mode === "commented") {
|
|
prompt = `{{ lang.phrases.comment_config }}`;
|
|
} else if (mode === "syntax") {
|
|
prompt = `{{ lang.phrases.syntax_config }}`;
|
|
}
|
|
NProgress.start();
|
|
fetch("/api/gpt", {
|
|
method: "POST",
|
|
headers: {"Content-Type": "application/json"},
|
|
body: JSON.stringify({
|
|
prompt: `${prompt}\n\n${selectedText}`
|
|
})
|
|
})
|
|
.then(response => response.text().then(text => {
|
|
|
|
if (!response.ok) {
|
|
// Пробуем получить error из JSON, если он там есть
|
|
let message = text;
|
|
try {
|
|
const json = JSON.parse(text);
|
|
if (json.error) {
|
|
message = json.error;
|
|
}
|
|
} catch (_) {
|
|
// Тело не JSON — оставим как есть
|
|
}
|
|
|
|
NProgress.done();
|
|
return alert(`Ошибка API (${response.status}):\n\n${message}`);
|
|
}
|
|
let data;
|
|
try {
|
|
data = JSON.parse(text);
|
|
} catch (e) {
|
|
NProgress.done();
|
|
alert("{{ lang.phrases.error_parsing }}");
|
|
return;
|
|
}
|
|
let result = typeof data.response === "string" ? data.response.trim() : "";
|
|
if (!result) {
|
|
NProgress.done();
|
|
return alert("{{ lang.phrases.empty_ans }}");
|
|
}
|
|
|
|
result = result.replace(/^```(?:[a-zA-Z]+)?\s*/gm, "");
|
|
result = result.replace(/```$/gm, "");
|
|
|
|
if (previewMode === "explanation" || previewMode === "syntax") {
|
|
showExplanation(result);
|
|
} else {
|
|
showPreview(selectedText, result, from, to);
|
|
}
|
|
NProgress.done();
|
|
}))
|
|
.catch(err => {
|
|
NProgress.done();
|
|
alert("Error: " + err.message);
|
|
console.error(err);
|
|
});
|
|
}
|
|
|
|
|
|
let previewOld = null;
|
|
let previewNew = null;
|
|
let previewFrom = null;
|
|
let previewTo = null;
|
|
|
|
function showExplanation(text) {
|
|
const container = document.getElementById("previewContent");
|
|
container.innerHTML = `<pre style="white-space:pre-wrap;">${text}</pre>`;
|
|
|
|
document.getElementById("applyButton").style.display = "none";
|
|
document.getElementById("previewModal").style.display = "block";
|
|
}
|
|
function showPreview(oldCode, newCode, from, to) {
|
|
previewOld = oldCode;
|
|
previewNew = newCode;
|
|
previewFrom = from;
|
|
previewTo = to;
|
|
|
|
const diffString = createUnifiedDiff(oldCode, newCode);
|
|
const container = document.getElementById("previewContent");
|
|
|
|
// Очищаем старое содержимое
|
|
container.innerHTML = "";
|
|
|
|
const diffUi = new Diff2HtmlUI(container, diffString, {
|
|
drawFileList: false,
|
|
matching: 'lines',
|
|
outputFormat: 'side-by-side'
|
|
});
|
|
|
|
diffUi.draw();
|
|
diffUi.highlightCode();
|
|
|
|
document.getElementById("previewModal").style.display = "block";
|
|
document.getElementById("applyButton").style.display = "inline-block";
|
|
}
|
|
|
|
function closePreview() {
|
|
document.getElementById("previewModal").style.display = "none";
|
|
document.querySelector("#previewModal button").disabled = false;
|
|
|
|
previewOld = previewNew = previewFrom = previewTo = null;
|
|
}
|
|
|
|
function applyPreview() {
|
|
if (previewNew && previewFrom && previewTo) {
|
|
myCodeMirror.replaceRange(previewNew.trim(), previewFrom, previewTo);
|
|
}
|
|
closePreview();
|
|
}
|
|
|
|
function createUnifiedDiff(oldText, newText) {
|
|
const oldLines = oldText.trim().split("\n");
|
|
const newLines = newText.trim().split("\n");
|
|
const fileName = "config.conf";
|
|
const header = `diff --git a/${fileName} b/${fileName}\n--- a/${fileName}\n+++ b/${fileName}\n`;
|
|
const diffLines = [header];
|
|
|
|
const maxLines = Math.max(oldLines.length, newLines.length);
|
|
for (let i = 0; i < maxLines; i++) {
|
|
const oldLine = oldLines[i] || "";
|
|
const newLine = newLines[i] || "";
|
|
|
|
if (oldLine === newLine) {
|
|
diffLines.push(" " + oldLine);
|
|
} else {
|
|
if (oldLine) diffLines.push("-" + oldLine);
|
|
if (newLine) diffLines.push("+" + newLine);
|
|
}
|
|
}
|
|
|
|
return diffLines.join("\n");
|
|
}
|
|
function doClipboardAction(action) {
|
|
const text = myCodeMirror.getSelection();
|
|
if (!text && action !== "paste") return;
|
|
|
|
navigator.clipboard[action === "paste" ? "readText" : "writeText"](
|
|
action === "paste" ? undefined : text
|
|
).then(result => {
|
|
if (action === "cut") {
|
|
myCodeMirror.replaceSelection("");
|
|
} else if (action === "paste") {
|
|
myCodeMirror.replaceSelection(result);
|
|
}
|
|
hideCustomMenu();
|
|
}).catch(err => {
|
|
alert("Clipboard error: " + err.message);
|
|
console.error(err);
|
|
});
|
|
}
|
|
|
|
function handleContextAction(mode) {
|
|
hideCustomMenu();
|
|
explainSelected(mode);
|
|
}
|
|
|
|
function hideCustomMenu() {
|
|
document.getElementById("customContextMenu").style.display = "none";
|
|
}
|
|
|
|
document.addEventListener("contextmenu", function (e) {
|
|
const isInCodeMirror = e.target.closest(".CodeMirror");
|
|
if (!isInCodeMirror) return;
|
|
|
|
e.preventDefault(); // блокируем стандартное меню
|
|
|
|
const menu = document.getElementById("customContextMenu");
|
|
menu.style.left = `${e.pageX + 8}px`;
|
|
menu.style.top = `${e.pageY}px`;
|
|
menu.style.display = "block";
|
|
|
|
setTimeout(() => {
|
|
document.addEventListener("click", hideCustomMenu, { once: true });
|
|
}, 1);
|
|
});
|
|
|
|
</script>
|
|
<style>
|
|
.context-item {
|
|
padding: 6px 12px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.context-item:hover {
|
|
background-color: #f0f0f0;
|
|
}
|
|
</style>
|
|
<div id="customContextMenu" style="
|
|
display: none;
|
|
position: absolute;
|
|
background: #fff;
|
|
border: 1px solid #ccc;
|
|
z-index: 10000;
|
|
padding: 5px;
|
|
font-family: sans-serif;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
|
min-width: 180px;
|
|
border-radius: 6px;
|
|
">
|
|
<div class="context-item" onclick="doClipboardAction('cut')">✂️ {{ lang.words.w_cut|title() }}</div>
|
|
<div class="context-item" onclick="doClipboardAction('copy')">📋 {{ lang.words.w_copy|title() }}</div>
|
|
<div class="context-item" onclick="doClipboardAction('paste')">📥 {{ lang.words.w_paste|title() }}</div>
|
|
<hr>
|
|
<div class="context-item" onclick="handleContextAction('explain')">💬 {{ lang.phrases.explain_selection }}</div>
|
|
<div class="context-item" onclick="handleContextAction('optimize')">⚙️ {{ lang.phrases.optimize_selection }}</div>
|
|
<div class="context-item" onclick="handleContextAction('commented')">📝 {{ lang.phrases.enter_comment }}</div>
|
|
<div class="context-item" onclick="handleContextAction('syntax')">✅ {{ lang.phrases.check_syntax }}</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
<div id="previewModal" style="display:none; position:fixed; top:10%; left:50%; transform:translateX(-50%);
|
|
background:white; border:1px solid #ccc; padding:20px; z-index:9999; width:80%; max-height:80%; overflow:auto; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
|
|
<h3>{{ lang.phrases.preview_change }}</h3>
|
|
<div id="previewContent"></div>
|
|
<div id="previewButtons" style="text-align:right; margin-top:10px;">
|
|
<button id="applyButton" onclick="applyPreview()">{{ lang.words.apply|title() }}</button>
|
|
<button onclick="closePreview()">{{ lang.words.cancel|title() }}</button>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|