feat: add browser receiver pairing settings
This commit is contained in:
parent
d6b8ab17f1
commit
396b77cf26
|
|
@ -52,6 +52,13 @@
|
||||||
'.browser-inbox-meta-value{color:var(--vt-color-text-secondary,#b7c0d4);min-width:0;overflow-wrap:anywhere}',
|
'.browser-inbox-meta-value{color:var(--vt-color-text-secondary,#b7c0d4);min-width:0;overflow-wrap:anywhere}',
|
||||||
'.browser-inbox-text{border:1px solid var(--vt-color-border,#202b46);background:var(--vt-color-surface,#15152c);border-radius:var(--vt-radius-lg,8px);padding:.75rem;font-size:.85rem;line-height:1.5;color:var(--vt-color-text-primary,#f4f7fb);white-space:pre-wrap;overflow-wrap:anywhere}',
|
'.browser-inbox-text{border:1px solid var(--vt-color-border,#202b46);background:var(--vt-color-surface,#15152c);border-radius:var(--vt-radius-lg,8px);padding:.75rem;font-size:.85rem;line-height:1.5;color:var(--vt-color-text-primary,#f4f7fb);white-space:pre-wrap;overflow-wrap:anywhere}',
|
||||||
'.browser-inbox-detail-actions{display:flex;gap:.5rem;flex-wrap:wrap}',
|
'.browser-inbox-detail-actions{display:flex;gap:.5rem;flex-wrap:wrap}',
|
||||||
|
'.browser-inbox-settings{display:grid;gap:.85rem;padding:1rem;max-width:560px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;color:var(--vt-color-text-primary,#f4f7fb)}',
|
||||||
|
'.browser-inbox-settings-field{display:grid;gap:.3rem}',
|
||||||
|
'.browser-inbox-settings-label{font-size:.78rem;color:var(--vt-color-text-muted,#7f8aa3)}',
|
||||||
|
'.browser-inbox-settings-input{width:100%;box-sizing:border-box;padding:.48rem .58rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:var(--vt-color-surface,#15152c);color:var(--vt-color-text-primary,#f4f7fb);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.78rem}',
|
||||||
|
'.browser-inbox-settings-actions{display:flex;gap:.5rem;flex-wrap:wrap}',
|
||||||
|
'.browser-inbox-settings-status{min-height:1.15rem;font-size:.76rem;color:var(--vt-color-text-muted,#7f8aa3)}',
|
||||||
|
'.browser-inbox-settings-status.error{color:#ffc6ce}',
|
||||||
'@media(max-width:760px){.browser-inbox-body{grid-template-columns:1fr}.browser-inbox-list{border-right:0;border-bottom:1px solid #16213e;max-height:45vh}.browser-inbox-meta{grid-template-columns:1fr}}'
|
'@media(max-width:760px){.browser-inbox-body{grid-template-columns:1fr}.browser-inbox-list{border-right:0;border-bottom:1px solid #16213e;max-height:45vh}.browser-inbox-meta{grid-template-columns:1fr}}'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
|
@ -766,9 +773,145 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var BrowserInboxSettings = {
|
||||||
|
mount: function (containerEl, props, api) {
|
||||||
|
injectStyles();
|
||||||
|
containerEl.innerHTML = '';
|
||||||
|
containerEl.className = 'browser-inbox-settings';
|
||||||
|
|
||||||
|
var receiverURLInput = el('input', {
|
||||||
|
className: 'browser-inbox-settings-input',
|
||||||
|
type: 'text',
|
||||||
|
readonly: 'readonly',
|
||||||
|
spellcheck: 'false',
|
||||||
|
autocomplete: 'off',
|
||||||
|
'data-browser-inbox-pairing-url': ''
|
||||||
|
});
|
||||||
|
var receiverTokenInput = el('input', {
|
||||||
|
className: 'browser-inbox-settings-input',
|
||||||
|
type: 'text',
|
||||||
|
readonly: 'readonly',
|
||||||
|
spellcheck: 'false',
|
||||||
|
autocomplete: 'off',
|
||||||
|
'data-browser-inbox-pairing-token': ''
|
||||||
|
});
|
||||||
|
var statusEl = el('div', { className: 'browser-inbox-settings-status' });
|
||||||
|
var copyURLButton = el('button', {
|
||||||
|
className: 'browser-inbox-btn',
|
||||||
|
type: 'button',
|
||||||
|
'data-browser-inbox-settings-action': 'copy-url',
|
||||||
|
textContent: 'Copy URL'
|
||||||
|
});
|
||||||
|
var copyTokenButton = el('button', {
|
||||||
|
className: 'browser-inbox-btn',
|
||||||
|
type: 'button',
|
||||||
|
'data-browser-inbox-settings-action': 'copy-token',
|
||||||
|
textContent: 'Copy Token'
|
||||||
|
});
|
||||||
|
var rotateTokenButton = el('button', {
|
||||||
|
className: 'browser-inbox-btn danger',
|
||||||
|
type: 'button',
|
||||||
|
'data-browser-inbox-settings-action': 'rotate-token',
|
||||||
|
textContent: 'Rotate Token'
|
||||||
|
});
|
||||||
|
|
||||||
|
containerEl.appendChild(el('div', { className: 'browser-inbox-settings-field' }, [
|
||||||
|
el('label', { className: 'browser-inbox-settings-label', textContent: 'Receiver URL' }),
|
||||||
|
receiverURLInput
|
||||||
|
]));
|
||||||
|
containerEl.appendChild(el('div', { className: 'browser-inbox-settings-field' }, [
|
||||||
|
el('label', { className: 'browser-inbox-settings-label', textContent: 'Pairing Token' }),
|
||||||
|
receiverTokenInput
|
||||||
|
]));
|
||||||
|
containerEl.appendChild(el('div', { className: 'browser-inbox-settings-actions' }, [
|
||||||
|
copyURLButton,
|
||||||
|
copyTokenButton,
|
||||||
|
rotateTokenButton
|
||||||
|
]));
|
||||||
|
containerEl.appendChild(statusEl);
|
||||||
|
|
||||||
|
function setStatus(message, isError) {
|
||||||
|
statusEl.textContent = message || '';
|
||||||
|
statusEl.className = 'browser-inbox-settings-status' + (isError ? ' error' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBusy(busy) {
|
||||||
|
copyURLButton.disabled = busy;
|
||||||
|
copyTokenButton.disabled = busy;
|
||||||
|
rotateTokenButton.disabled = busy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pairingAPI() {
|
||||||
|
if (!api || !api.browserReceiver) throw new Error('Browser receiver API is unavailable');
|
||||||
|
return api.browserReceiver;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPairing(pairing) {
|
||||||
|
receiverURLInput.value = text(pairing && pairing.receiverUrl).trim();
|
||||||
|
receiverTokenInput.value = text(pairing && pairing.receiverToken).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPairing() {
|
||||||
|
setBusy(true);
|
||||||
|
setStatus('Loading...', false);
|
||||||
|
return Promise.resolve().then(function () {
|
||||||
|
return pairingAPI().pairing();
|
||||||
|
}).then(function (pairing) {
|
||||||
|
applyPairing(pairing);
|
||||||
|
setStatus('', false);
|
||||||
|
}).catch(function (err) {
|
||||||
|
setStatus(text(err && err.message ? err.message : err), true);
|
||||||
|
}).then(function () {
|
||||||
|
setBusy(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyValue(value, label) {
|
||||||
|
if (!value) return;
|
||||||
|
if (typeof navigator === 'undefined' || !navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') {
|
||||||
|
setStatus('Clipboard unavailable', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.clipboard.writeText(value).then(function () {
|
||||||
|
setStatus(label + ' copied', false);
|
||||||
|
}).catch(function (err) {
|
||||||
|
setStatus(text(err && err.message ? err.message : err), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
copyURLButton.addEventListener('click', function () {
|
||||||
|
copyValue(receiverURLInput.value, 'URL');
|
||||||
|
});
|
||||||
|
copyTokenButton.addEventListener('click', function () {
|
||||||
|
copyValue(receiverTokenInput.value, 'Token');
|
||||||
|
});
|
||||||
|
rotateTokenButton.addEventListener('click', function () {
|
||||||
|
if (typeof window.confirm === 'function' && !window.confirm('Rotate pairing token?')) return;
|
||||||
|
setBusy(true);
|
||||||
|
setStatus('Rotating...', false);
|
||||||
|
Promise.resolve().then(function () {
|
||||||
|
return pairingAPI().rotateToken();
|
||||||
|
}).then(function (pairing) {
|
||||||
|
applyPairing(pairing);
|
||||||
|
setStatus('Token rotated', false);
|
||||||
|
}).catch(function (err) {
|
||||||
|
setStatus(text(err && err.message ? err.message : err), true);
|
||||||
|
}).then(function () {
|
||||||
|
setBusy(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
loadPairing();
|
||||||
|
},
|
||||||
|
unmount: function (containerEl) {
|
||||||
|
if (containerEl) containerEl.innerHTML = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
window.VerstakPluginRegister(PLUGIN_ID, {
|
window.VerstakPluginRegister(PLUGIN_ID, {
|
||||||
components: {
|
components: {
|
||||||
BrowserInboxView: BrowserInboxView
|
BrowserInboxView: BrowserInboxView,
|
||||||
|
BrowserInboxSettings: BrowserInboxSettings
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"events.subscribe",
|
"events.subscribe",
|
||||||
"events.publish",
|
"events.publish",
|
||||||
|
"browser.receiver.manage",
|
||||||
"files.write",
|
"files.write",
|
||||||
"storage.namespace",
|
"storage.namespace",
|
||||||
"ui.register"
|
"ui.register"
|
||||||
|
|
@ -47,6 +48,13 @@
|
||||||
"icon": "inbox",
|
"icon": "inbox",
|
||||||
"component": "BrowserInboxView"
|
"component": "BrowserInboxView"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"settingsPanels": [
|
||||||
|
{
|
||||||
|
"id": "verstak.browser-inbox.settings",
|
||||||
|
"title": "Browser Inbox",
|
||||||
|
"component": "BrowserInboxSettings"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ function makeDocument() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadComponent(document) {
|
function loadComponents(document) {
|
||||||
const registry = {};
|
const registry = {};
|
||||||
const sandbox = {
|
const sandbox = {
|
||||||
console,
|
console,
|
||||||
|
|
@ -125,7 +125,13 @@ function loadComponent(document) {
|
||||||
sandbox.window.window = sandbox.window;
|
sandbox.window.window = sandbox.window;
|
||||||
sandbox.window.document = document;
|
sandbox.window.document = document;
|
||||||
vm.runInNewContext(source, sandbox, { filename: sourcePath });
|
vm.runInNewContext(source, sandbox, { filename: sourcePath });
|
||||||
const component = registry['verstak.browser-inbox'] && registry['verstak.browser-inbox'].BrowserInboxView;
|
const components = registry['verstak.browser-inbox'];
|
||||||
|
if (!components) throw new Error('Browser Inbox components were not registered');
|
||||||
|
return components;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadComponent(document) {
|
||||||
|
const component = loadComponents(document).BrowserInboxView;
|
||||||
if (!component) throw new Error('BrowserInboxView was not registered');
|
if (!component) throw new Error('BrowserInboxView was not registered');
|
||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
@ -137,6 +143,10 @@ function makeApi(initialSettings = {}) {
|
||||||
const fileWrites = [];
|
const fileWrites = [];
|
||||||
const fileByteWrites = [];
|
const fileByteWrites = [];
|
||||||
const publishedEvents = [];
|
const publishedEvents = [];
|
||||||
|
const receiverPairing = {
|
||||||
|
receiverUrl: 'http://127.0.0.1:47731/api/browser-inbox/v1/captures',
|
||||||
|
receiverToken: 'initial-browser-token',
|
||||||
|
};
|
||||||
let nextWriteError = null;
|
let nextWriteError = null;
|
||||||
return {
|
return {
|
||||||
settings,
|
settings,
|
||||||
|
|
@ -185,6 +195,13 @@ function makeApi(initialSettings = {}) {
|
||||||
fileByteWrites.push({ relativePath, dataBase64, options });
|
fileByteWrites.push({ relativePath, dataBase64, options });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
browserReceiver: {
|
||||||
|
pairing: async () => ({ ...receiverPairing }),
|
||||||
|
rotateToken: async () => {
|
||||||
|
receiverPairing.receiverToken = 'rotated-browser-token';
|
||||||
|
return { ...receiverPairing };
|
||||||
|
},
|
||||||
|
},
|
||||||
getStoredCaptures(key = 'captures') {
|
getStoredCaptures(key = 'captures') {
|
||||||
return settings[key] || [];
|
return settings[key] || [];
|
||||||
},
|
},
|
||||||
|
|
@ -203,8 +220,37 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
|
||||||
return { component, container, document };
|
return { component, container, document };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function mountSettingsWithApi(api, document = makeDocument()) {
|
||||||
|
const component = loadComponents(document).BrowserInboxSettings;
|
||||||
|
const container = new FakeNode('div');
|
||||||
|
component.mount(container, {}, api);
|
||||||
|
await flush();
|
||||||
|
return { component, container, document };
|
||||||
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
const settingsComponents = loadComponents(makeDocument());
|
||||||
|
if (!settingsComponents.BrowserInboxSettings) throw new Error('BrowserInboxSettings was not registered');
|
||||||
|
|
||||||
const api = makeApi();
|
const api = makeApi();
|
||||||
|
const settingsView = await mountSettingsWithApi(makeApi());
|
||||||
|
const receiverURLInput = walk(settingsView.container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-pairing-url') === '');
|
||||||
|
const receiverTokenInput = walk(settingsView.container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-pairing-token') === '');
|
||||||
|
if (!receiverURLInput || receiverURLInput.value !== 'http://127.0.0.1:47731/api/browser-inbox/v1/captures') {
|
||||||
|
throw new Error('Browser Inbox settings did not render receiver URL');
|
||||||
|
}
|
||||||
|
if (!receiverTokenInput || receiverTokenInput.value !== 'initial-browser-token') {
|
||||||
|
throw new Error('Browser Inbox settings did not render pairing token');
|
||||||
|
}
|
||||||
|
const rotateTokenButton = walk(settingsView.container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-settings-action') === 'rotate-token');
|
||||||
|
if (!rotateTokenButton) throw new Error('Browser Inbox settings rotate token action missing');
|
||||||
|
rotateTokenButton.click();
|
||||||
|
await flush();
|
||||||
|
if (receiverTokenInput.value !== 'rotated-browser-token') {
|
||||||
|
throw new Error('Browser Inbox settings did not update token after rotation');
|
||||||
|
}
|
||||||
|
settingsView.component.unmount && settingsView.component.unmount(settingsView.container);
|
||||||
|
|
||||||
const { component, container } = await mountWithApi(api);
|
const { component, container } = await mountWithApi(api);
|
||||||
|
|
||||||
for (const name of ['browser.capture.page', 'browser.capture.selection', 'browser.capture.link', 'browser.capture.file']) {
|
for (const name of ['browser.capture.page', 'browser.capture.selection', 'browser.capture.link', 'browser.capture.file']) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue