feat: machine pages Порты, Статус, Изменить as dialogs over the current page; Действие column in the ports table
This commit is contained in:
1 parent
2009796421
commit
d92696226e
11 files changed
+141
-5
No files matched your search
@@ -89,3 +89,19 @@ func TestSSHFSOpen(t *testing.T) {
|
|||||||
t.Fatalf("a user without the machine got %d", w.Code)
|
t.Fatalf("a user without the machine got %d", w.Code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Renaming from the machine dialog keeps it open on «Изменить».
|
||||||
|
func TestRenameStaysOnEdit(t *testing.T) {
|
||||||
|
s, c := newTestServer(t)
|
||||||
|
tok := "tok-test"
|
||||||
|
var req *http.Request
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/agents/x1/rename", strings.NewReader("csrf="+tok+"&name=new"))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.AddCookie(c)
|
||||||
|
req.AddCookie(&http.Cookie{Name: csrfCookie, Value: tok})
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.Handler().ServeHTTP(w, req)
|
||||||
|
if loc := w.Header().Get("Location"); w.Code != http.StatusSeeOther || loc != "/agents/x1/edit" {
|
||||||
|
t.Fatalf("rename: %d to %q", w.Code, loc)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -402,7 +402,8 @@ func (s *Server) handleAgentRename(w http.ResponseWriter, r *http.Request) {
|
|||||||
if name != "" {
|
if name != "" {
|
||||||
_ = s.db.RenameAgent(id, name)
|
_ = s.db.RenameAgent(id, name)
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
// Back to «Изменить», with the new name: the machine dialog stays open.
|
||||||
|
http.Redirect(w, r, "/agents/"+id+"/edit", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- settings ---
|
// --- settings ---
|
||||||
|
|||||||
@@ -780,3 +780,16 @@ p.mv-a { margin:0; font-size:13px; }
|
|||||||
body:not(.multi) .fpane.under-plus .fm-bar:first-child { margin-right:40px; }
|
body:not(.multi) .fpane.under-plus .fm-bar:first-child { margin-right:40px; }
|
||||||
/* The bars' square buttons, as tall as the path box beside them. */
|
/* The bars' square buttons, as tall as the path box beside them. */
|
||||||
.fm-bar > .btn.sq, .fp-sel .btn.sq { width:34px; height:34px; }
|
.fm-bar > .btn.sq, .fp-sel .btn.sq { width:34px; height:34px; }
|
||||||
|
|
||||||
|
/* A machine's Порты, Статус, Изменить over the current page. */
|
||||||
|
dialog.machinedlg { max-width:1000px; width:94vw; max-height:90vh; padding:0; overflow:hidden; }
|
||||||
|
/* The dialog's title names the page: the page's own heading goes. */
|
||||||
|
.machinedlg .frag-area .wrap > h1:first-child { display:none; }
|
||||||
|
.machinedlg .frag-area .wrap { padding-top:18px; padding-bottom:18px; }
|
||||||
|
.machinedlg .frag-area { overflow:auto; max-height:calc(90vh - 58px); }
|
||||||
|
.mdmenu { min-width:180px; }
|
||||||
|
.mdmenu .md-head { padding:6px 10px 4px; font-size:12px; }
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
dialog.machinedlg { width:100vw; max-width:none; height:100dvh; max-height:none; margin:0; border-radius:0; }
|
||||||
|
.machinedlg .frag-area { max-height:calc(100dvh - 58px); }
|
||||||
|
}
|
||||||
@@ -109,6 +109,13 @@
|
|||||||
catch (e) { alert('Нет связи с сервером'); return; }
|
catch (e) { alert('Нет связи с сервером'); return; }
|
||||||
if (signedOut(r)) { location.href = '/login'; return; }
|
if (signedOut(r)) { location.href = '/login'; return; }
|
||||||
const to = new URL(r.url);
|
const to = new URL(r.url);
|
||||||
|
// From a dialog to a view (a machine deleted): the dialog closes.
|
||||||
|
if (!area.classList.contains('view') && viewOf(to.pathname)) {
|
||||||
|
const d = area.closest('dialog');
|
||||||
|
if (d) d.close();
|
||||||
|
go(to.href);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (area.classList.contains('view')) {
|
if (area.classList.contains('view')) {
|
||||||
const v = viewOf(to.pathname);
|
const v = viewOf(to.pathname);
|
||||||
if (v && v !== area.dataset.view) { go(to.href); return; }
|
if (v && v !== area.dataset.view) { go(to.href); return; }
|
||||||
|
|||||||
@@ -270,6 +270,7 @@
|
|||||||
const q = s => this.q(s);
|
const q = s => this.q(s);
|
||||||
q('.pane-x').onclick = ev => { ev.stopPropagation(); Workspace.closePane(this); };
|
q('.pane-x').onclick = ev => { ev.stopPropagation(); Workspace.closePane(this); };
|
||||||
q('.pane-head').addEventListener('pointerdown', ev => { if (Workspace.desk && !ev.target.closest('.pane-x')) Workspace.startDrag(ev, 'pane', this); });
|
q('.pane-head').addEventListener('pointerdown', ev => { if (Workspace.desk && !ev.target.closest('.pane-x')) Workspace.startDrag(ev, 'pane', this); });
|
||||||
|
q('.pane-head').addEventListener('contextmenu', ev => { if (window.MachineDialog) MachineDialog.menu(ev, this.agent); });
|
||||||
this.el.addEventListener('pointerdown', () => Workspace.setActivePane(this), true);
|
this.el.addEventListener('pointerdown', () => Workspace.setActivePane(this), true);
|
||||||
q('.fp-up').onclick = () => this.go(parent(this.cwd));
|
q('.fp-up').onclick = () => this.go(parent(this.cwd));
|
||||||
q('.fp-refresh').onclick = () => this.reload();
|
q('.fp-refresh').onclick = () => this.reload();
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// Порты, Статус and Изменить of a machine in a dialog over the current page:
|
||||||
|
// the server's pages as fragments, so nothing behind is lost. They open from
|
||||||
|
// the buttons of a machine's row, and from a menu on a tab's title or a
|
||||||
|
// pane's title bar (a right click, or a long press on a phone). Closing the
|
||||||
|
// dialog unmounts the fragment's module: its charts and polling stop.
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
const TITLES = {forwards: 'Порты', status: 'Статус', edit: 'Изменить'};
|
||||||
|
const dlg = document.getElementById('machinedlg');
|
||||||
|
const area = dlg.querySelector('.frag-area');
|
||||||
|
const page = /^\/agents\/([^/]+)\/(forwards|status|edit)$/;
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
function open(id, what, name) {
|
||||||
|
dlg.querySelector('.md-title').textContent = (name || id) + ' · ' + TITLES[what];
|
||||||
|
App.put(area, '<p class="muted" style="padding:16px 18px">загрузка…</p>');
|
||||||
|
if (!dlg.open) (window.FileViews ? FileViews.openModal(dlg) : dlg.showModal());
|
||||||
|
App.load(area, '/agents/' + encodeURIComponent(id) + '/' + what);
|
||||||
|
}
|
||||||
|
dlg.addEventListener('close', () => {
|
||||||
|
App.put(area, '');
|
||||||
|
// A form sent in the dialog (a new name, say) shows in the list behind.
|
||||||
|
if (changed && document.querySelector('main#app').dataset.view === 'machines') App.go('/', false);
|
||||||
|
changed = false;
|
||||||
|
});
|
||||||
|
dlg.querySelector('.md-close').onclick = () => dlg.close();
|
||||||
|
// «Изменить» drawn again after a new name: the title follows it.
|
||||||
|
new MutationObserver(() => {
|
||||||
|
const h = area.querySelector('[data-module="edit"] .wrap > h1');
|
||||||
|
if (h) dlg.querySelector('.md-title').textContent = h.textContent.trim() + ' · ' + TITLES.edit;
|
||||||
|
}).observe(area, {childList: true});
|
||||||
|
dlg.addEventListener('submit', () => { changed = true; });
|
||||||
|
// The fragment's own links: another page of a machine stays in the dialog,
|
||||||
|
// anything else closes it and goes on as usual.
|
||||||
|
area.addEventListener('click', ev => {
|
||||||
|
const a = ev.target.closest('a[href]');
|
||||||
|
if (!a || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || a.target || a.hasAttribute('download')) return;
|
||||||
|
const u = new URL(a.href, location.href), m = page.exec(u.pathname);
|
||||||
|
if (u.origin !== location.origin) return;
|
||||||
|
if (m) { ev.preventDefault(); ev.stopPropagation(); App.load(area, u.pathname + u.search); return; }
|
||||||
|
dlg.close();
|
||||||
|
}, true);
|
||||||
|
document.addEventListener('click', ev => {
|
||||||
|
const b = ev.target.closest('[data-dialog]');
|
||||||
|
if (!b || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey) return;
|
||||||
|
ev.preventDefault();
|
||||||
|
open(b.dataset.id, b.dataset.dialog, b.dataset.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
// menu: Порты, Статус, Изменить of agent, by the pointer.
|
||||||
|
const menu = document.createElement('div');
|
||||||
|
menu.className = 'addmenu mdmenu';
|
||||||
|
menu.hidden = true;
|
||||||
|
document.body.appendChild(menu);
|
||||||
|
function closeMenu() { menu.hidden = true; document.removeEventListener('pointerdown', outside, true); }
|
||||||
|
function outside(ev) { if (!menu.contains(ev.target)) closeMenu(); }
|
||||||
|
function showMenu(ev, agent) {
|
||||||
|
ev.preventDefault();
|
||||||
|
menu.innerHTML = '';
|
||||||
|
const head = document.createElement('div');
|
||||||
|
head.className = 'muted md-head';
|
||||||
|
head.textContent = agent.name;
|
||||||
|
menu.appendChild(head);
|
||||||
|
Object.keys(TITLES).forEach(k => {
|
||||||
|
const b = document.createElement('button');
|
||||||
|
b.type = 'button'; b.className = 'item';
|
||||||
|
b.innerHTML = '<span><b></b></span>';
|
||||||
|
b.querySelector('b').textContent = TITLES[k];
|
||||||
|
b.onclick = () => { closeMenu(); open(agent.id, k, agent.name); };
|
||||||
|
menu.appendChild(b);
|
||||||
|
});
|
||||||
|
menu.hidden = false;
|
||||||
|
const m = menu.getBoundingClientRect();
|
||||||
|
menu.style.left = Math.max(8, Math.min(innerWidth - m.width - 8, ev.clientX)) + 'px';
|
||||||
|
menu.style.top = Math.max(8, Math.min(innerHeight - m.height - 8, ev.clientY)) + 'px';
|
||||||
|
setTimeout(() => document.addEventListener('pointerdown', outside, true), 0);
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', ev => { if (ev.key === 'Escape' && !menu.hidden) closeMenu(); });
|
||||||
|
|
||||||
|
// A machine page asked for by its address: the machines view with it open.
|
||||||
|
const m = window.APP && window.APP.dialog && page.exec(window.APP.dialog);
|
||||||
|
if (m) {
|
||||||
|
const row = document.querySelector('[data-open][data-id="' + CSS.escape(m[1]) + '"]');
|
||||||
|
history.replaceState({}, '', '/');
|
||||||
|
open(m[1], m[2], row ? row.dataset.name : m[1]);
|
||||||
|
}
|
||||||
|
window.MachineDialog = {open, menu: showMenu};
|
||||||
|
})();
|
||||||
@@ -297,6 +297,7 @@ window.TermKeys = (function () {
|
|||||||
this.term.textarea.addEventListener('focus', () => W.setActivePane(this));
|
this.term.textarea.addEventListener('focus', () => W.setActivePane(this));
|
||||||
this.el.querySelector('.pane-x').onclick = ev => { ev.stopPropagation(); W.closePane(this); };
|
this.el.querySelector('.pane-x').onclick = ev => { ev.stopPropagation(); W.closePane(this); };
|
||||||
if (W.desk) this.el.querySelector('.pane-head').addEventListener('pointerdown', ev => W.startDrag(ev, 'pane', this));
|
if (W.desk) this.el.querySelector('.pane-head').addEventListener('pointerdown', ev => W.startDrag(ev, 'pane', this));
|
||||||
|
this.el.querySelector('.pane-head').addEventListener('contextmenu', ev => { if (window.MachineDialog) MachineDialog.menu(ev, this.agent); });
|
||||||
this.open();
|
this.open();
|
||||||
}
|
}
|
||||||
setState(state, text) {
|
setState(state, text) {
|
||||||
|
|||||||
@@ -36,6 +36,11 @@
|
|||||||
t.el.addEventListener('click', ev => { if (!ev.target.closest('.tab-x, input') && !dragJustEnded) activate(t); });
|
t.el.addEventListener('click', ev => { if (!ev.target.closest('.tab-x, input') && !dragJustEnded) activate(t); });
|
||||||
t.el.querySelector('.tab-x').onclick = ev => { ev.stopPropagation(); closeTab(t); };
|
t.el.querySelector('.tab-x').onclick = ev => { ev.stopPropagation(); closeTab(t); };
|
||||||
t.el.querySelector('.tab-title').addEventListener('dblclick', ev => { ev.stopPropagation(); rename(t); });
|
t.el.querySelector('.tab-title').addEventListener('dblclick', ev => { ev.stopPropagation(); rename(t); });
|
||||||
|
// Its machine's Порты, Статус, Изменить: a right click on the title.
|
||||||
|
t.el.addEventListener('contextmenu', ev => {
|
||||||
|
const p = t.panes.includes(activePane) ? activePane : t.panes[0];
|
||||||
|
if (window.MachineDialog && !ev.target.closest('input')) MachineDialog.menu(ev, p.agent);
|
||||||
|
});
|
||||||
if (desk) t.el.addEventListener('pointerdown', ev => { if (!ev.target.closest('.tab-x, input')) startDrag(ev, 'tab', t); });
|
if (desk) t.el.addEventListener('pointerdown', ev => { if (!ev.target.closest('.tab-x, input')) startDrag(ev, 'tab', t); });
|
||||||
const at = index == null ? tabs.length : index;
|
const at = index == null ? tabs.length : index;
|
||||||
tablist.insertBefore(t.el, tabs[at] ? tabs[at].el : mark);
|
tablist.insertBefore(t.el, tabs[at] ? tabs[at].el : mark);
|
||||||
|
|||||||
@@ -189,6 +189,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<dialog id="machinedlg" class="machinedlg" tabindex="-1"><div class="dlg-h"><strong class="md-title" style="flex:1"></strong>
|
||||||
|
<button type="button" class="btn ghost small icon close x md-close" aria-label="Закрыть"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button></div>
|
||||||
|
<div class="frag-area"></div></dialog>
|
||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
<script>window.APP = {admin: {{.IsAdmin}}, open: {{.Open}}, dialog: {{.Dialog}}, editLimit: {{.EditLimit}}};</script>
|
<script>window.APP = {admin: {{.IsAdmin}}, open: {{.Open}}, dialog: {{.Dialog}}, editLimit: {{.EditLimit}}};</script>
|
||||||
<script src="{{asset "wsutil.js"}}"></script>
|
<script src="{{asset "wsutil.js"}}"></script>
|
||||||
@@ -206,6 +209,7 @@
|
|||||||
<script src="{{asset "pages.js"}}"></script>
|
<script src="{{asset "pages.js"}}"></script>
|
||||||
<script src="{{asset "users.js"}}"></script>
|
<script src="{{asset "users.js"}}"></script>
|
||||||
<script src="{{asset "account.js"}}"></script>
|
<script src="{{asset "account.js"}}"></script>
|
||||||
|
<script src="{{asset "machine.js"}}"></script>
|
||||||
<script>App.start();</script>
|
<script>App.start();</script>
|
||||||
</body>
|
</body>
|
||||||
</html>{{end}}
|
</html>{{end}}
|
||||||
@@ -21,13 +21,13 @@
|
|||||||
<a class="btn ghost small icon" href="/agents/{{.ID}}/files" data-open="files" data-id="{{.ID}}" data-name="{{.Name}}" data-host="{{.Hostname}}" data-user="{{.TerminalUser}}" data-tip="Файлы" aria-label="Файлы">
|
<a class="btn ghost small icon" href="/agents/{{.ID}}/files" data-open="files" data-id="{{.ID}}" data-name="{{.Name}}" data-host="{{.Hostname}}" data-user="{{.TerminalUser}}" data-tip="Файлы" aria-label="Файлы">
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||||||
</a>
|
</a>
|
||||||
<a class="btn ghost small icon" href="/agents/{{.ID}}/forwards" data-tip="Порты" aria-label="Порты">
|
<a class="btn ghost small icon" href="/agents/{{.ID}}/forwards" data-dialog="forwards" data-id="{{.ID}}" data-name="{{.Name}}" data-tip="Порты" aria-label="Порты">
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
|
||||||
</a>
|
</a>
|
||||||
<a class="btn ghost small icon" href="/agents/{{.ID}}/status" data-tip="Статус" aria-label="Статус">
|
<a class="btn ghost small icon" href="/agents/{{.ID}}/status" data-dialog="status" data-id="{{.ID}}" data-name="{{.Name}}" data-tip="Статус" aria-label="Статус">
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
||||||
</a>
|
</a>
|
||||||
<a class="btn ghost small icon" href="/agents/{{.ID}}/edit" data-tip="Изменить" aria-label="Изменить">
|
<a class="btn ghost small icon" href="/agents/{{.ID}}/edit" data-dialog="edit" data-id="{{.ID}}" data-name="{{.Name}}" data-tip="Изменить" aria-label="Изменить">
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>
|
||||||
</a>
|
</a>
|
||||||
{{if $.IsAdmin}}<form method="post" action="/agents/{{.ID}}/delete" style="display:inline"
|
{{if $.IsAdmin}}<form method="post" action="/agents/{{.ID}}/delete" style="display:inline"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<h2>Правила</h2>
|
<h2>Правила</h2>
|
||||||
{{if .Rules}}
|
{{if .Rules}}
|
||||||
<table class="stack">
|
<table class="stack">
|
||||||
<thead><tr><th>Тип</th><th>Хост (порт)</th><th>→ Клиент (порт)</th><th>Слушать на</th><th>Метка</th><th></th></tr></thead>
|
<thead><tr><th>Тип</th><th>Хост (порт)</th><th>→ Клиент (порт)</th><th>Слушать на</th><th>Метка</th><th style="text-align:right">Действие</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range .Rules}}
|
{{range .Rules}}
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
Reference in new issue
Block a user