diff --git a/index.html b/index.html index 820c8dd6..b0c20b30 100644 --- a/index.html +++ b/index.html @@ -726,6 +726,22 @@ margin-bottom: 10px; } + .policy-function-group { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; + } + + .policy-function-label { + font-size: 12px; + font-weight: 500; + color: var(--text-secondary); + min-width: 80px; + text-transform: lowercase; + font-family: monospace; + } + .command-section { margin-top: 20px; } @@ -920,6 +936,14 @@ grid-template-columns: 1fr; } + .policy-function-group { + flex-wrap: wrap; + } + + .policy-function-label { + min-width: 60px; + } + .command-grid { grid-template-columns: 1fr; } @@ -1060,67 +1084,13 @@
-
-
Create Category
-
- Controls how new files and directories are created. Default: pfrd (percentage free random distribution) -
- -
- -
-
Search Category
-
- Controls how files are searched and accessed. Default: ff (first found) -
- -
- -
-
Action Category
-
- Controls how file attributes are modified. Default: epall (existing path all) -
- -
- -
-
Readdir Policy
-
- Controls how directory contents are read. Default: seq (sequential) -
- -
- Policy Categories:
- • Create: mkdir, create, mknod, symlink
- • Search: access, getattr, getxattr, ioctl, listxattr, open, readlink
- • Action: chmod, chown, link, removexattr, rename, rmdir, setxattr, truncate, unlink, utimens
- • Readdir: Directory listing operations + Individual Function Policies:
+ • Each function can have its own policy configured independently
+ • Click "Reset to Defaults" to restore default policies for all functions
+ • Policies are saved immediately when the "Save Policies" button is clicked
@@ -1930,46 +1900,151 @@ } // Policy management + const policyCategories = { + create: { + title: 'Create Category', + description: 'Controls how new files and directories are created', + functions: ['mkdir', 'create', 'mknod', 'symlink'], + default: 'pfrd', + options: [ + { value: 'pfrd', label: 'pfrd - Percentage Free Random Distribution' }, + { value: 'mfs', label: 'mfs - Most Free Space' }, + { value: 'lfs', label: 'lfs - Least Free Space' }, + { value: 'lus', label: 'lus - Least Used Space' }, + { value: 'rand', label: 'rand - Random' }, + { value: 'ff', label: 'ff - First Found' }, + { value: 'epmfs', label: 'epmfs - Existing Path Most Free Space' }, + { value: 'eprand', label: 'eprand - Existing Path Random' } + ] + }, + search: { + title: 'Search Category', + description: 'Controls how files are searched and accessed', + functions: ['access', 'getattr', 'getxattr', 'listxattr', 'open', 'readlink'], + default: 'ff', + options: [ + { value: 'ff', label: 'ff - First Found' }, + { value: 'newest', label: 'newest - Newest File' }, + { value: 'rand', label: 'rand - Random' } + ] + }, + action: { + title: 'Action Category', + description: 'Controls how file attributes are modified', + functions: ['chmod', 'chown', 'link', 'removexattr', 'rename', 'rmdir', 'setxattr', 'truncate', 'unlink', 'utimens'], + default: 'epall', + options: [ + { value: 'epall', label: 'epall - Existing Path All' }, + { value: 'all', label: 'all - All Branches' }, + { value: 'epff', label: 'epff - Existing Path First Found' }, + { value: 'ff', label: 'ff - First Found' } + ] + }, + readdir: { + title: 'Readdir Category', + description: 'Controls how directory contents are read', + functions: ['readdir'], + default: 'seq', + options: [ + { value: 'seq', label: 'seq - Sequential' }, + { value: 'cosr', label: 'cosr - Concurrent Open Sequential Read' }, + { value: 'cor', label: 'cor - Concurrent Open and Read' } + ] + } + }; + async function loadPolicies(mount) { - // Load current policy values from runtime configuration + const policyGrid = document.getElementById('policy-grid'); + policyGrid.innerHTML = '
Loading policies...'; + try { - const policyKeys = ['category.create', 'category.search', 'category.action', 'func.readdir']; + const allConfig = await API.getConfig(mount); const policyValues = {}; - for (const key of policyKeys) { - try { - const value = await API.getXattr(mount, key); - policyValues[key] = value; - } catch (e) { - console.warn(`Failed to load policy ${key}:`, e); - policyValues[key] = getDefaultPolicy(key); + for (const [category, config] of Object.entries(policyCategories)) { + for (const func of config.functions) { + const key = `func.${func}`; + policyValues[key] = allConfig[key] || config.default; } } - // Update UI with loaded values - Object.entries(policyValues).forEach(([key, value]) => { - const selectId = `policy-${key.split('.').pop()}`; - const select = document.getElementById(selectId); - if (select) { - select.value = value; - } - }); - AppState.policies = policyValues; + renderPolicyCards(); } catch (error) { console.error('Failed to load policies:', error); - showToast('Failed to load policies', 'error'); + const errorMsg = error.message || 'Unknown error'; + policyGrid.innerHTML = `
Failed to load policies: ${errorMsg}
`; + showToast(`Failed to load policies: ${errorMsg}`, 'error', 6000); } } - function getDefaultPolicy(key) { - const defaults = { - 'category.create': 'pfrd', - 'category.search': 'ff', - 'category.action': 'epall', - 'func.readdir': 'seq' - }; - return defaults[key] || ''; + function renderPolicyCards() { + const policyGrid = document.getElementById('policy-grid'); + policyGrid.innerHTML = ''; + + for (const [category, config] of Object.entries(policyCategories)) { + const card = document.createElement('div'); + card.className = 'policy-card'; + + const title = document.createElement('div'); + title.className = 'policy-title'; + title.textContent = config.title; + card.appendChild(title); + + const desc = document.createElement('div'); + desc.className = 'policy-description'; + desc.textContent = config.description; + card.appendChild(desc); + + for (const func of config.functions) { + const funcGroup = document.createElement('div'); + funcGroup.className = 'policy-function-group'; + + const funcLabel = document.createElement('label'); + funcLabel.className = 'policy-function-label'; + funcLabel.textContent = func; + funcGroup.appendChild(funcLabel); + + const select = document.createElement('select'); + select.className = 'policy-select'; + select.id = `policy-func-${func}`; + select.setAttribute('data-func', func); + + config.options.forEach(opt => { + const option = document.createElement('option'); + option.value = opt.value; + option.textContent = opt.label; + select.appendChild(option); + }); + + const key = `func.${func}`; + select.value = AppState.policies[key] || config.default; + + select.addEventListener('change', async () => { + if (!AppState.currentMount) { + showToast('No mount selected', 'error'); + select.value = AppState.policies[key] || config.default; + return; + } + + try { + await API.setXattr(AppState.currentMount, key, select.value); + AppState.policies[key] = select.value; + showToast(`${func} policy updated to ${select.value}`, 'success', 2000); + } catch (error) { + console.error(`Failed to update ${func} policy:`, error); + const errorMsg = error.message || 'Unknown error'; + showToast(`Failed to update ${func}: ${errorMsg}`, 'error', 6000); + select.value = AppState.policies[key] || config.default; + } + }); + + funcGroup.appendChild(select); + card.appendChild(funcGroup); + } + + policyGrid.appendChild(card); + } } async function savePolicies() { @@ -1978,18 +2053,13 @@ return; } - const policyMappings = { - 'policy-create': 'category.create', - 'policy-search': 'category.search', - 'policy-action': 'category.action', - 'policy-readdir': 'func.readdir' - }; - try { - for (const [selectId, policyKey] of Object.entries(policyMappings)) { - const select = document.getElementById(selectId); - if (select && select.value) { - await API.setXattr(AppState.currentMount, policyKey, select.value); + for (const [category, config] of Object.entries(policyCategories)) { + for (const func of config.functions) { + const select = document.getElementById(`policy-func-${func}`); + if (select) { + await API.setXattr(AppState.currentMount, `func.${func}`, select.value); + } } } @@ -1997,7 +2067,30 @@ await loadPolicies(AppState.currentMount); } catch (error) { console.error('Failed to save policies:', error); - showToast('Failed to save policy configuration', 'error'); + const errorMsg = error.message || 'Unknown error'; + showToast(`Failed to save policy configuration: ${errorMsg}`, 'error', 6000); + } + } + + async function resetPolicies() { + if (!AppState.currentMount) { + showToast('No mount selected', 'error'); + return; + } + + try { + for (const [category, config] of Object.entries(policyCategories)) { + for (const func of config.functions) { + await API.setXattr(AppState.currentMount, `func.${func}`, config.default); + } + } + + showToast('Policies reset to defaults', 'success'); + await loadPolicies(AppState.currentMount); + } catch (error) { + console.error('Failed to reset policies:', error); + const errorMsg = error.message || 'Unknown error'; + showToast(`Failed to reset policies: ${errorMsg}`, 'error', 6000); } } @@ -2214,7 +2307,7 @@ document.getElementById('save-branches-btn').addEventListener('click', saveBranches); document.getElementById('reset-branches-btn').addEventListener('click', () => loadBranches(AppState.currentMount)); document.getElementById('save-policies-btn').addEventListener('click', savePolicies); - document.getElementById('reset-policies-btn').addEventListener('click', () => loadPolicies(AppState.currentMount)); + document.getElementById('reset-policies-btn').addEventListener('click', resetPolicies); document.getElementById('export-config-btn').addEventListener('click', exportConfig); document.getElementById('import-config-btn').addEventListener('click', importConfig); document.getElementById('refresh-config-btn').addEventListener('click', () => loadConfig(AppState.currentMount));