portal: multi-line topic scope input + 'Add topic' on existing MQTT clients

Matches settings' new multi-topic-per-client support: the create form's
Topic scope field is now a textarea (one pattern per line), the list shows
all of a client's scopes, and a '+ Topic' button lets an admin grant an
existing client more topic access without recreating it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-08-13 14:01:24 +00:00
parent 411911c4c8
commit 06690f0581

View file

@ -997,6 +997,19 @@ function MqttClientsTab() {
if (res.ok) load()
}
async function addTopic(c: MqttClient) {
const topic = prompt(`Add a topic scope to "${c.name}" (e.g. hvac/mhi/#) — existing scopes are kept:`)
if (!topic || !topic.trim()) return
const res = await fetch(`/settings/api/mqtt-clients/${c.id}`, {
method: 'PATCH', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ add_topic_scope: topic.trim() }),
})
const data = await res.json()
if (!res.ok) { alert(data.error || 'Failed to add topic'); return }
load()
}
function copyPassword() {
if (!created) return
navigator.clipboard.writeText(created.password)
@ -1040,9 +1053,12 @@ function MqttClientsTab() {
placeholder="e.g. Kitchen Flash Reader" style={inputStyle} />
</label>
<label style={{ display: 'flex', flexDirection: 'column', gap: '0.3rem' }}>
<span style={{ fontSize: '0.78rem', fontWeight: 600, color: 'var(--text-mid)' }}>Topic scope</span>
<input value={topicScope} onChange={e => setTopicScope(e.target.value)}
placeholder="e.g. utilities/kitchen-meter/# or shellies/#" style={{ ...inputStyle, fontFamily: 'monospace' }} />
<span style={{ fontSize: '0.78rem', fontWeight: 600, color: 'var(--text-mid)' }}>
Topic scope
<span style={{ fontWeight: 400, color: 'var(--text-mid)' }}> one per line if it needs more than one</span>
</span>
<textarea value={topicScope} onChange={e => setTopicScope(e.target.value)} rows={2}
placeholder={'e.g. shellies/#\nhvac/mhi/#'} style={{ ...inputStyle, fontFamily: 'monospace', resize: 'vertical' }} />
</label>
<div style={{ display: 'flex', gap: '1.5rem' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', fontSize: '0.85rem', color: 'var(--text-dark)' }}>
@ -1143,13 +1159,22 @@ function MqttClientsTab() {
)}
</div>
<div style={{ fontSize: '0.72rem', color: 'var(--text-mid)', fontFamily: 'monospace' }}>
{c.topic_scope}
{c.topic_scope.split('\n').join(', ')}
<span style={{ marginLeft: '0.6rem', fontFamily: 'sans-serif' }}>
{[c.can_publish && 'publish', c.can_subscribe && 'subscribe'].filter(Boolean).join(' + ')}
</span>
</div>
</div>
<div style={{ flexShrink: 0, display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
{c.active && (
<button onClick={() => addTopic(c)} title="Add another topic scope to this client" style={{
background: 'transparent', color: 'var(--text-mid)',
border: '1px solid var(--card-border)', borderRadius: '6px',
padding: '0.35rem 0.85rem', fontSize: '0.78rem', fontWeight: 600, cursor: 'pointer',
}}>
+ Topic
</button>
)}
{c.active ? (
<button onClick={() => revoke(c)} style={{
background: 'transparent', color: 'var(--text-mid)',