ui: add community overlay slots
This commit is contained in:
@@ -45,7 +45,7 @@ Verification:
|
||||
- UI build and Community-targeted smoke/e2e pass.
|
||||
|
||||
Progress:
|
||||
- done:
|
||||
- done:
|
||||
- `deploy/community/*` already acts as the canonical Community delivery contour
|
||||
- Community capability model is already constrained to:
|
||||
- `REST`
|
||||
@@ -170,4 +170,5 @@ Progress:
|
||||
- Phase 3 dependency slice: added public billing seam in `crank-core` — `BillingHook`, `BillingGate`, `BillingError`, `SharedBillingHook`, and `NoopBillingHook` — so `cloud` can layer billing policy without private-only contracts in the base repo
|
||||
- Phase 3 runtime slice: `RuntimeRequestContext` now carries optional metering context (`workspace_id`, `agent_id`, `source`), `RuntimeExecutorBuilder` accepts a `MeteringSink`, and `RuntimeExecutor` emits `MeteringEvent` on invocation completion while Community apps keep using the default `NoopMeteringSink`
|
||||
- Phase 3 dependency slice: added `CacheBackendFactory` in `crank-runtime` with `BuiltinCacheBackendFactory`; unlike the original draft, the seam lives in runtime rather than core to avoid a `crank-core -> crank-runtime` dependency cycle around `RuntimeCacheStores`
|
||||
- Phase 5 / task `5.1`: added Community UI overlay foundation — `slot-registry.js`, `overlay-loader.js`, optional `CRANK_UI_OVERLAY_DIR` copy path in the UI build, settings slot mounts, and wizard protocol-card slot mounts; Community build stays noop without any private overlay package
|
||||
- backward-compatible alias `CommunityMachineCredentialVerifier` сохранен, поведение Community не изменено
|
||||
|
||||
@@ -277,6 +277,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-slot="settings.sso_panel"></div>
|
||||
<div data-slot="settings.totp_panel"></div>
|
||||
<div data-slot="settings.audit_panel"></div>
|
||||
<div data-slot="settings.billing_panel"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-card" style="margin-bottom: 20px;">
|
||||
|
||||
@@ -34,6 +34,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-slot="wizard.protocol_cards.graphql"></div>
|
||||
<div data-slot="wizard.protocol_cards.grpc"></div>
|
||||
<div data-slot="wizard.protocol_cards.soap"></div>
|
||||
<div data-slot="wizard.protocol_cards.websocket"></div>
|
||||
|
||||
</div><!-- /protocol-grid -->
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
(function() {
|
||||
var manifestPromise = null;
|
||||
var loadedScripts = Object.create(null);
|
||||
|
||||
function overlayBasePath() {
|
||||
return (window.APP_BASE || '/') + 'overlay/';
|
||||
}
|
||||
|
||||
function fetchManifest() {
|
||||
return fetch(overlayBasePath() + 'manifest.json', { cache: 'no-store' })
|
||||
.then(function(response) {
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error('overlay manifest request failed');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.catch(function() {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
function loadScript(path) {
|
||||
if (loadedScripts[path]) {
|
||||
return loadedScripts[path];
|
||||
}
|
||||
loadedScripts[path] = new Promise(function(resolve, reject) {
|
||||
var script = document.createElement('script');
|
||||
script.src = overlayBasePath() + path.replace(/^\/+/, '');
|
||||
script.async = false;
|
||||
script.onload = function() { resolve(); };
|
||||
script.onerror = function() { reject(new Error('overlay script load failed')); };
|
||||
document.head.appendChild(script);
|
||||
}).catch(function() {
|
||||
return null;
|
||||
});
|
||||
return loadedScripts[path];
|
||||
}
|
||||
|
||||
function load() {
|
||||
if (manifestPromise) {
|
||||
return manifestPromise;
|
||||
}
|
||||
manifestPromise = fetchManifest().then(function(manifest) {
|
||||
if (!(manifest && manifest.slots && window.CrankUiSlots)) {
|
||||
return null;
|
||||
}
|
||||
var tasks = Object.entries(manifest.slots)
|
||||
.filter(function(entry) {
|
||||
return window.CrankUiSlots.isAllowed(entry[0]) && typeof entry[1] === 'string';
|
||||
})
|
||||
.map(function(entry) {
|
||||
return loadScript(entry[1]);
|
||||
});
|
||||
return Promise.all(tasks).then(function() {
|
||||
return manifest;
|
||||
});
|
||||
});
|
||||
return manifestPromise;
|
||||
}
|
||||
|
||||
async function render(root, context) {
|
||||
await load();
|
||||
if (window.CrankUiSlots) {
|
||||
window.CrankUiSlots.renderAll(root, context);
|
||||
}
|
||||
}
|
||||
|
||||
window.CrankOverlay = {
|
||||
load: load,
|
||||
render: render,
|
||||
};
|
||||
}());
|
||||
+22
-4
@@ -193,6 +193,19 @@ async function loadCapabilities() {
|
||||
}
|
||||
}
|
||||
|
||||
async function renderOverlaySlots() {
|
||||
if (!window.CrankOverlay || typeof window.CrankOverlay.render !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
|
||||
await window.CrankOverlay.render(document, {
|
||||
workspace: window.getCurrentWorkspace ? window.getCurrentWorkspace() : null,
|
||||
capabilities: settingsCapabilities,
|
||||
locale: localStorage.getItem('crank_lang') || 'en',
|
||||
});
|
||||
}
|
||||
|
||||
function bindProfileSave() {
|
||||
var button = document.getElementById('settings-profile-save-btn');
|
||||
if (!button || button.dataset.bound === 'true') {
|
||||
@@ -418,12 +431,17 @@ async function loadWorkspaceSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
async function initSettingsPage() {
|
||||
injectLanguageSwitcher();
|
||||
bindSectionNavigation();
|
||||
loadProfile();
|
||||
loadCapabilities();
|
||||
await loadProfile();
|
||||
await loadCapabilities();
|
||||
bindProfileSave();
|
||||
bindPasswordSave();
|
||||
loadWorkspaceSettings();
|
||||
await loadWorkspaceSettings();
|
||||
await renderOverlaySlots();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
void initSettingsPage();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
(function() {
|
||||
var allowedSlots = {
|
||||
'settings.sso_panel': true,
|
||||
'settings.totp_panel': true,
|
||||
'settings.audit_panel': true,
|
||||
'settings.billing_panel': true,
|
||||
'wizard.protocol_cards.graphql': true,
|
||||
'wizard.protocol_cards.grpc': true,
|
||||
'wizard.protocol_cards.soap': true,
|
||||
'wizard.protocol_cards.websocket': true,
|
||||
};
|
||||
|
||||
var handlers = Object.create(null);
|
||||
|
||||
function noop() {}
|
||||
|
||||
function isAllowed(slotId) {
|
||||
return !!allowedSlots[slotId];
|
||||
}
|
||||
|
||||
function register(slotId, render) {
|
||||
if (!isAllowed(slotId) || typeof render !== 'function') {
|
||||
return false;
|
||||
}
|
||||
handlers[slotId] = render;
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderInto(target, slotId, context) {
|
||||
if (!(target && slotId && isAllowed(slotId))) {
|
||||
return;
|
||||
}
|
||||
var handler = handlers[slotId] || noop;
|
||||
handler(target, context || {});
|
||||
}
|
||||
|
||||
function renderAll(root, context) {
|
||||
var scope = root || document;
|
||||
scope.querySelectorAll('[data-slot]').forEach(function(target) {
|
||||
renderInto(target, target.getAttribute('data-slot'), context);
|
||||
});
|
||||
}
|
||||
|
||||
function knownSlots() {
|
||||
return Object.keys(allowedSlots);
|
||||
}
|
||||
|
||||
window.CrankUiSlots = {
|
||||
isAllowed: isAllowed,
|
||||
knownSlots: knownSlots,
|
||||
register: register,
|
||||
renderAll: renderAll,
|
||||
renderInto: renderInto,
|
||||
};
|
||||
}());
|
||||
@@ -86,9 +86,16 @@ async function initWizardPage() {
|
||||
await loadProtocolCapabilities();
|
||||
|
||||
await loadWizardPanels([1, 2, 3, 4, 5]);
|
||||
applyEditionProtocolVisibility(editionCapabilities);
|
||||
if (window.CrankOverlay && typeof window.CrankOverlay.render === 'function') {
|
||||
await window.CrankOverlay.render(document, {
|
||||
workspace: workspace,
|
||||
capabilities: editionCapabilities,
|
||||
locale: localStorage.getItem('crank_lang') || 'en',
|
||||
});
|
||||
}
|
||||
await loadWizardAuthResources();
|
||||
bindProtocolCards();
|
||||
applyEditionProtocolVisibility(editionCapabilities);
|
||||
bindWizardLiveActions();
|
||||
updateUpstreamAuthUi();
|
||||
renderEditionCapabilityHints(editionCapabilities);
|
||||
|
||||
@@ -16,6 +16,8 @@ const BUNDLES = {
|
||||
files: [
|
||||
'js/config.js',
|
||||
'js/i18n.js',
|
||||
'js/slot-registry.js',
|
||||
'js/overlay-loader.js',
|
||||
'js/dom.js',
|
||||
'js/diagnostics.js',
|
||||
'js/workspace.js',
|
||||
@@ -176,6 +178,7 @@ function rewriteHtml(relativePath, replacements, prefix) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
var overlayDirectory = process.env.CRANK_UI_OVERLAY_DIR || '';
|
||||
removeDirectory(DIST_DIR);
|
||||
ensureDirectory(BUNDLE_DIR);
|
||||
|
||||
@@ -199,6 +202,9 @@ async function main() {
|
||||
});
|
||||
copyDirectory(path.join(ROOT_DIR, 'icons'), path.join(DIST_DIR, 'icons'));
|
||||
copyFile(resolveBrandImage(), path.join(DIST_DIR, 'Crank.png'));
|
||||
if (overlayDirectory && fs.existsSync(overlayDirectory)) {
|
||||
copyDirectory(overlayDirectory, path.join(DIST_DIR, 'overlay'));
|
||||
}
|
||||
|
||||
rewriteHtml('index.html', manifest, '');
|
||||
rewriteHtml('html/login.html', manifest, '..');
|
||||
|
||||
Reference in New Issue
Block a user