From ccd0011a7f1237d98dac585d6e2ae40ad85b9904 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Tue, 9 Sep 2025 21:32:04 +1000 Subject: [PATCH] feat: add performance warning for large datasets (>1000 tasks) - Add modal warning dialog with Lucide icons when queries return >1000 tasks - Users can choose to cancel or continue with full load - Prevents accidental UI freezing on very large datasets - Uses proper DOM creation (no innerHTML) and static imports - Shows informative fallback message when loading is cancelled - Applied to both list and kanban views Addresses performance concerns while giving users informed choice. --- src/bases/base-view-factory.ts | 45 ++++++++- src/bases/helpers.ts | 180 +++++++++++++++++++++++++++++++++ src/bases/kanban-view.ts | 45 ++++++++- 3 files changed, 268 insertions(+), 2 deletions(-) diff --git a/src/bases/base-view-factory.ts b/src/bases/base-view-factory.ts index 75f6e326..f435402e 100644 --- a/src/bases/base-view-factory.ts +++ b/src/bases/base-view-factory.ts @@ -5,9 +5,10 @@ * It handles formula computation, data transformation, and rendering of TaskNotes items in Bases views. */ import TaskNotesPlugin from '../main'; -import { BasesDataItem, identifyTaskNotesFromBasesData, getBasesVisibleProperties } from './helpers'; +import { BasesDataItem, identifyTaskNotesFromBasesData, getBasesVisibleProperties, showPerformanceWarning } from './helpers'; import { TaskInfo } from '../types'; import { createTaskCard, updateTaskCard } from '../ui/TaskCard'; +import { setIcon } from 'obsidian'; export interface BasesContainerLike { results?: Map; @@ -20,6 +21,7 @@ export interface ViewConfig { errorPrefix: string; } + export function buildTasknotesBaseViewFactory(plugin: TaskNotesPlugin, config: ViewConfig) { return function tasknotesBaseViewFactory(basesContainer: BasesContainerLike) { let currentRoot: HTMLElement | null = null; @@ -122,6 +124,47 @@ export function buildTasknotesBaseViewFactory(plugin: TaskNotesPlugin, config: V if (renderCancellation.cancelled) return; + // Performance warning for large datasets + if (taskNotes.length > 1000) { + const shouldContinue = await showPerformanceWarning(taskNotes.length); + if (!shouldContinue || renderCancellation.cancelled) { + // Show partial message + itemsContainer.innerHTML = ''; + const partialEl = document.createElement('div'); + partialEl.className = 'tn-bases-partial'; + partialEl.style.cssText = 'padding: 20px; text-align: center; color: var(--text-muted);'; + + // Create icon and title + const titleDiv = document.createElement('div'); + titleDiv.style.cssText = 'margin-bottom: 8px; display: flex; align-items: center; justify-content: center; gap: 8px;'; + + const iconEl = document.createElement('div'); + iconEl.style.cssText = 'color: var(--text-warning); display: flex; align-items: center;'; + setIcon(iconEl, 'alert-triangle'); + titleDiv.appendChild(iconEl); + + const titleText = document.createElement('span'); + titleText.textContent = 'Performance limit reached'; + titleDiv.appendChild(titleText); + partialEl.appendChild(titleDiv); + + // Create description + const description = document.createElement('div'); + description.style.cssText = 'font-size: 0.9em;'; + description.textContent = `Found ${taskNotes.length} tasks, but loading was cancelled to prevent performance issues.`; + partialEl.appendChild(description); + + // Create advice + const advice = document.createElement('div'); + advice.style.cssText = 'font-size: 0.9em; margin-top: 8px;'; + advice.textContent = 'Consider refining your query to show fewer results.'; + partialEl.appendChild(advice); + + itemsContainer.appendChild(partialEl); + return; + } + } + if (taskNotes.length === 0) { itemsContainer.innerHTML = ''; const emptyEl = document.createElement('div'); diff --git a/src/bases/helpers.ts b/src/bases/helpers.ts index 1fc1bafc..18864639 100644 --- a/src/bases/helpers.ts +++ b/src/bases/helpers.ts @@ -1,5 +1,6 @@ import TaskNotesPlugin from '../main'; import { TaskInfo } from '../types'; +import { setIcon } from 'obsidian'; export interface BasesDataItem { key?: string; @@ -314,6 +315,185 @@ async function renderAllTasksAsync( /** * Render a raw Bases data item for debugging/inspection */ +/** + * Show performance warning dialog for large datasets + */ +export async function showPerformanceWarning(taskCount: number): Promise { + return new Promise((resolve) => { + // Create modal overlay + const overlay = document.createElement('div'); + overlay.className = 'tn-performance-warning-overlay'; + overlay.style.cssText = ` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + backdrop-filter: blur(2px); + `; + + // Create modal content + const modal = document.createElement('div'); + modal.className = 'tn-performance-warning-modal'; + modal.style.cssText = ` + background: var(--background-primary); + border: 1px solid var(--background-modifier-border); + border-radius: 8px; + padding: 24px; + max-width: 480px; + margin: 20px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + `; + + // Warning icon and title + const header = document.createElement('div'); + header.style.cssText = 'display: flex; align-items: center; margin-bottom: 16px;'; + + // Create warning icon using Lucide + const iconEl = document.createElement('div'); + iconEl.style.cssText = 'margin-right: 12px; color: var(--text-warning); display: flex; align-items: center;'; + setIcon(iconEl, 'alert-triangle'); + header.appendChild(iconEl); + + // Create title text + const titleEl = document.createElement('div'); + titleEl.style.cssText = 'font-size: 18px; font-weight: 600; color: var(--text-normal);'; + titleEl.textContent = 'Performance Warning'; + header.appendChild(titleEl); + modal.appendChild(header); + + // Warning message + const message = document.createElement('div'); + message.style.cssText = 'margin-bottom: 20px; line-height: 1.5; color: var(--text-normal);'; + + // Create intro paragraph + const intro = document.createElement('p'); + intro.style.cssText = 'margin: 0 0 12px 0;'; + intro.textContent = 'This query returned '; + + const taskCountStrong = document.createElement('strong'); + taskCountStrong.textContent = taskCount.toLocaleString() + ' tasks'; + intro.appendChild(taskCountStrong); + + const introEnd = document.createTextNode(', which may cause performance issues:'); + intro.appendChild(introEnd); + message.appendChild(intro); + + // Create issues list + const issuesList = document.createElement('ul'); + issuesList.style.cssText = 'margin: 0 0 12px 20px; padding: 0;'; + + const issues = ['Slow rendering and UI freezing', 'Decreased responsiveness']; + issues.forEach(issue => { + const li = document.createElement('li'); + li.textContent = issue; + issuesList.appendChild(li); + }); + message.appendChild(issuesList); + + // Create advice paragraph + const advice = document.createElement('p'); + advice.style.cssText = 'margin: 0; font-weight: 500;'; + advice.textContent = 'Consider refining your filter query to show fewer results, or continue if you need to see all tasks.'; + message.appendChild(advice); + + modal.appendChild(message); + + // Buttons + const buttonContainer = document.createElement('div'); + buttonContainer.style.cssText = 'display: flex; gap: 12px; justify-content: flex-end;'; + + const cancelButton = document.createElement('button'); + cancelButton.textContent = 'Cancel'; + cancelButton.style.cssText = ` + padding: 8px 16px; + border: 1px solid var(--background-modifier-border); + background: var(--background-secondary); + color: var(--text-normal); + border-radius: 4px; + cursor: pointer; + font-size: 14px; + `; + + const continueButton = document.createElement('button'); + continueButton.textContent = `Load All ${taskCount.toLocaleString()} Tasks`; + continueButton.style.cssText = ` + padding: 8px 16px; + border: none; + background: var(--interactive-accent); + color: var(--text-on-accent); + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + `; + + // Button hover effects + cancelButton.addEventListener('mouseenter', () => { + cancelButton.style.background = 'var(--background-modifier-hover)'; + }); + cancelButton.addEventListener('mouseleave', () => { + cancelButton.style.background = 'var(--background-secondary)'; + }); + + continueButton.addEventListener('mouseenter', () => { + continueButton.style.opacity = '0.9'; + }); + continueButton.addEventListener('mouseleave', () => { + continueButton.style.opacity = '1'; + }); + + // Event handlers + const cleanup = () => { + overlay.remove(); + }; + + cancelButton.addEventListener('click', () => { + cleanup(); + resolve(false); + }); + + continueButton.addEventListener('click', () => { + cleanup(); + resolve(true); + }); + + // Close on overlay click + overlay.addEventListener('click', (e) => { + if (e.target === overlay) { + cleanup(); + resolve(false); + } + }); + + // Close on Escape key + const handleKeydown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + cleanup(); + document.removeEventListener('keydown', handleKeydown); + resolve(false); + } + }; + document.addEventListener('keydown', handleKeydown); + + buttonContainer.appendChild(cancelButton); + buttonContainer.appendChild(continueButton); + modal.appendChild(buttonContainer); + overlay.appendChild(modal); + + // Add to document + document.body.appendChild(overlay); + + // Focus the continue button by default + setTimeout(() => continueButton.focus(), 100); + }); +} + export function renderBasesDataItem(container: HTMLElement, item: BasesDataItem, index: number): void { const itemEl = document.createElement('div'); itemEl.className = 'tn-bases-data-item'; diff --git a/src/bases/kanban-view.ts b/src/bases/kanban-view.ts index e24b45b0..54dde74b 100644 --- a/src/bases/kanban-view.ts +++ b/src/bases/kanban-view.ts @@ -1,12 +1,14 @@ import TaskNotesPlugin from '../main'; -import { BasesDataItem, identifyTaskNotesFromBasesData } from './helpers'; +import { BasesDataItem, identifyTaskNotesFromBasesData, showPerformanceWarning } from './helpers'; import { TaskInfo } from '../types'; import { getBasesGroupByConfig, BasesGroupByConfig } from './group-by'; import { getGroupNameComparator } from './group-ordering'; import { getBasesSortComparator } from './sorting'; import { createTaskCard, updateTaskCard } from '../ui/TaskCard'; +import { setIcon } from 'obsidian'; // Removed unused imports - using local BasesContainerLike interface for compatibility + // Use the same interface as base-view-factory for compatibility interface BasesContainerLike { results?: Map; @@ -181,6 +183,47 @@ export function buildTasknotesKanbanViewFactory(plugin: TaskNotesPlugin) { const dataItems = extractDataItems(); const taskNotes = await identifyTaskNotesFromBasesData(dataItems, plugin); + // Performance warning for large datasets + if (taskNotes.length > 1000) { + const shouldContinue = await showPerformanceWarning(taskNotes.length); + if (!shouldContinue) { + // Show partial message + board.innerHTML = ''; + const partialEl = document.createElement('div'); + partialEl.className = 'tn-bases-partial'; + partialEl.style.cssText = 'padding: 20px; text-align: center; color: var(--text-muted);'; + + // Create icon and title + const titleDiv = document.createElement('div'); + titleDiv.style.cssText = 'margin-bottom: 8px; display: flex; align-items: center; justify-content: center; gap: 8px;'; + + const iconEl = document.createElement('div'); + iconEl.style.cssText = 'color: var(--text-warning); display: flex; align-items: center;'; + setIcon(iconEl, 'alert-triangle'); + titleDiv.appendChild(iconEl); + + const titleText = document.createElement('span'); + titleText.textContent = 'Performance limit reached'; + titleDiv.appendChild(titleText); + partialEl.appendChild(titleDiv); + + // Create description + const description = document.createElement('div'); + description.style.cssText = 'font-size: 0.9em;'; + description.textContent = `Found ${taskNotes.length} tasks, but loading was cancelled to prevent performance issues.`; + partialEl.appendChild(description); + + // Create advice + const advice = document.createElement('div'); + advice.style.cssText = 'font-size: 0.9em; margin-top: 8px;'; + advice.textContent = 'Consider refining your query to show fewer results.'; + partialEl.appendChild(advice); + + board.appendChild(partialEl); + return; + } + } + if (taskNotes.length === 0) { board.innerHTML = ''; const empty = document.createElement('div');