feat(14-08): add Analysis Settings section to SettingsCloudTab
- Add cache limit number input with tier cap validation and usage display - Add segmented control for simplified vs detailed analysis progress (D-06) - Add segmented control for pause-batch vs continue-item failure behavior (D-11) - Load cache settings on mount via loadCacheSettings; surface API errors - Keep existing connection health/reconnect/disconnect UX intact - All 471 frontend tests pass; no regressions
This commit is contained in:
@@ -321,6 +321,145 @@
|
||||
@close="closeModal"
|
||||
@connected="handleConnected"
|
||||
/>
|
||||
|
||||
<!-- Phase 14: Analysis and cache settings -->
|
||||
<section
|
||||
data-test="cache-settings-section"
|
||||
class="bg-white border border-gray-200 rounded-xl p-6 mt-4"
|
||||
>
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-1">Analysis Settings</h3>
|
||||
|
||||
<!-- Settings error banner -->
|
||||
<div
|
||||
v-if="store.cacheSettingsError"
|
||||
data-test="cache-settings-error"
|
||||
class="mb-4 p-3 rounded-lg bg-red-50 border border-red-200 text-sm text-red-700"
|
||||
>
|
||||
{{ store.cacheSettingsError }}
|
||||
</div>
|
||||
|
||||
<div class="space-y-5">
|
||||
|
||||
<!-- Cache limit -->
|
||||
<div>
|
||||
<label
|
||||
for="cache-limit-input"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Cache limit (MB)
|
||||
</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<input
|
||||
id="cache-limit-input"
|
||||
data-test="cache-limit-input"
|
||||
type="number"
|
||||
:min="1"
|
||||
:max="cacheLimitMaxMB"
|
||||
:value="cacheLimitMB"
|
||||
@change="handleCacheLimitChange"
|
||||
class="w-28 border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
aria-label="Cache limit in megabytes"
|
||||
/>
|
||||
<span class="text-xs text-gray-500">
|
||||
Max: {{ cacheLimitMaxMB }} MB
|
||||
</span>
|
||||
<span
|
||||
v-if="cacheLimitError"
|
||||
data-test="cache-limit-error"
|
||||
class="text-xs text-red-600"
|
||||
>
|
||||
{{ cacheLimitError }}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
v-if="store.cacheUsage"
|
||||
data-test="cache-usage"
|
||||
class="text-xs text-gray-500 mt-1"
|
||||
>
|
||||
Currently using {{ cacheUsageMB }} MB of {{ cacheLimitMB }} MB
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Analysis progress detail -->
|
||||
<div>
|
||||
<span class="block text-sm font-medium text-gray-700 mb-1" id="progress-detail-label">
|
||||
Analysis progress detail
|
||||
</span>
|
||||
<div
|
||||
class="flex rounded-lg border border-gray-200 overflow-hidden w-fit"
|
||||
role="group"
|
||||
aria-labelledby="progress-detail-label"
|
||||
>
|
||||
<button
|
||||
data-test="progress-simplified"
|
||||
:class="[
|
||||
'px-4 py-1.5 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500',
|
||||
!store.detailedAnalysisProgress
|
||||
? 'bg-indigo-600 text-white font-medium'
|
||||
: 'bg-white text-gray-700 hover:bg-gray-50',
|
||||
]"
|
||||
:aria-pressed="!store.detailedAnalysisProgress"
|
||||
@click="handleProgressDetail(false)"
|
||||
>
|
||||
Simplified
|
||||
</button>
|
||||
<button
|
||||
data-test="progress-detailed"
|
||||
:class="[
|
||||
'px-4 py-1.5 text-sm border-l border-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500',
|
||||
store.detailedAnalysisProgress
|
||||
? 'bg-indigo-600 text-white font-medium'
|
||||
: 'bg-white text-gray-700 hover:bg-gray-50',
|
||||
]"
|
||||
:aria-pressed="store.detailedAnalysisProgress"
|
||||
@click="handleProgressDetail(true)"
|
||||
>
|
||||
Detailed
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Failure behavior -->
|
||||
<div>
|
||||
<span class="block text-sm font-medium text-gray-700 mb-1" id="failure-behavior-label">
|
||||
On analysis failure
|
||||
</span>
|
||||
<div
|
||||
class="flex rounded-lg border border-gray-200 overflow-hidden w-fit"
|
||||
role="group"
|
||||
aria-labelledby="failure-behavior-label"
|
||||
>
|
||||
<button
|
||||
data-test="failure-pause-batch"
|
||||
:class="[
|
||||
'px-4 py-1.5 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500',
|
||||
store.failureBehavior === 'pause_batch'
|
||||
? 'bg-indigo-600 text-white font-medium'
|
||||
: 'bg-white text-gray-700 hover:bg-gray-50',
|
||||
]"
|
||||
:aria-pressed="store.failureBehavior === 'pause_batch'"
|
||||
@click="handleFailureBehavior('pause_batch')"
|
||||
>
|
||||
Pause batch
|
||||
</button>
|
||||
<button
|
||||
data-test="failure-continue-item"
|
||||
:class="[
|
||||
'px-4 py-1.5 text-sm border-l border-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500',
|
||||
store.failureBehavior === 'continue_item'
|
||||
? 'bg-indigo-600 text-white font-medium'
|
||||
: 'bg-white text-gray-700 hover:bg-gray-50',
|
||||
]"
|
||||
:aria-pressed="store.failureBehavior === 'continue_item'"
|
||||
@click="handleFailureBehavior('continue_item')"
|
||||
>
|
||||
Skip and continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -334,6 +473,31 @@ import CloudCredentialModal from '../cloud/CloudCredentialModal.vue'
|
||||
|
||||
const store = useCloudConnectionsStore()
|
||||
|
||||
// ── Phase 14: Cache settings computed helpers ─────────────────────────────────
|
||||
|
||||
/** Default tier cap: 2 GB in MB if server hasn't responded yet. */
|
||||
const DEFAULT_TIER_CAP_BYTES = 2 * 1024 * 1024 * 1024
|
||||
|
||||
/** Tier cap in MB (from server or fallback). */
|
||||
const cacheLimitMaxMB = computed(() => {
|
||||
const cap = store.tierCapBytes ?? DEFAULT_TIER_CAP_BYTES
|
||||
return Math.floor(cap / (1024 * 1024))
|
||||
})
|
||||
|
||||
/** Current cache limit in MB. */
|
||||
const cacheLimitMB = computed(() => {
|
||||
return Math.round(store.cacheLimit / (1024 * 1024))
|
||||
})
|
||||
|
||||
/** Current cache usage in MB (from server). */
|
||||
const cacheUsageMB = computed(() => {
|
||||
if (!store.cacheUsage) return 0
|
||||
return Math.round(store.cacheUsage.total_bytes / (1024 * 1024))
|
||||
})
|
||||
|
||||
/** Local validation error for cache limit input. */
|
||||
const cacheLimitError = ref('')
|
||||
|
||||
const PROVIDERS = [
|
||||
{ key: 'google_drive', label: 'Google Drive', iconColor: 'text-blue-500' },
|
||||
{ key: 'onedrive', label: 'OneDrive', iconColor: 'text-sky-500' },
|
||||
@@ -358,6 +522,10 @@ const testingId = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
store.fetchConnections()
|
||||
// Phase 14: hydrate server-authoritative cache settings on mount
|
||||
if (typeof store.loadCacheSettings === 'function') {
|
||||
store.loadCacheSettings()
|
||||
}
|
||||
})
|
||||
|
||||
/** All connections for a given provider key (may be multiple). */
|
||||
@@ -530,4 +698,69 @@ async function submitRename(id) {
|
||||
renameError.value = e.message || 'Failed to rename connection'
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 14: Cache settings handlers ────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Handle cache limit input change — validate against tier cap, then persist.
|
||||
*/
|
||||
async function handleCacheLimitChange(event) {
|
||||
cacheLimitError.value = ''
|
||||
const rawValue = parseInt(event.target.value, 10)
|
||||
if (isNaN(rawValue) || rawValue < 1) {
|
||||
cacheLimitError.value = 'Enter a value of at least 1 MB'
|
||||
return
|
||||
}
|
||||
if (rawValue > cacheLimitMaxMB.value) {
|
||||
cacheLimitError.value = `Maximum cache limit is ${cacheLimitMaxMB.value} MB`
|
||||
return
|
||||
}
|
||||
const bytes = rawValue * 1024 * 1024
|
||||
try {
|
||||
if (typeof store.updateCacheSettings === 'function') {
|
||||
await store.updateCacheSettings({ cloud_cache_limit_bytes: bytes })
|
||||
}
|
||||
} catch {
|
||||
// store.cacheSettingsError set by the action; input reverts on next render
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle simplified vs detailed analysis progress mode.
|
||||
*/
|
||||
async function handleProgressDetail(enabled) {
|
||||
if (typeof store.setDetailedAnalysisProgress === 'function') {
|
||||
store.setDetailedAnalysisProgress(enabled)
|
||||
}
|
||||
try {
|
||||
if (typeof store.updateCacheSettings === 'function') {
|
||||
await store.updateCacheSettings({ analysis_progress_detail: enabled })
|
||||
}
|
||||
} catch {
|
||||
// Revert local state on API failure
|
||||
if (typeof store.setDetailedAnalysisProgress === 'function') {
|
||||
store.setDetailedAnalysisProgress(!enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change failure behavior for analysis jobs.
|
||||
*/
|
||||
async function handleFailureBehavior(behavior) {
|
||||
const previous = store.failureBehavior
|
||||
if (typeof store.setFailureBehavior === 'function') {
|
||||
store.setFailureBehavior(behavior)
|
||||
}
|
||||
try {
|
||||
if (typeof store.updateCacheSettings === 'function') {
|
||||
await store.updateCacheSettings({ analysis_failure_behavior: behavior })
|
||||
}
|
||||
} catch {
|
||||
// Revert on failure
|
||||
if (typeof store.setFailureBehavior === 'function') {
|
||||
store.setFailureBehavior(previous)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user