diff --git a/structures-frontend-next/src/components/SavedWidgetItem.vue b/structures-frontend-next/src/components/SavedWidgetItem.vue index 2077fb15..97ed5c23 100644 --- a/structures-frontend-next/src/components/SavedWidgetItem.vue +++ b/structures-frontend-next/src/components/SavedWidgetItem.vue @@ -14,14 +14,9 @@ const previewLoaded = ref(false) const getWidgetTitle = (): string => { try { - const config = JSON.parse(props.widget.config || '{}') - const aiTitle = config.aiTitle - if (aiTitle && aiTitle !== 'Test' && !aiTitle.includes('AI-generated') && aiTitle.length > 2) { - return aiTitle - } - const widgetName = props.widget.name || '' - if (widgetName && !widgetName.includes('AI-generated')) { - return widgetName + const name = props.widget.dataInsightsComponent?.name || '' + if (name && name.length > 0) { + return name } return 'Data Insight' } catch { @@ -31,14 +26,9 @@ const getWidgetTitle = (): string => { const getWidgetSubtitle = (): string => { try { - const config = JSON.parse(props.widget.config || '{}') - const aiSubtitle = config.aiSubtitle - if (aiSubtitle && !aiSubtitle.includes('AI-generated') && !aiSubtitle.includes('widget for:') && aiSubtitle.length > 5) { - return aiSubtitle - } - const widgetDesc = props.widget.description || '' - if (widgetDesc && !widgetDesc.includes('AI-generated')) { - return widgetDesc + const description = props.widget.dataInsightsComponent?.description || '' + if (description && description.length > 0) { + return description } return 'Data visualization' } catch { @@ -46,23 +36,82 @@ const getWidgetSubtitle = (): string => { } } -const executeWidgetHTML = () => { - const config = JSON.parse(props.widget.config || '{}') - const htmlContent = config.originalRawHtml || props.widget.src +const loadEchartsIfNeeded = (): Promise => { + return new Promise((resolve) => { + if ((window as any).echarts) { + resolve() + return + } + + if ((window as any).echartsLoading) { + const checkInterval = setInterval(() => { + if ((window as any).echarts) { + clearInterval(checkInterval) + resolve() + } + }, 100) + return + } + + ;(window as any).echartsLoading = true + const script = document.createElement('script') + script.src = 'https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js' + script.onload = () => { + ;(window as any).echartsLoading = false + resolve() + } + script.onerror = () => { + ;(window as any).echartsLoading = false + resolve() + } + document.head.appendChild(script) + }) +} + +const executeWidgetHTML = async () => { + const htmlContent = props.widget.dataInsightsComponent?.rawHtml || '' if (!htmlContent) return + if (htmlContent.includes('echarts')) { + await loadEchartsIfNeeded() + } + + executeWidgetElement(htmlContent) +} + +const executeWidgetElement = (htmlContent: string) => { try { const elementNameMatch = htmlContent.match(/customElements\.define\(['"`]([^'"`]+)['"`]/) - const elementName = elementNameMatch ? elementNameMatch[1] : null + let elementName = elementNameMatch ? elementNameMatch[1] : null if (!elementName) return + // If element already exists, create unique name for this widget instance if (customElements.get(elementName)) { + console.log('âš ī¸ Element already registered, using existing:', elementName) + // Check if it's the same class or different + const existingElement = customElements.get(elementName) + const newClassMatch = htmlContent.match(/class\s+(\w+)\s+extends\s+HTMLElement/) + const newClassName = newClassMatch ? newClassMatch[1] : null + + // If different implementation, create unique element name + if (newClassName && existingElement && existingElement.name !== newClassName) { + const uniqueName = `${elementName}-${props.widget.id?.substring(0, 8)}` + console.log('🔄 Creating unique element name:', uniqueName) + const modifiedHtml = htmlContent.replace( + `customElements.define('${elementName}'`, + `customElements.define('${uniqueName}'` + ) + eval(modifiedHtml) + elementName = uniqueName + } + createWidgetElement(elementName) return } + console.log('🔧 Registering new element:', elementName) eval(htmlContent) setTimeout(() => { @@ -78,51 +127,67 @@ const executeWidgetHTML = () => { const createWidgetElement = (elementName: string) => { const previewContainer = document.querySelector(`[data-widget-id="${props.widget.id}"] .widget-preview-content`) + console.log('🎨 Creating element for:', props.widget.dataInsightsComponent?.name) + console.log('đŸ“Ļ Container found:', !!previewContainer) + console.log('đŸˇī¸ Element name:', elementName) + console.log('✅ Custom element registered?', !!customElements.get(elementName)) + if (previewContainer) { const element = document.createElement(elementName) + console.log('🔧 Element created:', element) previewContainer.innerHTML = '' previewContainer.appendChild(element) + console.log('✅ Element appended to container') setTimeout(() => { + console.log('⏰ Checking shadow root after 300ms...') + console.log('🔍 Shadow root exists?', !!element.shadowRoot) + if (element.shadowRoot) { + console.log('✅ Shadow root confirmed!') const style = document.createElement('style') - style.textContent = ` - h1, h2, h3, h4, h5, h6, p, span, label, title, desc, .title, .description { display: none !important; } - :host { - padding: 0 !important; - margin: 0 !important; - border: none !important; - box-shadow: none !important; - background: transparent !important; - width: 100% !important; - height: 100% !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - } - canvas, svg { - max-width: 100% !important; - max-height: 100% !important; - width: auto !important; - height: auto !important; - display: block !important; - margin: auto !important; - } - ` element.shadowRoot.appendChild(style) + console.log('🎨 Styles appended') setTimeout(() => { + console.log('⏰ Setting preview loaded...') previewLoaded.value = true - }, 500) + console.log('✅ Preview loaded for:', props.widget.dataInsightsComponent?.name) + + if (element.shadowRoot) { + const chartContainer = element.shadowRoot.querySelector('.chart-container, [id="chart"]') + console.log('📊 Chart container in shadow:', !!chartContainer) + + const canvas = element.shadowRoot.querySelector('canvas') + const svg = element.shadowRoot.querySelector('svg') + console.log('🎨 Canvas found:', !!canvas) + console.log('🎨 SVG found:', !!svg) + + const chartElement = element.shadowRoot.querySelector('canvas, svg, [id="chart"]') as any + if (chartElement && chartElement.__ec_inner__) { + setTimeout(() => { + console.log('📊 Resizing echarts...') + chartElement.__ec_inner__.resize() + console.log('✅ ECharts resized') + }, 500) + } else { + console.log('â„šī¸ No echarts instance found (might be other chart library)') + } + } + }, 1000) + } else { + console.error('❌ No shadow root found!') } - }, 100) + }, 300) + } else { + console.error('❌ Preview container not found for:', props.widget.id) } } onMounted(() => { setTimeout(() => { executeWidgetHTML() - }, 100) + }, 200) }) @@ -150,8 +215,8 @@ onMounted(() => {
-
{{ getWidgetTitle() }}
-
{{ getWidgetSubtitle() }}
+
{{ getWidgetTitle() }}
+
{{ getWidgetSubtitle() }}
@@ -399,6 +589,7 @@ const addWidgetToGrid = (widget: DataInsightsWidget, x?: number, y?: number, w?: removeBtn.addEventListener('click', async () => { gridStack.value?.removeWidget(el) addedWidgetIds.delete(widgetInstanceId) + renderedWidgetIds.delete(widgetInstanceId) // Clean up render tracking setTimeout(() => { const remainingWidgets = document.querySelectorAll('.grid-stack-item') @@ -406,141 +597,167 @@ const addWidgetToGrid = (widget: DataInsightsWidget, x?: number, y?: number, w?: }, 100) }) } - const getWidgetSize = (widget: DataInsightsWidget) => { - const config = JSON.parse(widget.config || '{}') - const query = config.query || '' - - if (query.toLowerCase().includes('pie') || query.toLowerCase().includes('donut')) { - return { w: 3, h: 3 } - } else if (query.toLowerCase().includes('bar') || query.toLowerCase().includes('column')) { - return { w: 4, h: 3 } - } else if (query.toLowerCase().includes('line') || query.toLowerCase().includes('trend')) { - return { w: 5, h: 3 } - } else if (query.toLowerCase().includes('scatter') || query.toLowerCase().includes('heatmap')) { - return { w: 4, h: 4 } - } else { - return { w: 4, h: 3 } - } - } - - const defaultSize = getWidgetSize(widget) + const defaultSize = { w: 4, h: 4 } const options: any = { w: w || defaultSize.w, h: h || defaultSize.h, minW: 1, maxW: 12, minH: 1, - maxH: 12 + maxH: 12, + autoPosition: x === undefined && y === undefined + } + + if (x !== undefined) { + options.x = x + } + if (y !== undefined) { + options.y = y } - if (x !== undefined) options.x = Math.max(0, Math.min(x, 11)) - if (y !== undefined) options.y = Math.max(0, y) gridStack.value.makeWidget(el, options) + + setTimeout(() => { + const actualX = el.getAttribute('gs-x') + const actualY = el.getAttribute('gs-y') + + if (x !== undefined && actualX !== x.toString()) { + el.setAttribute('gs-x', x.toString()) + el.style.left = `${x * (100 / 12)}%` + } + if (y !== undefined && actualY !== y.toString()) { + el.setAttribute('gs-y', y.toString()) + const cellHeight = 71 + 5 + el.style.top = `${y * cellHeight}px` + } + }, 10) addedWidgetIds.add(widgetInstanceId) hasWidgets.value = true + // Function to render widget chart (used by both initial load and resize) + const renderWidgetChart = (targetEl: HTMLElement) => { + const widgetBody = targetEl.querySelector('.widget-body') + if (!widgetBody) return + + const widgetId = targetEl.getAttribute('data-widget-id') + const instanceId = targetEl.getAttribute('data-instance-id') + if (!widgetId || !instanceId) return + + if (renderedWidgetIds.has(instanceId)) { + return + } + + const widget = savedWidgets.value.find(w => w.id === widgetId) + if (!widget) return + + const htmlContent = widget.dataInsightsComponent?.rawHtml || '' + if (!htmlContent || !htmlContent.includes('customElements.define')) return + + const elementNameMatch = htmlContent.match(/customElements\.define\(['"`]([^'"`]+)['"`]/) + const storedElementName = targetEl.getAttribute('data-element-name') + const elementName = storedElementName || (elementNameMatch ? elementNameMatch[1] : null) + + if (!elementName || !customElements.get(elementName)) return + + widgetBody.innerHTML = '' + const element = document.createElement(elementName) + widgetBody.appendChild(element) + + renderedWidgetIds.add(instanceId) + + setTimeout(() => { + if (element.shadowRoot) { + const style = document.createElement('style') + style.textContent = ` + h1, h2, h3, h4, h5, h6, p, span:not([class*="apex"]), label, title, desc, + .title, .description, .container h3, .container p { + display: none !important; + visibility: hidden !important; + height: 0 !important; + } + :host { + padding: 0 !important; + margin: 0 !important; + border: none !important; + box-shadow: none !important; + background: transparent !important; + } + .container { + border: none !important; + padding: 0 !important; + background: transparent !important; + } + canvas, svg, .chart-container { + display: block !important; + max-width: 100% !important; + max-height: 100% !important; + } + ` + element.shadowRoot.appendChild(style) + + setTimeout(() => { + if (element.shadowRoot) { + const canvas = element.shadowRoot.querySelector('canvas') as any + if (canvas && canvas.__ec_inner__) { + canvas.__ec_inner__.resize() + } + } + + // Hide loading overlay + const loadingOverlay = targetEl.querySelector('.widget-loading-overlay') as HTMLElement + if (loadingOverlay) { + loadingOverlay.style.opacity = '0' + setTimeout(() => { + loadingOverlay.style.display = 'none' + }, 300) + } + }, 100) + } + }, 100) + } + const resizeObserver = new ResizeObserver((entries) => { entries.forEach((entry) => { - const widgetBody = entry.target.querySelector('.widget-body') - if (widgetBody) { + const targetEl = entry.target as HTMLElement + const instanceId = targetEl.getAttribute('data-instance-id') + + if (!instanceId) return + + if (!renderedWidgetIds.has(instanceId)) { setTimeout(() => { - const widgetId = entry.target.getAttribute('data-widget-id') - if (widgetId) { - const widget = savedWidgets.value.find(w => w.id === widgetId) - if (widget) { - - const config = JSON.parse(widget.config || '{}') - const htmlContent = config.originalRawHtml || widget.src - - if (htmlContent && htmlContent.includes('customElements.define')) { - const elementNameMatch = htmlContent.match(/customElements\.define\(['"`]([^'"`]+)['"`]/) - const elementName = elementNameMatch ? elementNameMatch[1] : null - - if (elementName && customElements.get(elementName)) { - widgetBody.innerHTML = '' - const element = document.createElement(elementName) - widgetBody.appendChild(element) - - setTimeout(() => { - if (element.shadowRoot) { - const style = document.createElement('style') - style.textContent = 'h1, h2, h3, h4, h5, h6, p, span, label, title, desc, .title, .description :host { padding: 0 !important; margin: 0 !important; border: none !important; box-shadow: none !important; } canvas, svg, [class*="chart"], [class*="apex"], [class*="visualization"], div[class*="chart"], div[class*="apex"], .chart-container { display: block !important; }' - element.shadowRoot.appendChild(style) - setTimeout(() => { - const chartElement = element.shadowRoot.querySelector('canvas, svg, [class*="chart"], [class*="apex"]') as any - if (chartElement) { - if (chartElement.__apexcharts) { - chartElement.__apexcharts.resize() - } - if (typeof chartElement.resize === 'function') { - chartElement.resize() - } - } - }, 100) - - } - }, 100) - } + renderWidgetChart(targetEl) + }, 100) + } else { + setTimeout(() => { + const widgetBody = targetEl.querySelector('.widget-body') + if (widgetBody) { + const element = widgetBody.querySelector('*') + if (element && (element as any).shadowRoot) { + const canvas = (element as any).shadowRoot.querySelector('canvas') as any + if (canvas && canvas.__ec_inner__) { + canvas.__ec_inner__.resize() } } } - }, 100) + }, 50) } }) }) resizeObserver.observe(el) + // Trigger initial render + setTimeout(() => { + renderWidgetChart(el) + }, 500) + setTimeout(() => { addResizeIcons() - }, 300) + }, 800) setTimeout(() => { - const elementNameMatch = htmlContent?.match(/customElements\.define\(['"`]([^'"`]+)['"`]/) - if (elementNameMatch) { - const elementName = elementNameMatch[1] - - if (customElements.get(elementName)) { - const widgetBody = el.querySelector('.widget-body') - if (widgetBody) { - const element = document.createElement(elementName) - widgetBody.innerHTML = '' - widgetBody.appendChild(element) - - setTimeout(() => { - if (element.shadowRoot) { - const h3Element = element.shadowRoot.querySelector('h3') - const pElement = element.shadowRoot.querySelector('p') - - let extractedTitle = '' - let extractedSubtitle = '' - - if (h3Element) { - extractedTitle = h3Element.textContent?.trim() || '' - } - if (pElement) { - extractedSubtitle = pElement.textContent?.trim() || '' - } - - if (extractedTitle || extractedSubtitle) { - try { - updateWidgetConfig(widget, { - aiTitle: extractedTitle, - aiSubtitle: extractedSubtitle - }) - } catch (error) { - } - } - - const style = document.createElement('style') - style.textContent = 'h1, h2, h3, h4, h5, h6, p, span, label, title, desc, .title, .description :host { padding: 0 !important; margin: 0 !important; border: none !important; box-shadow: none !important; } canvas, svg, [class*="chart"], [class*="apex"], [class*="visualization"], div[class*="chart"], div[class*="apex"], .chart-container { display: block !important; }' - element.shadowRoot.appendChild(style) - } - }, 100) - } - } - } - }, 1000) + addResizeIcons() + }, 1500) } const createDashboard = async () => { @@ -550,17 +767,35 @@ const createDashboard = async () => { } try { + const gridItems = document.querySelectorAll('.grid-stack .grid-stack-item') + const widgetInstances: any[] = [] + + gridItems.forEach(item => { + const el = item as HTMLElement + const instanceId = el.getAttribute('data-instance-id') + const widgetId = el.getAttribute('data-widget-id') + const x = parseInt(el.getAttribute('gs-x') || '0') + const y = parseInt(el.getAttribute('gs-y') || '0') + const w = parseInt(el.getAttribute('gs-w') || '4') + const h = parseInt(el.getAttribute('gs-h') || '4') + + if (widgetId && instanceId) { + widgetInstances.push({ instanceId, widgetId, x, y, w, h }) + } + }) + + const layoutJson = JSON.stringify({ widgets: widgetInstances }) + const newDashboard: any = { id: null, name: dashboardTitle.value.trim(), description: 'Dashboard', applicationId: props.applicationId, - layout: JSON.stringify({ widgets: [] }), + layout: layoutJson, created: new Date(), updated: new Date() } - const savedDashboard = await dashboardService.save(newDashboard) toast.add({ severity: 'success', summary: 'Created', detail: `Dashboard "${savedDashboard.name}" created successfully`, life: 3000 }) @@ -584,6 +819,7 @@ const updateDashboard = async () => { try { const gridItems = document.querySelectorAll('.grid-stack .grid-stack-item') const widgetInstances: any[] = [] + gridItems.forEach(item => { const el = item as HTMLElement const instanceId = el.getAttribute('data-instance-id') @@ -591,15 +827,17 @@ const updateDashboard = async () => { const x = parseInt(el.getAttribute('gs-x') || '0') const y = parseInt(el.getAttribute('gs-y') || '0') const w = parseInt(el.getAttribute('gs-w') || '4') - const h = parseInt(el.getAttribute('gs-h') || '3') + const h = parseInt(el.getAttribute('gs-h') || '4') if (widgetId && instanceId) { widgetInstances.push({ instanceId, widgetId, x, y, w, h }) } }) + const layoutJson = JSON.stringify({ widgets: widgetInstances }) + dashboard.value.name = dashboardTitle.value - dashboard.value.layout = JSON.stringify({ widgets: widgetInstances }) + dashboard.value.layout = layoutJson dashboard.value.updated = new Date() await dashboardService.save(dashboard.value) @@ -632,6 +870,15 @@ const updateDateRange = () => { } } +const clearDateRange = () => { + dateRange.value = { startDate: null, endDate: null } + updateDateRange() + + setTimeout(() => { + window.dispatchEvent(new Event('resize')) + }, 100) +} + const toggleDateRangePicker = () => { showDateRangePicker.value = !showDateRangePicker.value } @@ -640,55 +887,108 @@ const enterEditMode = () => { router.push(`/application/${props.applicationId}/dashboards/${props.dashboardId}/edit`) } -const exitEditMode = () => { - router.push(`/application/${props.applicationId}/dashboards/${props.dashboardId}`) -} - const updateGridMode = () => { if (!gridStack.value) return if (!isEditMode.value) { + gridStack.value.float(false) gridStack.value.setStatic(true) gridStack.value.enableMove(false) gridStack.value.enableResize(false) } else { gridStack.value.setStatic(false) + gridStack.value.float(false) gridStack.value.enableMove(true) gridStack.value.enableResize(true) } } -const updateWidgetConfig = (widget: DataInsightsWidget, updates: { aiTitle?: string, aiSubtitle?: string }) => { - try { - const config = JSON.parse(widget.config || '{}') - let configUpdated = false - - if (updates.aiTitle && !config.aiTitle) { - config.aiTitle = updates.aiTitle - configUpdated = true - } - if (updates.aiSubtitle && !config.aiSubtitle) { - config.aiSubtitle = updates.aiSubtitle - configUpdated = true - } - - if (configUpdated) { - widget.config = JSON.stringify(config) - return true - } - return false - } catch (error) { - return false - } -} - - watch(isEditMode, () => { if (gridStack.value) { updateGridMode() if (isEditMode.value) { + gridStack.value.enableMove(true) + gridStack.value.enableResize(true) setTimeout(() => setupDragDrop(), 100) + } else { + gridStack.value.enableMove(false) + gridStack.value.enableResize(false) + + setTimeout(() => { + const gridItems = document.querySelectorAll('.grid-stack-item') + gridItems.forEach((item) => { + const element = item as HTMLElement + const instanceId = element.getAttribute('data-instance-id') + const widgetId = element.getAttribute('data-widget-id') + + if (instanceId && widgetId) { + const widget = savedWidgets.value.find(w => w.id === widgetId) + if (!widget) return + + const widgetBody = element.querySelector('.widget-body') + if (!widgetBody) return + + renderedWidgetIds.delete(instanceId) + + const htmlContent = widget.dataInsightsComponent?.rawHtml || '' + if (!htmlContent || !htmlContent.includes('customElements.define')) return + + const elementNameMatch = htmlContent.match(/customElements\.define\(['"`]([^'"`]+)['"`]/) + const storedElementName = element.getAttribute('data-element-name') + const elementName = storedElementName || (elementNameMatch ? elementNameMatch[1] : null) + + if (!elementName || !customElements.get(elementName)) return + + widgetBody.innerHTML = '' + const chartElement = document.createElement(elementName) + widgetBody.appendChild(chartElement) + + renderedWidgetIds.add(instanceId) + + setTimeout(() => { + if (chartElement.shadowRoot) { + const style = document.createElement('style') + style.textContent = ` + h1, h2, h3, h4, h5, h6, p, span:not([class*="apex"]), label, title, desc, + .title, .description, .container h3, .container p { + display: none !important; + visibility: hidden !important; + height: 0 !important; + } + :host { + padding: 0 !important; + margin: 0 !important; + border: none !important; + box-shadow: none !important; + background: transparent !important; + } + .container { + border: none !important; + padding: 0 !important; + background: transparent !important; + } + canvas, svg, .chart-container { + display: block !important; + max-width: 100% !important; + max-height: 100% !important; + } + ` + chartElement.shadowRoot.appendChild(style) + + setTimeout(() => { + if (chartElement.shadowRoot) { + const canvas = chartElement.shadowRoot.querySelector('canvas') as any + if (canvas && canvas.__ec_inner__) { + canvas.__ec_inner__.resize() + } + } + }, 100) + } + }, 100) + } + }) + }, 500) } } }) @@ -704,7 +1004,9 @@ onMounted(async () => { initGrid() if (isEditMode.value) { - setTimeout(() => setupDragDrop(), 100) + setTimeout(() => { + setupDragDrop() + }, 500) } }, 200) }) @@ -716,16 +1018,13 @@ onMounted(async () => {
-
-