diff --git a/src/app/demo/superstore/config.js b/src/app/demo/superstore/config.js index f0e33e4e..fea70d01 100644 --- a/src/app/demo/superstore/config.js +++ b/src/app/demo/superstore/config.js @@ -6,7 +6,8 @@ import { BrainCircuit, Languages, AppWindow, - Activity + Activity, + Sparkles } from "lucide-react"; export const settings = { @@ -79,5 +80,12 @@ export const settings = { min_role: 1, description: 'Pulse Discover dashboard from eacanada server' }, + { + name: 'Pulse Metrics', + icon: , + path: '/pulse-metrics', + min_role: 1, + description: 'AI-powered Pulse metrics with custom time filters' + }, ], } diff --git a/src/app/demo/superstore/pulse-discover/PulseDiscover.jsx b/src/app/demo/superstore/pulse-discover/PulseDiscover.jsx index 9e25f861..877c8b13 100644 --- a/src/app/demo/superstore/pulse-discover/PulseDiscover.jsx +++ b/src/app/demo/superstore/pulse-discover/PulseDiscover.jsx @@ -1,13 +1,24 @@ "use client"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui"; -import { Activity } from "lucide-react"; +import { Activity, MoreVertical, X } from "lucide-react"; import { TableauEmbedEACanada } from "@/components/TableauEmbed"; export const PulseDiscover = () => { const dashboardUrl = 'https://prod-ca-a.online.tableau.com/t/eacanada/views/PulseDiscoverPulseMCP/Dashboard1'; const containerRef = useRef(null); + const [selectedTimeRange, setSelectedTimeRange] = useState(''); + const [showFilterMenu, setShowFilterMenu] = useState(false); + + const timeRangeOptions = [ + { label: 'This Month', value: 'this_month' }, + { label: 'Last Month', value: 'last_month' }, + { label: 'Last 3 Months', value: 'last_3_months' }, + { label: 'This Year', value: 'this_year' }, + { label: 'Last Year', value: 'last_year' }, + { label: 'Last 3 Years', value: 'last_3_years' }, + ]; useEffect(() => { const logSize = () => { @@ -21,18 +32,370 @@ export const PulseDiscover = () => { return () => window.removeEventListener('resize', logSize); }, []); + // Apply filter when selectedTimeRange changes + useEffect(() => { + const applyFilter = async () => { + const fieldName = 'Adjusted Date'; // The field name in the datasource + + console.log('================================================='); + console.log('๐Ÿ” PULSE DISCOVER FILTER'); + console.log('================================================='); + console.log('Field Name:', fieldName); + console.log('Selected Time Range:', selectedTimeRange || 'NONE (Clearing filter)'); + console.log('================================================='); + + const applyFilterToViz = async () => { + let viz = document.getElementById('pulseDiscoverViz'); + + if (!viz) { + const tableauVizElements = document.querySelectorAll('tableau-viz'); + if (tableauVizElements.length > 0) { + viz = tableauVizElements[0]; + } + } + + if (!viz) { + setTimeout(() => applyFilterToViz(), 500); + return; + } + + try { + if (!viz.workbook) { + setTimeout(() => applyFilterToViz(), 500); + return; + } + } catch (error) { + setTimeout(() => applyFilterToViz(), 500); + return; + } + + try { + const activeSheet = viz.workbook.activeSheet; + + // If no time range selected, clear the filter + if (!selectedTimeRange) { + console.log('๐Ÿงน CLEARING FILTER FROM ALL WORKSHEETS'); + if (activeSheet.sheetType === 'dashboard') { + const worksheets = activeSheet.worksheets; + console.log(`Found ${worksheets.length} worksheets in dashboard`); + + // Try multiple possible date field names + const possibleDateFields = [ + 'Adjusted Date', + 'Date', + 'Order Date', + 'Created Date', + 'Timestamp', + 'Activity Date', + 'Login Date', + '[Adjusted Date]', + '[Date]', + '[Order Date]' + ]; + + for (const worksheet of worksheets) { + console.log(`\n ๐Ÿ”ง Clearing filters on worksheet: "${worksheet.name}"`); + let filterCleared = false; + + for (const dateField of possibleDateFields) { + try { + await worksheet.clearFilterAsync(dateField); + console.log(` โœ… Filter cleared for field: "${dateField}"`); + filterCleared = true; + } catch (error) { + // This field doesn't exist, continue + } + } + + if (!filterCleared) { + console.log(` โ„น๏ธ No date filters to clear on this worksheet`); + } + } + } else { + await activeSheet.clearFilterAsync(fieldName); + console.log(` โœ… Filter cleared from sheet: ${activeSheet.name}`); + } + + // Also clear Pulse metric filters + console.log('๐Ÿ’ซ CLEARING PULSE METRIC FILTERS:'); + try { + const pulseObjects = viz.workbook?.pulse || []; + + if (pulseObjects && pulseObjects.length > 0) { + console.log(`Found ${pulseObjects.length} Pulse metric(s) to clear`); + + for (const pulse of pulseObjects) { + try { + await pulse.clearFiltersAsync(); + console.log(` โœ… Filters cleared from Pulse metric`); + } catch (pulseError) { + console.error(` โŒ Error clearing Pulse metric filters:`, pulseError); + } + } + } else { + console.log(' โ„น๏ธ No separate Pulse objects found'); + console.log(' ๐Ÿ’ก Pulse metrics are likely embedded as worksheets and already cleared above'); + } + } catch (error) { + console.error(' โš ๏ธ Error accessing Pulse objects:', error); + } + + console.log('================================================='); + return; + } + + // Calculate date ranges based on selected option + const now = new Date(); + let startDate, endDate; + + // Set time to midnight for consistency + const getDateAtMidnight = (date) => { + const d = new Date(date); + d.setHours(0, 0, 0, 0); + return d; + }; + + const today = getDateAtMidnight(now); + + switch (selectedTimeRange) { + case 'this_month': + // First day of current month to last day of current month + startDate = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0); + endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999); + break; + case 'last_month': + // First day of last month to last day of last month + startDate = new Date(now.getFullYear(), now.getMonth() - 1, 1, 0, 0, 0, 0); + endDate = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999); + break; + case 'last_3_months': + // First day of 3 months ago to today + startDate = new Date(now.getFullYear(), now.getMonth() - 3, 1, 0, 0, 0, 0); + endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999); + break; + case 'this_year': + // January 1st of this year to today + startDate = new Date(now.getFullYear(), 0, 1, 0, 0, 0, 0); + endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999); + break; + case 'last_year': + // January 1st to December 31st of last year + startDate = new Date(now.getFullYear() - 1, 0, 1, 0, 0, 0, 0); + endDate = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 999); + break; + case 'last_3_years': + // January 1st of 3 years ago to today + startDate = new Date(now.getFullYear() - 3, 0, 1, 0, 0, 0, 0); + endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999); + break; + default: + return; + } + + // Format dates for Tableau (month/day/year format) + const formatDateForTableau = (date) => { + const month = date.getMonth() + 1; // getMonth() is 0-indexed + const day = date.getDate(); + const year = date.getFullYear(); + const hours = date.getHours(); + const minutes = date.getMinutes(); + const seconds = date.getSeconds(); + const ampm = hours >= 12 ? 'PM' : 'AM'; + const displayHours = hours % 12 || 12; + return `${month}/${day}/${year} ${displayHours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')} ${ampm}`; + }; + + console.log('๐Ÿ“… DATE RANGE CALCULATED:'); + console.log(' Start Date:', formatDateForTableau(startDate)); + console.log(' End Date:', formatDateForTableau(endDate)); + console.log(' ISO Start:', startDate.toISOString()); + console.log(' ISO End:', endDate.toISOString()); + console.log(' Days in range:', Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24))); + console.log('================================================='); + + console.log('๐ŸŽฏ APPLYING FILTER TO WORKSHEETS:'); + if (activeSheet.sheetType === 'dashboard') { + const worksheets = activeSheet.worksheets; + console.log(`Found ${worksheets.length} worksheets in dashboard`); + + // Log all worksheet details to understand what we're working with + console.log('๐Ÿ“‹ WORKSHEET DETAILS:'); + for (let i = 0; i < worksheets.length; i++) { + const ws = worksheets[i]; + const details = { + sheetType: ws.sheetType, + isHidden: ws.isHidden + }; + // Only try to access size if it's safe + try { + if (ws.size) { + details.size = ws.size; + } + } catch (e) { + // Size property not available for this worksheet type + } + console.log(` [${i}] Name: "${ws.name}"`, details); + } + console.log('================================================='); + + for (const worksheet of worksheets) { + console.log(`\n ๐Ÿ”ง Working on worksheet: "${worksheet.name}"`); + + // First, try to get existing filters to see what date fields are available + try { + const filters = await worksheet.getFiltersAsync(); + console.log(` Current filters:`, filters.map(f => ({ + fieldName: f.fieldName, + filterType: f.filterType + }))); + } catch (filterError) { + console.log(` Could not get filters:`, filterError.message); + } + + // Try multiple possible date field names + const possibleDateFields = [ + 'Adjusted Date', + 'Date', + 'Order Date', + 'Created Date', + 'Timestamp', + 'Activity Date', + 'Login Date', + '[Adjusted Date]', + '[Date]', + '[Order Date]' + ]; + + let filterApplied = false; + + for (const dateField of possibleDateFields) { + try { + await worksheet.applyRangeFilterAsync(dateField, { + min: startDate, + max: endDate + }); + console.log(` โœ… Filter applied using field: "${dateField}"`); + filterApplied = true; + break; // If successful, stop trying other fields + } catch (error) { + // This field doesn't exist or can't be filtered, try next one + console.log(` โญ๏ธ Field "${dateField}" not available or can't be filtered`); + } + } + + if (!filterApplied) { + console.error(` โŒ Could not apply filter to worksheet "${worksheet.name}" - no valid date field found`); + } + } + } else { + await activeSheet.applyRangeFilterAsync(fieldName, { + min: startDate, + max: endDate + }); + console.log(` โœ… Filter applied to sheet: "${activeSheet.name}"`); + } + + // Try to apply filter to Pulse metrics if they exist + console.log('๐Ÿ’ซ CHECKING FOR PULSE METRICS:'); + try { + // Log what's available on viz and workbook + console.log('๐Ÿ” DEBUG: Available viz properties:', Object.keys(viz)); + console.log('๐Ÿ” DEBUG: Available workbook properties:', Object.keys(viz.workbook)); + + // Check various possible locations for Pulse objects + const pulseObjects = viz.workbook?.pulse || []; + const extensions = viz.workbook?.extensions || []; + + console.log(' viz.workbook.pulse:', pulseObjects); + console.log(' viz.workbook.extensions:', extensions); + + // Check if any worksheets have "Pulse" in their name + const pulseWorksheets = activeSheet.worksheets?.filter(ws => + ws.name.toLowerCase().includes('pulse') || + ws.name.toLowerCase().includes('metric') || + ws.name.toLowerCase().includes('discover') + ) || []; + + console.log(` Worksheets with Pulse/Metric/Discover in name: ${pulseWorksheets.length}`); + pulseWorksheets.forEach(ws => console.log(` - "${ws.name}"`)); + + if (pulseObjects && pulseObjects.length > 0) { + console.log(`Found ${pulseObjects.length} Pulse metric(s) via viz.workbook.pulse`); + + for (const pulse of pulseObjects) { + try { + // Use applyTimeDimensionAsync for Pulse metrics + await pulse.applyTimeDimensionAsync({ + min: startDate, + max: endDate + }); + console.log(` โœ… Time filter applied to Pulse metric`); + } catch (pulseError) { + console.error(` โŒ Error applying time filter to Pulse metric:`, pulseError); + + // Fallback: Try regular filter method for Pulse + try { + await pulse.applyFilterAsync(fieldName, { + min: startDate, + max: endDate + }); + console.log(` โœ… Regular filter applied to Pulse metric (fallback)`); + } catch (fallbackError) { + console.error(` โŒ Fallback filter also failed:`, fallbackError); + } + } + } + } else { + console.log(' โ„น๏ธ No separate Pulse objects found via viz.workbook.pulse'); + console.log(' ๐Ÿ’ก Pulse metrics are likely embedded as worksheets and already filtered above'); + console.log(' ๐Ÿ’ก Check the worksheet names above - if you see your Pulse metrics listed, they should be filtered'); + } + } catch (error) { + console.error(' โš ๏ธ Error accessing Pulse objects:', error); + } + + console.log('================================================='); + console.log('โœจ FILTER APPLICATION COMPLETE'); + console.log('================================================='); + } catch (error) { + console.error('Error applying date filter:', error); + } + }; + + await applyFilterToViz(); + }; + + applyFilter(); + }, [selectedTimeRange]); + return (
-
- -
- Pulse Discover Dashboard - - Explore insights and analytics from the Pulse Discover dashboard - +
+
+ +
+ Pulse Discover Dashboard + + Explore insights and analytics from the Pulse Discover dashboard + {selectedTimeRange && ( + + โ€ข {timeRangeOptions.find(opt => opt.value === selectedTimeRange)?.label} + + )} + +
+
+
+
@@ -57,6 +420,73 @@ export const PulseDiscover = () => {
+ + {/* Time Range Filter Menu */} + {showFilterMenu && ( +
setShowFilterMenu(false)}> +
e.stopPropagation()}> +
+

+ + Select Time Range +

+ +
+ +
+ {timeRangeOptions.map((option) => { + const isSelected = selectedTimeRange === option.value; + + return ( + + ); + })} +
+ +
+ + +
+
+
+ )}
); }; diff --git a/src/app/demo/superstore/pulse-metrics/PulseMetrics.jsx b/src/app/demo/superstore/pulse-metrics/PulseMetrics.jsx new file mode 100644 index 00000000..61db8a08 --- /dev/null +++ b/src/app/demo/superstore/pulse-metrics/PulseMetrics.jsx @@ -0,0 +1,719 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui"; +import { Activity, MoreVertical, X } from "lucide-react"; +import { TableauEmbedEACanada } from "@/components/TableauEmbed"; +import { useTableauSessionEACanada } from "@/hooks"; + +// Map custom time ranges to valid Pulse time dimensions +const PULSE_TIME_DIMENSION_MAP = { + 'MonthToDate': 'MonthToDate', + 'LastMonth': 'LastMonth', + 'Last3Months': null, // No direct Pulse equivalent + 'YearToDate': 'YearToDate', + 'LastYear': 'LastYear', + 'Last3Years': null, // No direct Pulse equivalent +}; + +export const PulseMetrics = () => { + const dashboardUrl = 'https://prod-ca-a.online.tableau.com/t/eacanada/views/SalesandChurn-nopulse/Dashboard1'; + const containerRef = useRef(null); + const [selectedTimeRange, setSelectedTimeRange] = useState(''); + const [showFilterMenu, setShowFilterMenu] = useState(false); + const [pulseReady, setPulseReady] = useState(0); // Track how many Pulse components are ready + const pulseElementsRef = useRef([]); // Store references to Pulse DOM elements + + // Get authentication token for Pulse metrics + const { + data: user, + isSuccess: isSessionSuccess, + } = useTableauSessionEACanada(); + + const pulseMetrics = [ + { + id: 'pulse1', + src: 'https://prod-ca-a.online.tableau.com/pulse/site/eacanada/metrics/436e6eda-7565-4ec7-b164-2cdf1903c912', + layout: 'default', + theme: { + // Font settings - Bubblegum Sans is super cute! ๐ŸŽ€ + fontCssUrl: 'https://fonts.googleapis.com/css2?family=Bubblegum+Sans&display=swap', + fontSize: '16', + // Page-level colors - soft pastel pink theme + backgroundColor: '#FFF5F7', + backgroundColorOpaque: '#FFFFFF', + foregroundColor: '#FF69B4', + // Chart colors - playful pastels + chart: { + axisGrid: '#FFB6C1', + axisGridActive: '#FF69B4', + axisLabel: '#FF1493', + bar: '#FF69B4', + barAverage: '#FFB6C1', + barAxis: '#FFB6C1', + barAxisLabel: '#FF1493', + barCumulative: '#DDA0DD', + barCumulativeLabel: '#DDA0DD', + barFavorable: '#98FB98', + barLabel: '#FF69B4', + barLabelAverage: '#FFB6C1', + barLabelFavorable: '#32CD32', + barLabelUnfavorable: '#FF6B9D', + barLabelUnspecified: '#DDA0DD', + barSum: '#DDA0DD', + barUnfavorable: '#FF6B9D', + barUnspecified: '#DDA0DD', + changeFavorable: '#98FB98', + changeUnfavorable: '#FF6B9D', + currentValue: '#FFFFFF', + currentValueDot: '#FF1493', + currentValueDotBorder: '#FF69B4', + dotBorder: '#FF69B4', + hoverDot: '#FF1493', + hoverLine: '#FF69B4', + line: '#FF69B4', + projection: '#98FB98', + range: '#FFF0F5' + } + } + }, + { + id: 'pulse2', + src: 'https://prod-ca-a.online.tableau.com/pulse/site/eacanada/metrics/63fefae0-755b-461a-8db6-0453f9bc7a6e', + layout: 'default', + theme: { + fontCssUrl: 'https://fonts.googleapis.com/css2?family=Bubblegum+Sans&display=swap', + fontSize: '16', + backgroundColor: '#F0F8FF', + backgroundColorOpaque: '#FFFFFF', + foregroundColor: '#4169E1', + chart: { + axisGrid: '#87CEEB', + axisGridActive: '#4169E1', + axisLabel: '#1E90FF', + bar: '#4169E1', + barAverage: '#87CEEB', + barAxis: '#87CEEB', + barAxisLabel: '#1E90FF', + barCumulative: '#9370DB', + barCumulativeLabel: '#9370DB', + barFavorable: '#98FB98', + barLabel: '#4169E1', + barLabelAverage: '#87CEEB', + barLabelFavorable: '#32CD32', + barLabelUnfavorable: '#FF6B9D', + barLabelUnspecified: '#9370DB', + barSum: '#9370DB', + barUnfavorable: '#FF6B9D', + barUnspecified: '#9370DB', + changeFavorable: '#98FB98', + changeUnfavorable: '#FF6B9D', + currentValue: '#FFFFFF', + currentValueDot: '#1E90FF', + currentValueDotBorder: '#4169E1', + dotBorder: '#4169E1', + hoverDot: '#1E90FF', + hoverLine: '#4169E1', + line: '#4169E1', + projection: '#98FB98', + range: '#F0F8FF' + } + } + }, + { + id: 'pulse3', + src: 'https://prod-ca-a.online.tableau.com/pulse/site/eacanada/metrics/cd306965-a275-49fa-8f9e-25adf2f57309', + layout: 'default', + theme: { + fontCssUrl: 'https://fonts.googleapis.com/css2?family=Bubblegum+Sans&display=swap', + fontSize: '16', + backgroundColor: '#FFF8DC', + backgroundColorOpaque: '#FFFFFF', + foregroundColor: '#FFD700', + chart: { + axisGrid: '#FFE4B5', + axisGridActive: '#FFD700', + axisLabel: '#FFA500', + bar: '#FFD700', + barAverage: '#FFE4B5', + barAxis: '#FFE4B5', + barAxisLabel: '#FFA500', + barCumulative: '#DDA0DD', + barCumulativeLabel: '#DDA0DD', + barFavorable: '#98FB98', + barLabel: '#FFD700', + barLabelAverage: '#FFE4B5', + barLabelFavorable: '#32CD32', + barLabelUnfavorable: '#FF6B9D', + barLabelUnspecified: '#DDA0DD', + barSum: '#DDA0DD', + barUnfavorable: '#FF6B9D', + barUnspecified: '#DDA0DD', + changeFavorable: '#98FB98', + changeUnfavorable: '#FF6B9D', + currentValue: '#FFFFFF', + currentValueDot: '#FFA500', + currentValueDotBorder: '#FFD700', + dotBorder: '#FFD700', + hoverDot: '#FFA500', + hoverLine: '#FFD700', + line: '#FFD700', + projection: '#98FB98', + range: '#FFF8DC' + } + } + }, + { + id: 'pulse4', + src: 'https://prod-ca-a.online.tableau.com/pulse/site/eacanada/metrics/bd71f5a2-a7f5-4db8-bc6e-f581485dc8e9', + layout: 'default', + theme: { + fontCssUrl: 'https://fonts.googleapis.com/css2?family=Bubblegum+Sans&display=swap', + fontSize: '16', + backgroundColor: '#F0FFF0', + backgroundColorOpaque: '#FFFFFF', + foregroundColor: '#32CD32', + chart: { + axisGrid: '#98FB98', + axisGridActive: '#32CD32', + axisLabel: '#228B22', + bar: '#32CD32', + barAverage: '#98FB98', + barAxis: '#98FB98', + barAxisLabel: '#228B22', + barCumulative: '#9370DB', + barCumulativeLabel: '#9370DB', + barFavorable: '#98FB98', + barLabel: '#32CD32', + barLabelAverage: '#98FB98', + barLabelFavorable: '#228B22', + barLabelUnfavorable: '#FF6B9D', + barLabelUnspecified: '#9370DB', + barSum: '#9370DB', + barUnfavorable: '#FF6B9D', + barUnspecified: '#9370DB', + changeFavorable: '#98FB98', + changeUnfavorable: '#FF6B9D', + currentValue: '#FFFFFF', + currentValueDot: '#228B22', + currentValueDotBorder: '#32CD32', + dotBorder: '#32CD32', + hoverDot: '#228B22', + hoverLine: '#32CD32', + line: '#32CD32', + projection: '#98FB98', + range: '#F0FFF0' + } + } + }, + ]; + + // Get the current Pulse time dimension based on selected filter + const pulseTimeDimension = selectedTimeRange ? + (PULSE_TIME_DIMENSION_MAP[selectedTimeRange] || 'MonthToDate') : + 'MonthToDate'; + + const timeRangeOptions = [ + { label: 'This Month', value: 'MonthToDate' }, + { label: 'Last Month', value: 'LastMonth' }, + { label: 'Last 3 Months', value: 'Last3Months' }, + { label: 'This Year', value: 'YearToDate' }, + { label: 'Last Year', value: 'LastYear' }, + { label: 'Last 3 Years', value: 'Last3Years' }, + ]; + + // Set up event listeners for Pulse components when they render + useEffect(() => { + if (!isSessionSuccess) return; + + let readyCount = 0; + let mounted = true; + let checkForPulse = null; + + // Give React time to render the Pulse components to the DOM + const initialDelay = setTimeout(() => { + console.log('๐Ÿ” Starting to search for Pulse components in DOM...'); + + // Poll for Pulse components to be available in the DOM + checkForPulse = setInterval(() => { + const pulseElements = document.querySelectorAll('tableau-pulse'); + + if (pulseElements.length > 0) { + console.log(`๐Ÿ” Found ${pulseElements.length} Pulse components in DOM`); + } + + if (pulseElements.length === 4 && mounted) { + clearInterval(checkForPulse); + console.log(`โœ… All 4 Pulse components found, setting up event listeners...`); + + pulseElementsRef.current = Array.from(pulseElements); + + pulseElements.forEach((pulse, index) => { + console.log(`๐Ÿ“‹ Pulse metric ${index + 1} properties:`, { + id: pulse.id, + src: pulse.src, + tagName: pulse.tagName, + hasToken: !!pulse.getAttribute('token'), + methods: typeof pulse.applyTimeDimensionAsync + }); + + // Add FirstInteractive event listener - using string literal as per Tableau docs + pulse.addEventListener('firstinteractive', (e) => { + readyCount++; + console.log(`โœ… Pulse metric ${index + 1} is now interactive (${readyCount}/4)`); + console.log(` Event type: ${e.type}`); + console.log(` Methods available:`, typeof pulse.applyTimeDimensionAsync); + + if (readyCount === 4 && mounted) { + console.log('๐ŸŽ‰ All Pulse metrics are interactive and ready!'); + setPulseReady(4); + } + }, { once: true }); // Use { once: true } instead of manual removeEventListener + + // Add PulseTimeDimensionChanged event listener + pulse.addEventListener('pulsetimedimensionchanged', (e) => { + console.log(`๐Ÿ“… Pulse metric ${index + 1} time dimension changed to: "${e.detail?.timeDimension}"`); + }); + + // Add error event listener + pulse.addEventListener('pulseerror', (e) => { + console.error(`โŒ Pulse metric ${index + 1} error:`, e.detail); + }); + + console.log(` โœ“ Event listeners added to Pulse metric ${index + 1}`); + }); + + // Fallback: if events don't fire within 10 seconds, set ready anyway + setTimeout(() => { + if (readyCount < 4 && mounted) { + console.warn('โš ๏ธ Pulse metrics did not fire firstinteractive events within 10s'); + console.warn('โš ๏ธ Setting ready state anyway to allow filter interaction'); + setPulseReady(4); + } + }, 10000); + } + }, 500); + }, 1000); // Wait 1 second before starting to poll + + // Cleanup + return () => { + mounted = false; + clearTimeout(initialDelay); + if (checkForPulse) clearInterval(checkForPulse); + }; + }, [isSessionSuccess]); + + // Apply filter to Pulse metrics and dashboard when selectedTimeRange changes + useEffect(() => { + if (pulseReady < 4) { + console.log(`โณ Waiting for Pulse metrics to be ready (${pulseReady}/4)...`); + return; + } + + const applyFilter = async () => { + console.log('================================================='); + console.log('๐Ÿ” PULSE METRICS FILTER'); + console.log('================================================='); + console.log('Selected Time Range:', selectedTimeRange || 'NONE (Clearing filter)'); + console.log('================================================='); + + const applyFilterToViz = async () => { + let viz = document.getElementById('pulseMetricsViz'); + + if (!viz) { + const tableauVizElements = document.querySelectorAll('tableau-viz'); + if (tableauVizElements.length > 0) { + viz = tableauVizElements[0]; + } + } + + if (!viz) { + console.log('โณ Viz not ready yet, retrying...'); + setTimeout(() => applyFilterToViz(), 500); + return; + } + + // Check if viz is properly initialized + if (!viz.workbook || !viz.workbook.activeSheet) { + console.log('โณ Viz workbook not ready yet, retrying...'); + setTimeout(() => applyFilterToViz(), 500); + return; + } + + try { + const activeSheet = viz.workbook.activeSheet; + + // If no time range selected, reset everything + if (!selectedTimeRange) { + + console.log('๐Ÿงน CLEARING FILTERS FROM DASHBOARD WORKSHEETS'); + if (activeSheet.sheetType === 'dashboard') { + const worksheets = activeSheet.worksheets; + console.log(`Found ${worksheets.length} worksheets in dashboard`); + + for (const worksheet of worksheets) { + try { + await worksheet.clearFilterAsync('Adjusted Date'); + console.log(` โœ… Filter cleared from worksheet: "${worksheet.name}"`); + } catch (error) { + console.error(` โŒ Error clearing filter from worksheet "${worksheet.name}":`, error); + } + } + } + + console.log('\n๐Ÿงน RESETTING PULSE METRICS & DASHBOARD'); + + // Reset Pulse metrics to MonthToDate and wait for confirmation + if (pulseElementsRef.current.length > 0) { + console.log(' ๐Ÿ“… Resetting Pulse metrics to MonthToDate...'); + + // Set up promises to wait for time dimension changed events + const resetPromises = pulseElementsRef.current.map((pulse, i) => + new Promise(async (resolve) => { + const handler = (e) => { + console.log(` โœ… Pulse metric ${i + 1} reset confirmed: "${e.detail.timeDimension}"`); + pulse.removeEventListener('pulsetimedimensionchanged', handler); + resolve(); + }; + pulse.addEventListener('pulsetimedimensionchanged', handler, { once: true }); + + try { + await pulse.applyTimeDimensionAsync('MonthToDate'); + } catch (error) { + console.error(` โŒ Error resetting Pulse metric ${i + 1}:`, error); + pulse.removeEventListener('pulsetimedimensionchanged', handler); + resolve(); + } + }) + ); + + await Promise.all(resetPromises); + console.log(' ๐ŸŽ‰ All Pulse metrics reset complete'); + } + + console.log(' โœ… Dashboard filters cleared'); + + console.log('================================================='); + return; + } + + console.log('๐Ÿ“… TIME DIMENSION SELECTED:'); + console.log(' Time Dimension:', selectedTimeRange); + console.log('================================================='); + + // Calculate date ranges for dashboard filtering + const now = new Date(); + let startDate, endDate; + + switch (selectedTimeRange) { + case 'MonthToDate': + startDate = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0); + endDate = now; + break; + case 'LastMonth': + startDate = new Date(now.getFullYear(), now.getMonth() - 1, 1, 0, 0, 0, 0); + endDate = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999); + break; + case 'Last3Months': + startDate = new Date(now.getFullYear(), now.getMonth() - 3, 1, 0, 0, 0, 0); + endDate = now; + break; + case 'YearToDate': + startDate = new Date(now.getFullYear(), 0, 1, 0, 0, 0, 0); + endDate = now; + break; + case 'LastYear': + startDate = new Date(now.getFullYear() - 1, 0, 1, 0, 0, 0, 0); + endDate = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 999); + break; + case 'Last3Years': + startDate = new Date(now.getFullYear() - 3, 0, 1, 0, 0, 0, 0); + endDate = now; + break; + default: + return; + } + + const formatDateForTableau = (date) => { + const month = date.getMonth() + 1; + const day = date.getDate(); + const year = date.getFullYear(); + return `${month}/${day}/${year}`; + }; + + console.log('๐Ÿ“Š DATE RANGE FOR DASHBOARD:'); + console.log(' Start Date:', formatDateForTableau(startDate)); + console.log(' End Date:', formatDateForTableau(endDate)); + console.log('================================================='); + + // Apply date range filter to dashboard worksheets + console.log('๐ŸŽฏ APPLYING FILTER TO DASHBOARD WORKSHEETS:'); + if (activeSheet.sheetType === 'dashboard') { + const worksheets = activeSheet.worksheets; + console.log(`Found ${worksheets.length} worksheets in dashboard`); + + for (const worksheet of worksheets) { + console.log(`\n ๐Ÿ”ง Working on worksheet: "${worksheet.name}"`); + try { + await worksheet.applyRangeFilterAsync('Adjusted Date', { + min: startDate, + max: endDate + }); + console.log(` โœ… Filter applied to worksheet`); + } catch (error) { + console.error(` โŒ Error applying filter to worksheet "${worksheet.name}":`, error); + } + } + } + + // Apply time dimension to Pulse metrics using applyTimeDimensionAsync() + console.log('\n๐Ÿ’ซ APPLYING TIME DIMENSION TO PULSE METRICS:'); + const pulseTimeDimension = PULSE_TIME_DIMENSION_MAP[selectedTimeRange]; + + if (pulseTimeDimension && pulseElementsRef.current.length > 0) { + console.log(` ๐Ÿ“… Applying time dimension: "${pulseTimeDimension}"`); + + // Set up promises to wait for time dimension changed events + const timeDimensionPromises = pulseElementsRef.current.map((pulse, i) => + new Promise(async (resolve) => { + const handler = (e) => { + pulse.removeEventListener('pulsetimedimensionchanged', handler); + resolve(); + }; + pulse.addEventListener('pulsetimedimensionchanged', handler, { once: true }); + + try { + await pulse.applyTimeDimensionAsync(pulseTimeDimension); + } catch (error) { + console.error(` โŒ Error applying time dimension to Pulse metric ${i + 1}:`, error); + pulse.removeEventListener('pulsetimedimensionchanged', handler); + resolve(); + } + }) + ); + + // Wait for all Pulse metrics to confirm the time dimension change + await Promise.all(timeDimensionPromises); + console.log(' ๐ŸŽ‰ All Pulse metrics time dimension changes confirmed'); + + } else if (selectedTimeRange && !pulseTimeDimension) { + console.log(` โ„น๏ธ No direct Pulse equivalent for "${selectedTimeRange}"`); + console.log(` โ„น๏ธ Pulse metrics will keep their current time dimension`); + console.log(` โ„น๏ธ Dashboard will be filtered to the custom date range`); + } else { + console.log(` โœ… Pulse metrics showing default time dimension`); + } + + console.log('================================================='); + console.log('โœจ FILTER APPLICATION COMPLETE'); + console.log('================================================='); + } catch (error) { + console.error('Error applying filter:', error); + } + }; + + await applyFilterToViz(); + }; + + applyFilter(); + }, [selectedTimeRange, pulseReady]); + + return ( +
+
+
+ {/* Header Card with Filter Button */} + + +
+
+ +
+ Pulse Metrics & Dashboard + + AI-powered insights from Tableau Pulse + {selectedTimeRange && ( + + โ€ข {timeRangeOptions.find(opt => opt.value === selectedTimeRange)?.label} + + )} + +
+
+
+ +
+
+
+
+ + {/* Pulse Metrics Grid */} + {isSessionSuccess && ( +
+ {pulseMetrics.map((metric, index) => ( + + +
+ + {/* Font Settings */} + + + + {/* Page-Level Colors */} + + + + + {/* Chart Colors */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ ))} +
+ )} + + {/* Dashboard Card */} + + + Pulse Discover Dashboard + + Explore insights and analytics from the Pulse Discover dashboard + + + +
+
+ +
+
+
+
+
+ + {/* Time Range Filter Menu */} + {showFilterMenu && ( +
setShowFilterMenu(false)}> +
e.stopPropagation()}> +
+

+ + Select Time Range +

+ +
+ +
+ {timeRangeOptions.map((option) => { + const isSelected = selectedTimeRange === option.value; + + return ( + + ); + })} +
+ +
+ + +
+
+
+ )} +
+
+ ); +}; diff --git a/src/app/demo/superstore/pulse-metrics/page.jsx b/src/app/demo/superstore/pulse-metrics/page.jsx new file mode 100644 index 00000000..6af25e29 --- /dev/null +++ b/src/app/demo/superstore/pulse-metrics/page.jsx @@ -0,0 +1,30 @@ +import dynamic from 'next/dynamic'; +import { Demo, FloatingAssistant } from '@/components'; +import { settings } from '../config'; + +// Dynamically import PulseMetrics with SSR disabled since it uses browser-only APIs +const PulseMetrics = dynamic( + () => import('./PulseMetrics').then((mod) => ({ default: mod.PulseMetrics })), + { + ssr: false, + loading: () =>
Loading Pulse metrics...
+ } +); + +const Page = () => { + const pageName = 'Pulse Metrics'; + + return ( + + + + + ) +} + +export default Page; diff --git a/src/app/demo/superstore/tabnext/TabNext.jsx b/src/app/demo/superstore/tabnext/TabNext.jsx index 38ab6686..795dd40e 100644 --- a/src/app/demo/superstore/tabnext/TabNext.jsx +++ b/src/app/demo/superstore/tabnext/TabNext.jsx @@ -37,11 +37,14 @@ export const TabNext = () => { return 'MFG_Production_Scraps1'; }, []); // Salesforce username for JWT Bearer Flow (comes from user store via session) - const salesforceUsername = useMemo(() => - session?.user?.salesforceUsername || - session?.user?.email?.split('@')[0] || - "", - [session]); + const salesforceUsername = useMemo(() => { + const username = session?.user?.salesforceUsername || + session?.user?.email?.split('@')[0] || + ""; + console.log('[TabNext] salesforceUsername:', username); + console.log('[TabNext] session.user:', session?.user); + return username; + }, [session]); // Initialize SDK and render dashboard // This follows the Tableau Next embedding pattern: @@ -169,7 +172,10 @@ export const TabNext = () => { // JWT Bearer Flow - authenticates with Salesforce using username and certificate // Reference: https://developer.salesforce.com/docs/analytics/sdk/guide/sdk-access-token.html const handleJWTBearerAuth = useCallback(async () => { + console.log('[TabNext] handleJWTBearerAuth called with username:', salesforceUsername); + if (!salesforceUsername) { + console.error('[TabNext] No salesforceUsername provided'); setError('Salesforce username is required. Please ensure your user profile includes a salesforceUsername.'); setStatus("idle"); return; @@ -178,6 +184,8 @@ export const TabNext = () => { try { setError(""); setStatus("authenticating"); + + console.log('[TabNext] Sending JWT auth request...'); const resp = await fetch('/api/tabnext/jwt-auth', { method: 'POST', @@ -185,14 +193,20 @@ export const TabNext = () => { body: JSON.stringify({ salesforce_username: salesforceUsername }), }); + console.log('[TabNext] JWT auth response status:', resp.status); + if (!resp.ok) { const text = await resp.text(); + console.error('[TabNext] JWT auth failed:', text); setError(`Authentication failed: ${text}`); setStatus("idle"); return; } const data = await resp.json(); + console.log('[TabNext] JWT auth success, authCredential length:', data.authCredential?.length); + console.log('[TabNext] Instance URL:', data.instance_url); + const authCredential = data.authCredential; if (!authCredential) { throw new Error('No authCredential in response'); @@ -201,6 +215,7 @@ export const TabNext = () => { const instanceUrl = data.instance_url; await initializeAndRender(authCredential, instanceUrl); } catch (e) { + console.error('[TabNext] Authentication error:', e); setError(`Authentication error: ${e.message || String(e)}`); setStatus("idle"); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9cc1b70c..a6e04f0f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ 'use client' import { useState, useEffect } from 'react'; +import Script from 'next/script'; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { SessionProvider } from 'next-auth/react'; @@ -23,6 +24,12 @@ export default function RootLayout({ return ( + {/* Load Tableau Embedding API v3 from CDN */} +