-
-
-
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 */}
+
diff --git a/src/components/TableauEmbed/TableauToolbar/actions.js b/src/components/TableauEmbed/TableauToolbar/actions.js
index 60683949..7ea878e5 100644
--- a/src/components/TableauEmbed/TableauToolbar/actions.js
+++ b/src/components/TableauEmbed/TableauToolbar/actions.js
@@ -1,32 +1,42 @@
-// eslint-disable-next-line no-unused-vars
-import { tab_embed } from 'libs';
-
export const exportImage = async (viz) => {
viz.exportImageAsync();
}
export const exportPDF = async (viz) => {
- viz.displayDialogAsync(tab_embed.TableauDialogType.ExportPDF);
+ // Access TableauDialogType from the global tableau object (loaded via CDN script)
+ if (typeof window !== 'undefined' && window.tableau) {
+ viz.displayDialogAsync(window.tableau.TableauDialogType.ExportPDF);
+ }
}
export const exportCrossTab = async (viz) => {
- viz.displayDialogAsync(tab_embed.TableauDialogType.ExportCrossTab);
+ if (typeof window !== 'undefined' && window.tableau) {
+ viz.displayDialogAsync(window.tableau.TableauDialogType.ExportCrossTab);
+ }
}
export const exportData = async (viz) => {
- viz.displayDialogAsync(tab_embed.TableauDialogType.ExportData);
+ if (typeof window !== 'undefined' && window.tableau) {
+ viz.displayDialogAsync(window.tableau.TableauDialogType.ExportData);
+ }
}
export const exportPPT = async (viz) => {
- viz.displayDialogAsync(tab_embed.TableauDialogType.ExportPowerPoint);
+ if (typeof window !== 'undefined' && window.tableau) {
+ viz.displayDialogAsync(window.tableau.TableauDialogType.ExportPowerPoint);
+ }
}
export const exportTWBX = async (viz) => {
- viz.displayDialogAsync(tab_embed.TableauDialogType.ExportWorkbook);
+ if (typeof window !== 'undefined' && window.tableau) {
+ viz.displayDialogAsync(window.tableau.TableauDialogType.ExportWorkbook);
+ }
}
export const shareViz = async (viz) => {
- viz.displayDialogAsync(tab_embed.TableauDialogType.Share);
+ if (typeof window !== 'undefined' && window.tableau) {
+ viz.displayDialogAsync(window.tableau.TableauDialogType.Share);
+ }
}
export const refreshData = async (viz) => {
diff --git a/src/components/TableauEmbed/TableauViz.jsx b/src/components/TableauEmbed/TableauViz.jsx
index 33a49bd0..603e5948 100644
--- a/src/components/TableauEmbed/TableauViz.jsx
+++ b/src/components/TableauEmbed/TableauViz.jsx
@@ -1,9 +1,6 @@
"use client";
import { useEffect, useState, useRef, forwardRef, useId } from 'react';
-// eslint-disable-next-line no-unused-vars
-import { tab_embed } from 'libs';
-
import { TableauToolbar, XSLayout, SMLayout, MDLayout, LGLayout, XLLayout, XL2Layout } from 'components';
import { getLayoutProps, parseClassNameForLayouts } from './vizUtils';
diff --git a/src/components/TableauEmbed/TableauWebAuthor.jsx b/src/components/TableauEmbed/TableauWebAuthor.jsx
index 708f3419..a5f1031e 100644
--- a/src/components/TableauEmbed/TableauWebAuthor.jsx
+++ b/src/components/TableauEmbed/TableauWebAuthor.jsx
@@ -2,10 +2,6 @@
import { useState, useRef, forwardRef, useId } from 'react';
-// eslint-disable-next-line no-unused-vars
-import { tab_embed } from 'libs';
-
-
// handles post authentication logic requiring an initialized object to operate
export const TableauWebAuthor = forwardRef(function Viz(props, ref) {
const { src, jwt, height, width, isPublic } = props;
diff --git a/src/libs/index.js b/src/libs/index.js
index e76d7499..8600fb16 100644
--- a/src/libs/index.js
+++ b/src/libs/index.js
@@ -1,4 +1,5 @@
-export const tab_embed = typeof window !== 'undefined' ? require("./tableau.embedding.3.latest.min.js") : null;
+// Note: The Tableau Embedding API is now loaded via CDN script tag in layout.tsx
+// See: https://public.tableau.com/javascripts/api/tableau.embedding.3.latest.min.js
export {
tabAuthJWT, tabAuthPAT, tabAuthJWTEACanada, tabSignOut, getSubscriptions, getSpecifications,
diff --git a/src/models/Users/userStore.ts b/src/models/Users/userStore.ts
index 45f6c346..9519f22f 100644
--- a/src/models/Users/userStore.ts
+++ b/src/models/Users/userStore.ts
@@ -65,7 +65,7 @@ export const Users = [
role: 1,
vector_store: 'superstore_jchen',
uaf: {"Region": ["Central","East"]},
- salesforceUsername: "xavier.acme@storm-31c05a56906a6d.com"
+ salesforceUsername: "yvetteacme.8wplepayv6fh.3s72gsi9ogfp@salesforce.com"
},
{
id: 'c',