sessions: move the ad-hoc session form into a modal

The ad-hoc session form sat in a collapsible framed box above the session
list. It now opens as a modal from a button on the right of the Sessions
heading, dropping the page's framed form box inside the dialog, with the
primary button first and Cancel at the same height as Connect, matching the
other modals. Section headings centre their heading and action button on
one line.

Part of #254

tests: the ad-hoc session form opens as a modal
(cherry picked from commit 7048ca6860)
This commit is contained in:
Jad Seifeddine
2026-10-13 13:58:04 +07:00
committed by Dave Kempe
parent c421515a79
commit e1746b4077
4 changed files with 71 additions and 20 deletions
+2
View File
@@ -363,6 +363,8 @@ tbody tr:last-child td { border-bottom: none; }
padding-bottom: var(--s-2);
border-bottom: 1px solid var(--border);
}
/* A section's action button sits on the right of its heading band. */
.section-head .section-action { margin-left: auto; align-self: center; }
.section-head strong {
color: var(--accent);
font-size: var(--fz-sm);
+33 -20
View File
@@ -46,6 +46,8 @@
font-weight: normal;
}
.btn-add-hop:hover { background: var(--surface); border-color: var(--accent); }
.adhoc-modal form { background: transparent; border: 0; padding: 0; margin: 0; max-width: none; width: auto; }
.adhoc-modal #new-session-fields { margin-top: 0; }
</style>
<script>(function(){var c=localStorage.getItem('rustguac_theme_colors');if(c){try{var o=JSON.parse(c),r=document.documentElement.style;for(var k in o)r.setProperty('--'+k.replace(/_/g,'-'),o[k]);if(o.bg_pattern&&o.bg_pattern!=='none'){var s=document.createElement('style');s.id='bg-pattern-style';s.textContent='body{background-image:'+o.bg_pattern+';background-attachment:fixed}';document.head.appendChild(s)}}catch(e){}}})();</script>
</head>
@@ -70,12 +72,12 @@
Use the <a href="/connections.html">Connections</a> to connect to saved entries.
</div>
<form id="session-form" style="display:none">
<div style="display:flex;align-items:center;gap:0.5em;cursor:pointer" id="new-session-toggle">
<span id="new-session-arrow" style="color:var(--accent)">&#9654;</span>
<strong style="color:var(--accent)">New Ad-Hoc Session</strong>
</div>
<div id="new-session-fields" style="display:none;margin-top:0.8em">
<div id="adhoc-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="adhoc-modal-title">
<div class="modal adhoc-modal">
<h3 id="adhoc-modal-title">New Ad-Hoc Session</h3>
<p class="modal-hint">A one-off session to any host. Nothing is saved; use Connections for entries you come back to.</p>
<form id="session-form">
<div id="new-session-fields">
<label>Type
<select id="session-type" style="display:block;width:100%;padding:0.4em;margin-top:0.2em;background:var(--input);border:1px solid var(--border);color:var(--text);font-family:monospace;font-size:1em;border-radius:3px;">
<option value="ssh" selected>SSH</option>
@@ -175,16 +177,23 @@
<label>Banner message (shown to user before session starts)
<input type="text" id="banner" placeholder="e.g. Please enter your credentials when prompted">
</label>
<button type="submit" id="connect-btn">Connect</button>
<div class="modal-actions">
<button type="submit" id="connect-btn">Connect</button>
<button type="button" class="btn-cancel" id="adhoc-cancel">Cancel</button>
</div>
<div id="error"></div>
</div>
</form>
</div>
</div>
<div id="sessions">
<div class="section-head">
<strong>Sessions</strong>
<span class="section-count" id="session-counts"></span>
<button type="button" class="btn-add section-action" id="new-session-btn" style="display:none" title="Start a one-off session to any host. Nothing is saved.">&#9889; New ad-hoc session</button>
</div>
<div id="session-filters" class="list-tools"></div>
<table>
<thead><tr>
<th>ID</th><th>Type</th><th>Host</th><th>User</th><th>Owner</th><th>Status</th><th>Viewers</th><th></th><th></th>
@@ -237,7 +246,7 @@
}
if (level >= 3) {
// poweruser+ can create ad-hoc sessions
document.getElementById('session-form').style.display = '';
document.getElementById('new-session-btn').style.display = '';
document.getElementById('reports-link').style.display = '';
}
})
@@ -248,7 +257,7 @@
document.documentElement.setAttribute('data-role-level', '4');
// API key users are always admin
myRole = 'admin';
document.getElementById('session-form').style.display = '';
document.getElementById('new-session-btn').style.display = '';
document.getElementById('admin-link').style.display = '';
document.getElementById('tokens-link').style.display = '';
document.getElementById('reports-link').style.display = '';
@@ -318,17 +327,21 @@
jumpSection.style.display = v === 'vdi' ? 'none' : '';
});
// Collapsible "New Ad-Hoc Session" section
document.getElementById('new-session-toggle').addEventListener('click', function() {
var fields = document.getElementById('new-session-fields');
var arrow = document.getElementById('new-session-arrow');
if (fields.style.display === 'none') {
fields.style.display = '';
arrow.innerHTML = '&#9660;';
} else {
fields.style.display = 'none';
arrow.innerHTML = '&#9654;';
}
// "New ad-hoc session" opens the form in a modal; Cancel, Escape or
// a click on the backdrop closes it. The form itself is unchanged.
var adhocModal = document.getElementById('adhoc-modal');
function openAdhoc() {
adhocModal.classList.add('active');
document.getElementById('error').textContent = '';
var first = document.getElementById('session-type');
if (first) { try { first.focus(); } catch (e) {} }
}
function closeAdhoc() { adhocModal.classList.remove('active'); }
document.getElementById('new-session-btn').addEventListener('click', openAdhoc);
document.getElementById('adhoc-cancel').addEventListener('click', closeAdhoc);
adhocModal.addEventListener('mousedown', function(e) { if (e.target === adhocModal) closeAdhoc(); });
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && adhocModal.classList.contains('active')) closeAdhoc();
});
// Show jump section on initial load (default is SSH)
+1
View File
@@ -28,6 +28,7 @@ One spec per change, named after the branch that introduced it:
| `connections-folder-filter` | #254: live filter box, ARIA tree, arrow keys, `f` shortcut |
| `connections-app-shell` | #254: clipped body, two scroll panes, folder change scrolls the pane not the page |
| `role-nav` | one nav on every page, gated by `data-role-level` |
| `sessions-adhoc-modal` | ad-hoc form opens as a modal |
The fixture suppresses the first-visit onboarding tour (`options.onboarding`
re-enables it) and defaults to the admin role; pass `role` to test gating.
@@ -0,0 +1,35 @@
// The ad-hoc session form lives in a modal opened from the button on the
// Sessions heading; Cancel and Connect share a height.
import { test, expect } from '@playwright/test';
import { installApi, settle } from '../lib/api.mjs';
test('the ad-hoc form opens as a modal and Cancel closes it', async ({ page }) => {
await installApi(page, { role: 'poweruser' });
await page.goto('/sessions.html');
await settle(page);
const modal = page.locator('#adhoc-modal');
await expect(modal).not.toHaveClass(/active/);
const btn = page.locator('#new-session-btn');
await expect(btn).toBeVisible();
await btn.click();
await expect(modal).toHaveClass(/active/);
await expect(modal).toHaveAttribute('role', 'dialog');
await expect(page.locator('#adhoc-modal-title')).toBeVisible();
await expect(page.locator('#session-form')).toBeVisible();
const [ok, cancel] = await Promise.all([
page.locator('#connect-btn').boundingBox(),
page.locator('#adhoc-cancel').boundingBox()
]);
expect(Math.abs(ok.height - cancel.height), 'Connect and Cancel must be the same height').toBeLessThanOrEqual(1);
await page.locator('#adhoc-cancel').click();
await expect(modal).not.toHaveClass(/active/);
});
test('a viewer gets no ad-hoc button', async ({ page }) => {
await installApi(page, { role: 'viewer' });
await page.goto('/sessions.html');
await settle(page);
await expect(page.locator('#new-session-btn')).toBeHidden();
});