diff --git a/.nvmrc b/.nvmrc index deed13c0..603606bc 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -lts/jod +18.17.0 diff --git a/LANGUAGE_SELECTOR_GUIDE.md b/LANGUAGE_SELECTOR_GUIDE.md new file mode 100644 index 00000000..7cccf0e3 --- /dev/null +++ b/LANGUAGE_SELECTOR_GUIDE.md @@ -0,0 +1,124 @@ +# Language Selector Implementation Guide + +The language selector is now available on every page through a global context. Here's how to use it: + +## 🚀 Quick Setup + +The `LanguageProvider` is already wrapped around the entire app in `src/app/layout.tsx`, so the language selector is available everywhere. + +## 📝 Adding Language Selector to Any Page + +### 1. Import the Components + +```jsx +import { LanguageSelector } from '@/components/LanguageSelector'; +import { useLanguage } from '@/contexts/LanguageContext'; +``` + +### 2. Use the Language Context + +```jsx +export const YourComponent = () => { + const { t } = useLanguage(); + + return ( +
+

{t.title}

+

{t.subtitle}

+ {/* Your content here */} +
+ ); +}; +``` + +### 3. Add the Language Selector to Your Header + +```jsx +
+ + {/* Other header elements */} +
+``` + +## 🌍 Available Languages + +- **🇺🇸 English** (default) +- **🇪🇸 Español** (Spanish) +- **🇫🇷 Français** (French) + +## 📚 Available Translations + +The `t` object contains all translated strings. Common keys include: + +- `t.title` - Page title +- `t.subtitle` - Page subtitle +- `t.close` - Close button +- `t.previous` - Previous button +- `t.next` - Next button +- `t.email` - Email label +- `t.subject` - Subject label +- `t.message` - Message label + +## 🔧 Example Implementation + +Here's a complete example of adding the language selector to a page: + +```jsx +"use client"; + +import { LanguageSelector } from '@/components/LanguageSelector'; +import { useLanguage } from '@/contexts/LanguageContext'; + +export const MyPage = () => { + const { t } = useLanguage(); + + return ( +
+ {/* Header */} +
+
+
+

{t.title}

+

{t.subtitle}

+
+
+ + +
+
+
+ + {/* Page Content */} +
+

{t.message}

+
+
+ ); +}; +``` + +## ✨ Features + +- **Global State**: Language selection persists across all pages +- **Automatic Translation**: All text updates instantly when language changes +- **Click Outside to Close**: Dropdown closes when clicking outside +- **Flag Icons**: Visual language indicators with country flags +- **Responsive Design**: Works on all screen sizes + +## 🎯 Current Pages with Language Selector + +- ✅ Home page (`/demo/contractor`) +- ✅ Safety Dashboard (`/demo/contractor/safety`) + + +## 📝 Adding to New Pages + +To add the language selector to any new page, simply: + +1. Import `LanguageSelector` and `useLanguage` +2. Add `` to your header +3. Use `t.keyName` for any text that should be translated + +The language context is already available everywhere, so no additional setup is needed! diff --git a/TABLEAU_CONFIG.md b/TABLEAU_CONFIG.md new file mode 100644 index 00000000..e59e6da6 --- /dev/null +++ b/TABLEAU_CONFIG.md @@ -0,0 +1,38 @@ +# Tableau Cloud Configuration + +## Environment Variables + +Create a `.env.local` file in the root directory with the following variables: + +```bash +# Tableau Cloud Configuration +NEXT_PUBLIC_TABLEAU_BASE_URL=https://prod-useast-b.online.tableau.com +NEXT_PUBLIC_TABLEAU_SITE_ID=embeddingplaybook +``` + +## Current Configuration + +The system is currently configured to use: +- **Base URL**: `https://prod-useast-b.online.tableau.com` +- **Site ID**: `embeddingplaybook` + +## Authentication + +The navigation system uses Tableau's REST API for authentication. Users will need to provide their Tableau Cloud credentials to access their dashboards. + +## Features + +- ✅ Real Tableau Cloud authentication +- ✅ Dynamic dashboard loading +- ✅ Folder-based organization +- ✅ Search functionality +- ✅ Responsive design +- ✅ Dark mode optimized + +## Testing + +To test the system: +1. Navigate to the Safety Dashboard page +2. Click "Sign In to Tableau" in the navigation panel +3. Enter your Tableau Cloud credentials +4. Browse and select dashboards from your site diff --git a/TABLEAU_TRANSLATION_API.md b/TABLEAU_TRANSLATION_API.md new file mode 100644 index 00000000..9773e127 --- /dev/null +++ b/TABLEAU_TRANSLATION_API.md @@ -0,0 +1,342 @@ +# Tableau Data Translation API + +This API provides comprehensive translation capabilities for all Tableau data including metrics, dashboards, worksheets, filters, and mark selections. + +## 🚀 Quick Start + +```jsx +import { useTableauTranslation } from '@/hooks/useTableauTranslation'; + +const MyComponent = () => { + const { translateMetric, translateDashboard, translateMarks } = useTableauTranslation(); + + // Translate any Tableau data + const translatedMetric = translateMetric(metricData); + const translatedDashboard = translateDashboard(dashboardData); + const translatedMarks = translateMarks(markData); + + return
{translatedMetric.name}
; +}; +``` + +## 📚 Available Translation Functions + +### Core Translation Functions + +| Function | Description | Input | Output | +|----------|-------------|-------|--------| +| `translateData(data, dataType)` | Translate any Tableau data | `object`, `string` | `object` | +| `translateMetric(metricData)` | Translate metric data | `object` | `object` | +| `translateDashboard(dashboardData)` | Translate dashboard data | `object` | `object` | +| `translateWorksheet(worksheetData)` | Translate worksheet data | `object` | `object` | +| `translateFilter(filterData)` | Translate filter data | `object` | `object` | +| `translateMarks(markData)` | Translate mark selection data | `object` | `object` | +| `translateDataArray(dataArray, dataType)` | Translate array of data | `Array`, `string` | `Array` | + +### Utility Functions + +| Function | Description | Returns | +|----------|-------------|---------| +| `getCurrentLanguage()` | Get current language code | `string` | +| `hasTranslation(text)` | Check if text has translation | `boolean` | +| `translations` | Direct access to translations | `object` | +| `language` | Current language code | `string` | + +## 🌍 Supported Languages + +- **🇺🇸 English** (default) +- **🇪🇸 Spanish** (Español) +- **🇫🇷 French** (Français) + +## 📊 Supported Data Types + +### 1. Metrics +```jsx +const { translateMetric } = useTableauTranslation(); + +const metricData = { + name: "Total Contractors", + displayName: "Total Contractors", + description: "Number of active contractors", + value: 150, + units: "contractors" +}; + +const translatedMetric = translateMetric(metricData); +// Result: { name: "Total de Contratistas", displayName: "Total de Contratistas", ... } +``` + +### 2. Dashboards +```jsx +const { translateDashboard } = useTableauTranslation(); + +const dashboardData = { + title: "Safety Overview", + description: "Monitor safety compliance", + worksheets: [...] +}; + +const translatedDashboard = translateDashboard(dashboardData); +``` + +### 3. Worksheets +```jsx +const { translateWorksheet } = useTableauTranslation(); + +const worksheetData = { + name: "Safety Incidents", + title: "Safety Incidents Analysis", + columns: [ + { fieldName: "Incident Type", displayName: "Incident Type" }, + { fieldName: "Severity", displayName: "Severity" } + ] +}; + +const translatedWorksheet = translateWorksheet(worksheetData); +``` + +### 4. Filters +```jsx +const { translateFilter } = useTableauTranslation(); + +const filterData = { + fieldName: "Insurance Status", + displayName: "Insurance Status", + values: ["Active", "Expired", "Pending"], + options: ["All", "Active", "Expired", "Pending"] +}; + +const translatedFilter = translateFilter(filterData); +``` + +### 5. Mark Selections +```jsx +const { translateMarks } = useTableauTranslation(); + +const markData = { + columns: [ + { fieldName: "Contractor Name", displayName: "Contractor Name" }, + { fieldName: "Risk Level", displayName: "Risk Level" } + ], + data: [ + ["ABC Corp", "High Risk"], + ["XYZ Ltd", "Low Risk"] + ] +}; + +const translatedMarks = translateMarks(markData); +``` + +## 🔧 Advanced Usage + +### Batch Translation +```jsx +const { translateDataArray } = useTableauTranslation(); + +const metrics = [ + { name: "Total Contractors", value: 150 }, + { name: "Safety Incidents", value: 5 }, + { name: "Compliance Rate", value: 95 } +]; + +const translatedMetrics = translateDataArray(metrics, 'metric'); +``` + +### Conditional Translation +```jsx +const { hasTranslation, translateMetric } = useTableauTranslation(); + +const processMetric = (metric) => { + if (hasTranslation(metric.name)) { + return translateMetric(metric); + } + return metric; // Use original if no translation +}; +``` + +### Language-Specific Logic +```jsx +const { getCurrentLanguage, translateMetric } = useTableauTranslation(); + +const processData = (data) => { + const language = getCurrentLanguage(); + + if (language === 'es') { + // Spanish-specific processing + return translateMetric(data); + } else if (language === 'fr') { + // French-specific processing + return translateMetric(data); + } + + return data; +}; +``` + +## 📝 Adding New Translations + +### 1. Update Language Context +Add new metric names to `src/contexts/LanguageContext.jsx`: + +```jsx +metrics: { + "New Metric Name": "New Metric Name", + "Another Metric": "Another Metric", + // ... existing translations +} +``` + +### 2. Add Translations for All Languages +```jsx +// English +"New Metric Name": "New Metric Name", + +// Spanish +"New Metric Name": "Nuevo Nombre de Métrica", + +// French +"New Metric Name": "Nouveau Nom de Métrique", +``` + +### 3. Use in Components +```jsx +const { translateMetric } = useTableauTranslation(); +const translatedMetric = translateMetric(metricData); +``` + +## 🎯 Common Use Cases + +### 1. Tableau Dashboard Integration +```jsx +const Dashboard = () => { + const { translateDashboard } = useTableauTranslation(); + const [dashboardData, setDashboardData] = useState(null); + + useEffect(() => { + // Fetch dashboard data from Tableau + fetchDashboardData().then(data => { + const translated = translateDashboard(data); + setDashboardData(translated); + }); + }, []); + + return
{dashboardData?.title}
; +}; +``` + +### 2. Dynamic Filter Translation +```jsx +const FilterComponent = ({ filterData }) => { + const { translateFilter } = useTableauTranslation(); + const translatedFilter = translateFilter(filterData); + + return ( + + ); +}; +``` + +### 3. Mark Selection Translation +```jsx +const MarkSelectionHandler = ({ marksData }) => { + const { translateMarks } = useTableauTranslation(); + const translatedMarks = translateMarks(marksData); + + // Process translated mark data + translatedMarks.data.forEach(row => { + console.log('Translated row:', row); + }); +}; +``` + +## 🔍 Debugging + +### Check Available Translations +```jsx +const { translations, hasTranslation } = useTableauTranslation(); + +console.log('Available metrics:', translations.metrics); +console.log('Has translation for "Total Contractors":', hasTranslation("Total Contractors")); +``` + +### Verify Translation Results +```jsx +const { translateMetric, getCurrentLanguage } = useTableauTranslation(); + +const originalMetric = { name: "Total Contractors" }; +const translatedMetric = translateMetric(originalMetric); + +console.log('Language:', getCurrentLanguage()); +console.log('Original:', originalMetric.name); +console.log('Translated:', translatedMetric.name); +``` + +## ⚡ Performance Tips + +1. **Memoize translations** for large datasets +2. **Use batch translation** for arrays +3. **Check for translations** before processing +4. **Cache translated data** when possible + +```jsx +const { translateDataArray } = useTableauTranslation(); + +// Good: Batch translate +const translatedData = useMemo(() => + translateDataArray(largeDataset, 'metric'), + [largeDataset] +); + +// Avoid: Individual translations in loops +largeDataset.forEach(item => translateMetric(item)); // ❌ +``` + +## 🚀 Integration Examples + +### With Tableau Embedding API +```jsx +const TableauViz = () => { + const { translateMarks } = useTableauTranslation(); + + useEffect(() => { + const viz = document.getElementById('tableau-viz'); + + viz.addEventListener('markselectionchanged', (event) => { + event.detail.getMarksAsync().then(marks => { + const translatedMarks = translateMarks(marks); + console.log('Translated marks:', translatedMarks); + }); + }); + }, []); + + return
; +}; +``` + +### With React State +```jsx +const MetricsDashboard = () => { + const { translateMetric } = useTableauTranslation(); + const [metrics, setMetrics] = useState([]); + + const loadMetrics = async () => { + const data = await fetchMetrics(); + const translated = data.map(metric => translateMetric(metric)); + setMetrics(translated); + }; + + return ( +
+ {metrics.map(metric => ( +
{metric.name}
+ ))} +
+ ); +}; +``` + +This API provides a complete solution for translating all Tableau data in your application! 🎯 diff --git a/dev.sh b/dev.sh new file mode 100755 index 00000000..cad869ff --- /dev/null +++ b/dev.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Auto-switch to correct Node.js version and start dev server +echo "🚀 Starting Tableau Embedding Playbook Development Server" +echo "========================================================" + +# Check if nvm is available +if command -v nvm &> /dev/null; then + echo "📦 Switching to Node.js version specified in .nvmrc..." + nvm use +else + echo "⚠️ nvm not found. Please ensure you're using Node.js 18.17.0+" +fi + +echo "🔧 Starting development server on port 3001..." +npm run dev diff --git a/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 b/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 new file mode 100644 index 00000000..2b7778a8 --- /dev/null +++ b/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 @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDrWpY2DG +LS76oMng5F90DBAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKTlmV/tcXfrlxWG +UH+xdR0uofwQNjJczPEoc1hr9GHPAAAAoMgND3S4JelppliKkIVPGxVmdYm0BsLZofP+NT +W5i4Arv/y741ji++C8m55aKepwXP86Q+pvMOVq4PkLwsDce+1WhGlbOrzi6PXSLjjguwDp +CmPLzuqVmKbLodAdFAWPAvGD7EekpshoX2WcmHNTyHuoSKCVvzH6OFkX8jtepbPyECU0Qp +o/X+YzhZ2UJ/g7WEk3oFqm4ax0QV8uQx6FbS8= +-----END OPENSSH PRIVATE KEY----- diff --git a/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.pub b/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.pub new file mode 100644 index 00000000..052e0278 --- /dev/null +++ b/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.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKTlmV/tcXfrlxWGUH+xdR0uofwQNjJczPEoc1hr9GHP allisonreynoldsc@gmail.com diff --git a/package-lock.json b/package-lock.json index d6100d17..c7dbb7ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "langchain": "^0.3.15", "langsmith": "^0.3.7", "lucide-react": "^0.439.0", - "next": "^14.2.24", + "next": "^14.2.35", "next-auth": "^4.24.11", "nextra": "^3.0.6", "nextra-theme-docs": "^3.0.6", @@ -86,7 +86,7 @@ "@types/node": "^22.7.5", "autoprefixer": "^10.4.20", "eslint": "^8.57.0", - "eslint-config-next": "^14.2.24", + "eslint-config-next": "^14.2.35", "postcss": "^8.5.3", "tailwindcss": "^3.4.1", "typescript": "^5.6.2" @@ -3618,29 +3618,26 @@ } }, "node_modules/@next/env": { - "version": "14.2.24", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.24.tgz", - "integrity": "sha512-LAm0Is2KHTNT6IT16lxT+suD0u+VVfYNQqM+EJTKuFRRuY2z+zj01kueWXPCxbMBDt0B5vONYzabHGUNbZYAhA==", - "license": "MIT" + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==" }, "node_modules/@next/eslint-plugin-next": { - "version": "14.2.24", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.24.tgz", - "integrity": "sha512-FDL3qs+5DML0AJz56DCVr+KnFYivxeAX73En8QbPw9GjJZ6zbfvqDy+HrarHFzbsIASn7y8y5ySJ/lllSruNVQ==", + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.35.tgz", + "integrity": "sha512-Jw9A3ICz2183qSsqwi7fgq4SBPiNfmOLmTPXKvlnzstUwyvBrtySiY+8RXJweNAs9KThb1+bYhZh9XWcNOr2zQ==", "dev": true, - "license": "MIT", "dependencies": { "glob": "10.3.10" } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.24", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.24.tgz", - "integrity": "sha512-7Tdi13aojnAZGpapVU6meVSpNzgrFwZ8joDcNS8cJVNuP3zqqrLqeory9Xec5TJZR/stsGJdfwo8KeyloT3+rQ==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", + "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -3650,13 +3647,12 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.24", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.24.tgz", - "integrity": "sha512-lXR2WQqUtu69l5JMdTwSvQUkdqAhEWOqJEYUQ21QczQsAlNOW2kWZCucA6b3EXmPbcvmHB1kSZDua/713d52xg==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", + "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -3666,13 +3662,12 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.24", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.24.tgz", - "integrity": "sha512-nxvJgWOpSNmzidYvvGDfXwxkijb6hL9+cjZx1PVG6urr2h2jUqBALkKjT7kpfurRWicK6hFOvarmaWsINT1hnA==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", + "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -3682,13 +3677,12 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.24", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.24.tgz", - "integrity": "sha512-PaBgOPhqa4Abxa3y/P92F3kklNPsiFjcjldQGT7kFmiY5nuFn8ClBEoX8GIpqU1ODP2y8P6hio6vTomx2Vy0UQ==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", + "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -3698,13 +3692,12 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.24", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.24.tgz", - "integrity": "sha512-vEbyadiRI7GOr94hd2AB15LFVgcJZQWu7Cdi9cWjCMeCiUsHWA0U5BkGPuoYRnTxTn0HacuMb9NeAmStfBCLoQ==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", + "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -3714,13 +3707,12 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.24", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.24.tgz", - "integrity": "sha512-df0FC9ptaYsd8nQCINCzFtDWtko8PNRTAU0/+d7hy47E0oC17tI54U/0NdGk7l/76jz1J377dvRjmt6IUdkpzQ==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", + "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -3730,13 +3722,12 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.24", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.24.tgz", - "integrity": "sha512-ZEntbLjeYAJ286eAqbxpZHhDFYpYjArotQ+/TW9j7UROh0DUmX7wYDGtsTPpfCV8V+UoqHBPU7q9D4nDNH014Q==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", + "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -3746,13 +3737,12 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.24", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.24.tgz", - "integrity": "sha512-9KuS+XUXM3T6v7leeWU0erpJ6NsFIwiTFD5nzNg8J5uo/DMIPvCp3L1Ao5HjbHX0gkWPB1VrKoo/Il4F0cGK2Q==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", + "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", "cpu": [ "ia32" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -3762,13 +3752,12 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.24", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.24.tgz", - "integrity": "sha512-cXcJ2+x0fXQ2CntaE00d7uUH+u1Bfp/E0HsNQH79YiLaZE5Rbm7dZzyAYccn3uICM7mw+DxoMqEfGXZtF4Fgaw==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", + "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -11754,13 +11743,12 @@ } }, "node_modules/eslint-config-next": { - "version": "14.2.24", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.24.tgz", - "integrity": "sha512-9r1ujK++Pgpfixr5+DQ6rXDIQmSzuDbBlAQYMkJRMz9KWqovX7ESUTC0EAyBfOCl3ubkoeplw+aoXDuih3A8fw==", + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.35.tgz", + "integrity": "sha512-BpLsv01UisH193WyT/1lpHqq5iJ/Orfz9h/NOOlAmTUq4GY349PextQ62K4XpnaM9supeiEn3TaOTeQO07gURg==", "dev": true, - "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "14.2.24", + "@next/eslint-plugin-next": "14.2.35", "@rushstack/eslint-patch": "^1.3.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -16399,12 +16387,11 @@ "dev": true }, "node_modules/next": { - "version": "14.2.24", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.24.tgz", - "integrity": "sha512-En8VEexSJ0Py2FfVnRRh8gtERwDRaJGNvsvad47ShkC2Yi8AXQPXEA2vKoDJlGFSj5WE5SyF21zNi4M5gyi+SQ==", - "license": "MIT", + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", + "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", "dependencies": { - "@next/env": "14.2.24", + "@next/env": "14.2.35", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -16419,15 +16406,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.24", - "@next/swc-darwin-x64": "14.2.24", - "@next/swc-linux-arm64-gnu": "14.2.24", - "@next/swc-linux-arm64-musl": "14.2.24", - "@next/swc-linux-x64-gnu": "14.2.24", - "@next/swc-linux-x64-musl": "14.2.24", - "@next/swc-win32-arm64-msvc": "14.2.24", - "@next/swc-win32-ia32-msvc": "14.2.24", - "@next/swc-win32-x64-msvc": "14.2.24" + "@next/swc-darwin-arm64": "14.2.33", + "@next/swc-darwin-x64": "14.2.33", + "@next/swc-linux-arm64-gnu": "14.2.33", + "@next/swc-linux-arm64-musl": "14.2.33", + "@next/swc-linux-x64-gnu": "14.2.33", + "@next/swc-linux-x64-musl": "14.2.33", + "@next/swc-win32-arm64-msvc": "14.2.33", + "@next/swc-win32-ia32-msvc": "14.2.33", + "@next/swc-win32-x64-msvc": "14.2.33" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", diff --git a/package.json b/package.json index f1f02069..be72f234 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,14 @@ "version": "0.0.1", "description": "Tableau Embedded Playbook", "scripts": { - "dev": "next lint && next dev", + "dev": "next dev --port 3000", + "dev:fast": "next dev --port 3000 --turbo", + "dev:lint": "next lint && next dev --port 3000", "build": "next build", "export": "next export", "start": "next start", "lint": "next lint", + "lint:fix": "next lint --fix", "info": "next info", "demo": "next lint && next build && next start", "save": "git add pages public && git commit -m 'saving content (/public & /pages)'", @@ -73,7 +76,7 @@ "langchain": "^0.3.15", "langsmith": "^0.3.7", "lucide-react": "^0.439.0", - "next": "^14.2.24", + "next": "^14.2.35", "next-auth": "^4.24.11", "nextra": "^3.0.6", "nextra-theme-docs": "^3.0.6", @@ -103,7 +106,7 @@ "@types/node": "^22.7.5", "autoprefixer": "^10.4.20", "eslint": "^8.57.0", - "eslint-config-next": "^14.2.24", + "eslint-config-next": "^14.2.35", "postcss": "^8.5.3", "tailwindcss": "^3.4.1", "typescript": "^5.6.2" diff --git a/public/img/demos/Veriforce_contractor_2.svg b/public/img/demos/Veriforce_contractor_2.svg new file mode 100644 index 00000000..2a6e88cd --- /dev/null +++ b/public/img/demos/Veriforce_contractor_2.svg @@ -0,0 +1,713 @@ + + + + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KLUv/QBY9EEDGlsLqyiQRESYDwCAjVlRaVmzsFxZRSCd0ZlbiHru+FOVGZffnkkEAEAAAAAQNArH +CtQK8Wy/YJwRQCCPoeQ7z+FbNcutlACex5x4RsHyDQfwcA6r4Dx+5SccgI5VNb2IVTU9vwQQELGq +vlQsIRDOZVWr5QLijhYHuL33c+wl49od27Ctnai4zmOpJkkRmsBcp3B5hj/ZVpW1jbtw+KWCbTL0 +WFoeq12pmnbhmkq+WdqAFB3fsI3T8J1rpySNZ5QW5/Bc47JK87A8w5qc0zDJ0rwmIM5v7WvmF/9Y +z3VKU91wjcpvWDslaeyq7xzXYlqe81jdV3ElJbPxjII7rOSOc1qCxy/4MsOpupKq2RyOzDEHoQnA +952CYziOJF0DQL7ryM9xmWXRcgCx/DFFAMiceY7XMF/FlXAcyVVc8eZpjrlMe2J8tco5eYslq+JN +bA/o/DvnzHPMlaQuRC6nYBvTzTE2q6XpNV+S5Y/j+81XcUX0n/yxep3neJ1ZdBqnYDdfxZVRbLHk +OHKKiFaukkECatWu79X0fjigluhfaHqEaJa+LAfEFDyPcTyNZxSc37UtoAXzlMwiNIE4gJzDc7wZ +F1q3685Zsmi5hcs0/NHG0f+SNF91nteqF0XTmy/4ouUxHt8Zl0XLd86JVXVKpj2glii9YrIqtowQ +eczOY5geanQt5zAA5I3WwmOLdkpShNZH32LJG7McPx+KJh3nAtp5jtd8FVdMXJbTcp2Z4XgDauNH ++tDUwn+ap/j58SxVdJ2KOTl+y66AWEVL8JyWb/uOA9RaHOCO8fi2cbqWWfGtCYhn+1WnNFueU3Nd +q6CWouEZRmkBFnec4/Ct3fFK693HOZxzBuTyLKNkXP7cdT27JpV8v56WsGN2ndFiFhxj0XBF07yX +pAgtpyWg1gfcMF3LeUpmaKCJy+AcZk8V3ZrEL/4clytqJktlsgwAIVQmzvN4hjMquhXQVjELapkA +mKlCmtd8tglpPVNpvTTSLFr3WgCa1wq0rk20ng0IaV3rWRraaD2DQxitx7P9kjlg5mnxC76AWqKU +zzF4FbMjRMbQ6ABqmgJmeDwPyDn5narollUzXPU9w1UTl3FUE6f17rU+aqTmta/18Zve9/GXo+j7 ++XVN9F88/e8leYrd/P4kvQ9D8fRnSX5dOzt5nr38o//lKZZfz0bZrZo4Tcx69/33sIt97GQvu9nP +nna1r53tbXd71r3334de9KMnfelNf/rUq371rG+96/v3///wi3/85C+/+c+ffvWvn/3td38PffjD +MBTDMSTDMszM8AzTUA3XkA3b0A276MUvhqIojiIplqIpnmIqquIqsmIrumIf/fjHcBTHcSTHcjTH +c0xHdVxHdmxHd8w66clPhqRIjiRJlqRJnmRKquRKsmRLumQvffnLsBTLsSTLsjTLs0xLtVxLtmxL +t+ymN78ZmqI5mqRZmplpnmZqquZqsmZrumY//fnP8BTP8STP8jTP80xP9VxP9mxP9+ypT38apmI6 +pmRapmZ6pmmqpmvKpm3qpllXvfrVUBXVUSXVUjXVU01VVV1VVm1VV+2rX/8aruI6ruRaruZ6rumq +ruvKru3qrp317GdDVmRHlmRLNjPZk01ZlV1Zlm1Zl+2tb38btmI7tmRbtmZ7tmmrtmvLtm3rtt31 +7ndDV3RHl3RL13RPN3VVd3VZt3Vdl2zHVmzD1re9ddmWZdmVVdmUPVmTJdmRzUQ2ZD3b2XZl13VV +13Q913IlV3H9a19blVVV9VRLdVRD1atuyqZqeqZkKqY/7Wl7rud5lud4hmc/W3M1s9MszdEMzW62 +5VqeZVmOZVj60iVZUiVPsiRHMiQ96abiGLLiSopjG2ZsqJLh6P7W/OVvPZulZSfF8PvMTM3v3bNl +13L0Z2/LrC1D8rOiH13RHcVQzL7Yfvb8ps8+u6ZZmppkT90zc9lzPddTDU/TNE0zIz1bZqM7tiM7 +ruN6juZYhmMrsl/MUjIU/fpVU/zh/22mjm1nU1Lsoe8dzdyWXdVMTU+zVEmVHMXwe7WrXc266qZu +2qZtyqZsyqZruqZqqqZqmqZpmqZnaqZmWqZlWqZkSqZjOqZiKqZhGqY//alPfdpT92zP9mxP9szY +cz3VMz3T8zzN0zzLkzzHczzFMzz/+U9/9tM1XbM1WXM1VTM1U9M0S7M0SXM0R1M0Q/Ob3/RmN92y +LTO2XEu1TMuzNMuyJMuxFMuw/KUve9lLl2xJllxJlUzJkzTJkiRJkQzJT3qyk+3IjuqYjuZojuQ4 +juIYjtkf++iKrciKq6iKqWiKpUiKoyiKoejFLrphG7LhGqrhGZphGZLhGIphGP6wh+5vP/vVn/70 +n7/85B9/Jv7w+99//65n/epXr/rUn970pSe96EMf+u+9797tbV+72tN+9rObvexkH/vYxR72nzWy +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 +uR2LaoLI7ALwnBgbrD2dBQ== + + + diff --git a/public/img/themes/veriforce/slack-logo.png b/public/img/themes/veriforce/slack-logo.png new file mode 100644 index 00000000..c98be1ff Binary files /dev/null and b/public/img/themes/veriforce/slack-logo.png differ diff --git a/public/img/themes/veriforce/veriforce-logo-big.png b/public/img/themes/veriforce/veriforce-logo-big.png new file mode 100644 index 00000000..7b1abd4d Binary files /dev/null and b/public/img/themes/veriforce/veriforce-logo-big.png differ diff --git a/public/img/themes/veriforce/veriforce-logo.jpeg b/public/img/themes/veriforce/veriforce-logo.jpeg new file mode 100644 index 00000000..19ae43a8 Binary files /dev/null and b/public/img/themes/veriforce/veriforce-logo.jpeg differ diff --git a/public/img/themes/veriforce/veriforce-logo.png b/public/img/themes/veriforce/veriforce-logo.png new file mode 100644 index 00000000..3b849fe0 --- /dev/null +++ b/public/img/themes/veriforce/veriforce-logo.png @@ -0,0 +1,15 @@ + + + + + + + + + + + Veriforce + + + Contractor Risk Management + diff --git a/src/app/api/auth/[...nextauth]/options.ts b/src/app/api/auth/[...nextauth]/options.ts index 4710d2dc..45239594 100644 --- a/src/app/api/auth/[...nextauth]/options.ts +++ b/src/app/api/auth/[...nextauth]/options.ts @@ -160,13 +160,97 @@ export const authOptions: AuthOptions = { token.uaf = user.uaf || {}; token.tableau = user.tableau; token.rest_token = user.rest_token; + token.token_created = Date.now(); } + + // Check if tokens need refresh (JWT tokens expire in 9 minutes) + if (token.tableau && token.token_created) { + const tokenAge = Date.now() - (token.token_created as number); + const refreshThreshold = 8 * 60 * 1000; // 8 minutes in milliseconds + + if (tokenAge > refreshThreshold) { + console.log('🔄 JWT tokens are old, refreshing...'); + try { + // Import the refresh function here to avoid circular dependencies + const { handleJWT } = await import('@/models/Session/controller'); + + const jwt_client_id = process.env.TABLEAU_JWT_CLIENT_ID; + const embed_secret = process.env.TABLEAU_EMBED_JWT_SECRET; + const embed_secret_id = process.env.TABLEAU_EMBED_JWT_SECRET_ID; + const rest_secret = process.env.TABLEAU_REST_JWT_SECRET; + const rest_secret_id = process.env.TABLEAU_REST_JWT_SECRET_ID; + + const embed_scopes = [ + "tableau:views:embed", + "tableau:views:embed_authoring", + "tableau:insights:embed", + ]; + const embed_options = { + jwt_secret: embed_secret, + jwt_secret_id: embed_secret_id, + jwt_client_id + }; + + const rest_scopes = [ + "tableau:content:read", + "tableau:datasources:read", + "tableau:workbooks:read", + "tableau:projects:read", + "tableau:insights:read", + "tableau:metric_subscriptions:read", + "tableau:insight_definitions_metrics:read", + "tableau:insight_metrics:read", + "tableau:metrics:download", + ]; + const rest_options = { + jwt_secret: rest_secret, + jwt_secret_id: rest_secret_id, + jwt_client_id + }; + + const { credentials, rest_token, embed_token } = await handleJWT( + token.email as string, + embed_options, + embed_scopes, + rest_options, + rest_scopes, + (token.uaf as any) || {} + ); + + // Update token with fresh data + token.tableau = { + ...token.tableau, + username: credentials.username, + user_id: credentials.user_id, + embed_token, + rest_token, + rest_key: credentials.rest_key, + site_id: credentials.site_id, + site: credentials.site, + created: credentials.created, + expires: credentials.expiration + }; + token.rest_token = rest_token; + token.token_created = Date.now(); + + console.log('✅ JWT tokens refreshed successfully'); + } catch (error) { + console.error('❌ Failed to refresh JWT tokens:', error); + // Don't throw error, just log it and continue with existing tokens + } + } + } + return token; }, async session({ session, token }: { session: Session; token: JWT; }) { const customSession = session as CustomSession; if (customSession.user) { customSession.user.demo = token.demo as string; + // Add tableau data to session for client-side access + (customSession as any).tableau = token.tableau; + (customSession as any).rest_token = token.rest_token; + (customSession as any).embed_token = (token.tableau as any)?.embed_token; } return session; } diff --git a/src/app/api/auth/refresh-tokens/route.ts b/src/app/api/auth/refresh-tokens/route.ts new file mode 100644 index 00000000..612ba3fc --- /dev/null +++ b/src/app/api/auth/refresh-tokens/route.ts @@ -0,0 +1,124 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getToken } from 'next-auth/jwt'; +import { SessionModel } from '@/models'; +import { UAF } from '@/models/Session/controller'; + +interface TableauToken { + username?: string; + user_id?: string; + embed_token?: string; + rest_token?: string; + rest_key?: string; + site_id?: string; + site?: string; + created?: Date; + expires?: Date; + uaf?: UAF; +} + +export async function POST(request: NextRequest) { + try { + // Get the current JWT token from the session + const token = await getToken({ req: request }); + + if (!token?.tableau) { + return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); + } + + const tableau = token.tableau as TableauToken; + const userEmail = token.email; + + if (!userEmail) { + return NextResponse.json({ error: 'User email not found' }, { status: 400 }); + } + + console.log('🔄 Refreshing tokens for user:', userEmail); + + // Get the JWT configuration from environment variables + const jwt_client_id = process.env.TABLEAU_JWT_CLIENT_ID; + const embed_secret = process.env.TABLEAU_EMBED_JWT_SECRET; + const embed_secret_id = process.env.TABLEAU_EMBED_JWT_SECRET_ID; + const rest_secret = process.env.TABLEAU_REST_JWT_SECRET; + const rest_secret_id = process.env.TABLEAU_REST_JWT_SECRET_ID; + + if (!jwt_client_id || !embed_secret || !embed_secret_id || !rest_secret || !rest_secret_id) { + return NextResponse.json({ error: 'JWT configuration missing' }, { status: 500 }); + } + + // Client-safe Connected App scopes + const embed_scopes = [ + "tableau:views:embed", + "tableau:views:embed_authoring", + "tableau:insights:embed", + ]; + const embed_options = { + jwt_secret: embed_secret, + jwt_secret_id: embed_secret_id, + jwt_client_id + }; + + // Backend secured Connected App scopes + const rest_scopes = [ + "tableau:content:read", + "tableau:datasources:read", + "tableau:workbooks:read", + "tableau:projects:read", + "tableau:insights:read", + "tableau:metric_subscriptions:read", + "tableau:insight_definitions_metrics:read", + "tableau:insight_metrics:read", + "tableau:metrics:download", + ]; + const rest_options = { + jwt_secret: rest_secret, + jwt_secret_id: rest_secret_id, + jwt_client_id + }; + + // Create a new session with fresh tokens + const session = new SessionModel(userEmail); + await session.jwt(userEmail, embed_options, embed_scopes, rest_options, rest_scopes, tableau.uaf || {}); + + if (!session.authorized) { + return NextResponse.json({ error: 'Failed to refresh tokens' }, { status: 500 }); + } + + const { + username, + user_id, + embed_token, + rest_token, + rest_key, + site_id, + site, + created, + expires + } = session; + + // Return the refreshed token data + const refreshedTokens = { + tableau: { + username, + user_id, + embed_token, + rest_token, + rest_key, + site_id, + site, + created, + expires + } + }; + + console.log('✅ Tokens refreshed successfully for user:', userEmail); + + return NextResponse.json(refreshedTokens); + + } catch (error) { + console.error('❌ Error refreshing tokens:', error); + return NextResponse.json({ + error: 'Failed to refresh tokens', + details: error.message + }, { status: 500 }); + } +} diff --git a/src/app/api/tableau/views/route.js b/src/app/api/tableau/views/route.js new file mode 100644 index 00000000..8c975395 --- /dev/null +++ b/src/app/api/tableau/views/route.js @@ -0,0 +1,155 @@ +import { NextResponse } from 'next/server'; +import { getToken } from "next-auth/jwt"; + +export const dynamic = 'force-dynamic'; + +export async function GET(request) { + try { + // Get JWT token which contains the Tableau authentication data + const token = await getToken({ req: request }); + + if (!token?.tableau) { + return NextResponse.json({ error: 'Not authenticated or missing Tableau data' }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const workbookId = searchParams.get('workbookId'); + + // Extract Tableau data from JWT token + const { tableau } = token; + const siteId = tableau.site_id; + const jwtToken = tableau.rest_token; // This is the JWT REST token + + console.log('➡️ Backend: Received request for views:', { + siteId, + workbookId, + hasJWT: !!jwtToken, + userEmail: token.email + }); + + if (!siteId || !workbookId || !jwtToken) { + return NextResponse.json({ + error: 'Missing authentication data from JWT token or workbookId', + details: { hasSiteId: !!siteId, hasWorkbookId: !!workbookId, hasJWT: !!jwtToken } + }, { status: 400 }); + } + + // First, authenticate with Tableau using the JWT token + console.log('🔐 Authenticating with Tableau using JWT...'); + + const authUrl = `${process.env.NEXT_PUBLIC_ANALYTICS_DOMAIN}/api/3.26/auth/signin`; + const authBody = ` + + + + + + `; + + const authResponse = await fetch(authUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/xml', + 'Accept': 'application/xml' + }, + body: authBody + }); + + if (!authResponse.ok) { + const authErrorText = await authResponse.text(); + console.error('❌ Tableau Auth Error:', { + status: authResponse.status, + statusText: authResponse.statusText, + url: authResponse.url, + errorDetails: authErrorText + }); + return NextResponse.json({ + error: `Tableau Authentication Error: ${authResponse.status} ${authResponse.statusText}`, + details: authErrorText + }, { status: authResponse.status }); + } + + const authXml = await authResponse.text(); + console.log('✅ Tableau auth response:', authXml.substring(0, 500) + '...'); + + // Extract session token from auth response + const sessionTokenMatch = authXml.match(/]*token="([^"]*)"[^>]*>/); + if (!sessionTokenMatch || !sessionTokenMatch[1]) { + console.error('❌ Could not extract session token from auth response'); + return NextResponse.json({ + error: 'Failed to extract session token from Tableau auth response' + }, { status: 500 }); + } + + const sessionToken = sessionTokenMatch[1]; + console.log('🎯 Got Tableau session token:', sessionToken.substring(0, 20) + '...'); + + const tableauApiUrl = `${process.env.NEXT_PUBLIC_ANALYTICS_DOMAIN}/api/3.19/sites/${siteId}/workbooks/${workbookId}/views`; + + console.log('📡 Backend: Calling Tableau REST API for views:', tableauApiUrl); + + const response = await fetch(tableauApiUrl, { + headers: { + 'X-Tableau-Auth': sessionToken, + 'Content-Type': 'application/xml' + } + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('❌ Backend: Tableau Views API Error:', { + status: response.status, + statusText: response.statusText, + url: response.url, + errorDetails: errorText + }); + return NextResponse.json({ error: `Tableau Views API Error: ${response.status} ${response.statusText}`, details: errorText }, { status: response.status }); + } + + const xmlText = await response.text(); + console.log('✅ Backend: Got views XML response:', xmlText.substring(0, 500) + '...'); + + // Parse XML and extract views - use regex for server-side parsing + const parseViewsXML = (xmlString) => { + const views = []; + + // Extract view elements using regex + const viewMatches = xmlString.match(/]*\/?>|]*>.*?<\/view>/gs) || []; + + viewMatches.forEach(viewXml => { + // Extract attributes using regex + const getAttr = (name) => { + const match = viewXml.match(new RegExp(`${name}="([^"]*)"`, 'i')); + return match ? match[1] : ''; + }; + + const viewData = { + id: getAttr('id'), + name: getAttr('name'), + contentUrl: getAttr('contentUrl'), + workbookId: workbookId, + createdAt: getAttr('createdAt'), + updatedAt: getAttr('updatedAt'), + viewUrlName: getAttr('viewUrlName') + }; + + views.push(viewData); + }); + + return views; + }; + + const views = parseViewsXML(xmlText); + + console.log('✅ Backend: Returning views:', { + count: views.length, + workbookId + }); + + return NextResponse.json({ views }); + + } catch (error) { + console.error('❌ Backend: Error fetching views:', error); + return NextResponse.json({ error: 'Failed to fetch views', details: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/tableau/workbooks/route.js b/src/app/api/tableau/workbooks/route.js new file mode 100644 index 00000000..d429c55d --- /dev/null +++ b/src/app/api/tableau/workbooks/route.js @@ -0,0 +1,212 @@ +import { NextResponse } from 'next/server'; +import { getToken } from "next-auth/jwt"; + +export const dynamic = 'force-dynamic'; + +export async function GET(request) { + try { + // Get JWT token which contains the Tableau authentication data + const token = await getToken({ req: request }); + + if (!token?.tableau) { + return NextResponse.json({ error: 'Not authenticated or missing Tableau data' }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const pageSize = searchParams.get('pageSize') || '100'; + const pageNumber = searchParams.get('page') || '1'; + + // Extract Tableau data from JWT token + const { tableau } = token; + const siteId = tableau.site_id; + const userId = tableau.user_id; + const jwtToken = tableau.rest_token; // This is the JWT REST token + + console.log('➡️ Backend: Received request for workbooks:', { + siteId, + userId, + hasJWT: !!jwtToken, + pageSize, + pageNumber, + userEmail: token.email + }); + + if (!siteId || !userId || !jwtToken) { + return NextResponse.json({ + error: 'Missing authentication data from JWT token', + details: { hasSiteId: !!siteId, hasUserId: !!userId, hasJWT: !!jwtToken } + }, { status: 400 }); + } + + // First, authenticate with Tableau using the JWT token + console.log('🔐 Authenticating with Tableau using JWT...'); + + const authUrl = `${process.env.NEXT_PUBLIC_ANALYTICS_DOMAIN}/api/3.26/auth/signin`; + const authBody = ` + + + + + + `; + + const authResponse = await fetch(authUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/xml', + 'Accept': 'application/xml' + }, + body: authBody + }); + + if (!authResponse.ok) { + const authErrorText = await authResponse.text(); + console.error('❌ Tableau Auth Error:', { + status: authResponse.status, + statusText: authResponse.statusText, + url: authResponse.url, + errorDetails: authErrorText + }); + return NextResponse.json({ + error: `Tableau Authentication Error: ${authResponse.status} ${authResponse.statusText}`, + details: authErrorText + }, { status: authResponse.status }); + } + + const authXml = await authResponse.text(); + console.log('✅ Tableau auth response:', authXml.substring(0, 500) + '...'); + + // Extract session token from auth response + const sessionTokenMatch = authXml.match(/]*token="([^"]*)"[^>]*>/); + if (!sessionTokenMatch || !sessionTokenMatch[1]) { + console.error('❌ Could not extract session token from auth response'); + return NextResponse.json({ + error: 'Failed to extract session token from Tableau auth response' + }, { status: 500 }); + } + + const sessionToken = sessionTokenMatch[1]; + console.log('🎯 Got Tableau session token:', sessionToken.substring(0, 20) + '...'); + + // Use general workbooks endpoint + const url = `${process.env.NEXT_PUBLIC_ANALYTICS_DOMAIN}/api/3.26/sites/${siteId}/workbooks`; + + const params = new URLSearchParams({ + pageSize: pageSize.toString(), + pageNumber: pageNumber.toString(), + }); + + console.log('📡 Backend: Making API call to:', `${url}?${params}`); + + const response = await fetch(`${url}?${params}`, { + headers: { + 'X-Tableau-Auth': sessionToken, + 'Content-Type': 'application/xml', + 'Accept': 'application/xml' + } + }); + + if (!response.ok) { + console.error('❌ Backend: Tableau API Error:', { + status: response.status, + statusText: response.statusText, + url: response.url + }); + + const errorText = await response.text(); + console.error('❌ Backend: Error response:', errorText); + + return NextResponse.json( + { + error: `Tableau API Error: ${response.status} ${response.statusText}`, + details: errorText + }, + { status: response.status } + ); + } + + const xmlText = await response.text(); + console.log('✅ Backend: Got XML response:', xmlText.substring(0, 500) + '...'); + + // Parse XML and extract workbooks - use a simple XML parser for Node.js + const parseXML = (xmlString) => { + const workbooks = []; + + // Extract workbook elements using regex (simple approach for server-side) + const workbookMatches = xmlString.match(/]*>.*?<\/workbook>/gs) || []; + + workbookMatches.forEach(workbookXml => { + // Extract attributes using regex + const getAttr = (name) => { + const match = workbookXml.match(new RegExp(`${name}="([^"]*)"`, 'i')); + return match ? match[1] : ''; + }; + + // Extract project info + const projectMatch = workbookXml.match(/]*>/i); + const projectId = projectMatch ? projectMatch[0].match(/id="([^"]*)"/)?.[1] || '' : ''; + const projectName = projectMatch ? projectMatch[0].match(/name="([^"]*)"/)?.[1] || '' : ''; + + const workbook = { + id: getAttr('id'), + name: getAttr('name'), + description: getAttr('description'), + contentUrl: getAttr('contentUrl'), + webPageUrl: getAttr('webpageUrl'), + showTabs: getAttr('showTabs') === 'true', + size: parseInt(getAttr('size') || '0'), + createdAt: getAttr('createdAt'), + updatedAt: getAttr('updatedAt'), + encryptExtracts: getAttr('encryptExtracts') === 'true', + defaultViewId: getAttr('defaultViewId'), + projectId: projectId, + projectName: projectName + }; + + workbooks.push(workbook); + }); + + return workbooks; + }; + + const workbooks = parseXML(xmlText); + + // Get pagination info using regex + const paginationMatch = xmlText.match(/]*>/i); + let totalAvailable = 0; + let pageSizeAttr = 0; + let pageNumberAttr = 0; + + if (paginationMatch) { + const paginationXml = paginationMatch[0]; + totalAvailable = parseInt(paginationXml.match(/totalAvailable="([^"]*)"/)?.[1] || '0'); + pageSizeAttr = parseInt(paginationXml.match(/pageSize="([^"]*)"/)?.[1] || '0'); + pageNumberAttr = parseInt(paginationXml.match(/pageNumber="([^"]*)"/)?.[1] || '0'); + } + + console.log('✅ Backend: Returning workbooks:', { + count: workbooks.length, + totalAvailable, + pageSize: pageSizeAttr, + pageNumber: pageNumberAttr, + hasMore: parseInt(pageNumber) * parseInt(pageSize) < totalAvailable + }); + + return NextResponse.json({ + workbooks, + pagination: { + totalAvailable, + pageSize: pageSizeAttr, + pageNumber: pageNumberAttr, + hasMore: parseInt(pageNumber) * parseInt(pageSize) < totalAvailable + } + }); + + } catch (error) { + console.error('❌ Backend: Error in workbooks API:', error); + return NextResponse.json( + { error: 'Internal server error', details: error.message }, + { status: 500 } + ); + } +} diff --git a/src/app/api/test/session/route.js b/src/app/api/test/session/route.js new file mode 100644 index 00000000..6e55b3b1 --- /dev/null +++ b/src/app/api/test/session/route.js @@ -0,0 +1,41 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '../../auth/[...nextauth]/options'; + +export const dynamic = 'force-dynamic'; + +export async function GET(request) { + try { + // Get the session to see if user is authenticated + const session = await getServerSession(authOptions); + + console.log('🔍 Test API - Session check:', { + hasSession: !!session, + sessionKeys: session ? Object.keys(session) : [], + user: session?.user ? { + name: session.user.name, + email: session.user.email, + hasRestKey: !!session.user.rest_key, + hasEmbedToken: !!session.user.embed_token, + userId: session.user.user_id, + siteId: session.user.site_id + } : null + }); + + return NextResponse.json({ + authenticated: !!session, + session: session ? { + user: session.user, + expires: session.expires + } : null, + timestamp: new Date().toISOString() + }); + + } catch (error) { + console.error('❌ Test API Error:', error); + return NextResponse.json( + { error: 'Internal server error', details: error.message }, + { status: 500 } + ); + } +} diff --git a/src/app/api/user/route.js b/src/app/api/user/route.js index 804e7637..ba9b4504 100644 --- a/src/app/api/user/route.js +++ b/src/app/api/user/route.js @@ -26,8 +26,9 @@ export async function POST(req) { vectors, uaf, embed_token: tableau.embed_token, - // rest_token: tableau.rest_token, // only for debugging the JWT on the client + rest_key: tableau.rest_token, // REST API token for Tableau API calls user_id: tableau.user_id, + site_id: tableau.site_id, // Add site_id for API calls site: tableau.site, created: tableau.created, expires: tableau.expires diff --git a/src/app/demo/contractor/Home.jsx b/src/app/demo/contractor/Home.jsx new file mode 100644 index 00000000..7b20c831 --- /dev/null +++ b/src/app/demo/contractor/Home.jsx @@ -0,0 +1,710 @@ +"use client"; + +import { useState, useRef, useEffect } from 'react'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui"; +import { Metrics, TableauEmbed, LanguageSelector } from '@/components'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { + Shield, + Filter, + X, + MessageSquare, + Send +} from 'lucide-react'; + +export const description = "Demo Contractor Risk Management - Comprehensive safety and compliance tracking dashboard with real-time alerts and self-service analytics"; + +export const Home = () => { + const [selectedMarks, setSelectedMarks] = useState([]); + const [currentUser, setCurrentUser] = useState(null); + + // Message modal state + const [showMessageModal, setShowMessageModal] = useState(false); + const [editableMessage, setEditableMessage] = useState(''); + + // State filter - MULTIPLE SELECTION + const [selectedStates, setSelectedStates] = useState([]); // Array of selected states + const [tempSelectedStates, setTempSelectedStates] = useState([]); // Temporary selection before applying + const [showStateFilterPopup, setShowStateFilterPopup] = useState(false); + const [availableStates, setAvailableStates] = useState([]); // Will be populated from dashboard + const listenersSetupRef = useRef(false); + + // Get language context + const { t } = useLanguage(); + + // Get current user - re-fetch when component mounts or when session might change + useEffect(() => { + const fetchUser = async () => { + try { + const response = await fetch('/api/user', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (response.ok) { + const userData = await response.json(); + setCurrentUser(userData); + } + } catch (error) { + console.error('Error fetching user:', error); + } + }; + fetchUser(); + }, []); // Initial fetch + + // Also fetch user when the page becomes visible (user might have switched in another tab) + useEffect(() => { + const handleVisibilityChange = () => { + if (!document.hidden) { + const fetchUser = async () => { + try { + const response = await fetch('/api/user', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (response.ok) { + const userData = await response.json(); + setCurrentUser(userData); + console.log('User updated on visibility change:', userData); + } + } catch (error) { + console.error('Error fetching user on visibility change:', error); + } + }; + fetchUser(); + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + return () => document.removeEventListener('visibilitychange', handleVisibilityChange); + }, []); + + // Re-fetch user when window regains focus or storage changes (user might have switched) + useEffect(() => { + let isMounted = true; + + const fetchUser = async () => { + if (!isMounted) return; + + try { + const response = await fetch('/api/user', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (response.ok && isMounted) { + const userData = await response.json(); + // Only update if user actually changed + setCurrentUser(prev => { + if (prev?.email === userData?.email && prev?.name === userData?.name) { + return prev; // No change, return same reference + } + return userData; + }); + } + } catch (error) { + if (isMounted) { + console.error('Error fetching user:', error); + } + } + }; + + const handleFocus = () => fetchUser(); + const handleStorageChange = () => fetchUser(); + + // Listen for focus and storage changes + window.addEventListener('focus', handleFocus); + window.addEventListener('storage', handleStorageChange); + + // Also check periodically every 5 seconds (reduced from 2 to prevent excessive re-renders) + const interval = setInterval(fetchUser, 5000); + + return () => { + isMounted = false; + window.removeEventListener('focus', handleFocus); + window.removeEventListener('storage', handleStorageChange); + clearInterval(interval); + }; + }, []); + + // AGGRESSIVE scroll prevention - completely stop Tableau from causing any scroll + useEffect(() => { + const initialScrollY = window.scrollY; + + // IMMEDIATELY lock the page to prevent ANY scrolling + document.body.style.position = 'fixed'; + document.body.style.top = '0'; + document.body.style.left = '0'; + document.body.style.width = '100%'; + document.body.style.height = '100%'; + document.body.style.overflow = 'hidden'; + + // Prevent ALL focus events globally + const preventAllFocus = (e) => { + e.preventDefault(); + e.stopPropagation(); + if (e.target) { + e.target.blur(); + } + return false; + }; + + // Add aggressive focus prevention to ALL elements + const addFocusPrevention = () => { + // Prevent focus on all tableau elements + const tableauElements = document.querySelectorAll('tableau-viz, tableau-viz *'); + tableauElements.forEach(el => { + el.setAttribute('tabindex', '-1'); + el.style.outline = 'none'; + el.addEventListener('focus', preventAllFocus, true); + el.addEventListener('click', preventAllFocus, true); + }); + + // Prevent focus on all iframes + const iframes = document.querySelectorAll('iframe'); + iframes.forEach(iframe => { + iframe.setAttribute('tabindex', '-1'); + iframe.style.outline = 'none'; + iframe.addEventListener('focus', preventAllFocus, true); + iframe.addEventListener('load', preventAllFocus, true); + }); + }; + + // Apply focus prevention immediately and repeatedly + addFocusPrevention(); + const focusTimer = setInterval(addFocusPrevention, 100); + + const forceScrollPosition = () => { + if (window.scrollY !== initialScrollY) { + window.scrollTo(0, initialScrollY); + } + }; + + const scrollTimer = setInterval(forceScrollPosition, 50); + + // Prevent ALL scroll events + const preventScroll = (e) => { + e.preventDefault(); + e.stopPropagation(); + window.scrollTo(0, initialScrollY); + return false; + }; + + // Add scroll prevention to window and document + window.addEventListener('scroll', preventScroll, { passive: false, capture: true }); + window.addEventListener('wheel', preventScroll, { passive: false, capture: true }); + window.addEventListener('touchmove', preventScroll, { passive: false, capture: true }); + document.addEventListener('scroll', preventScroll, { passive: false, capture: true }); + + const releaseTimer = setTimeout(() => { + clearInterval(focusTimer); + clearInterval(scrollTimer); + + // Remove scroll prevention + window.removeEventListener('scroll', preventScroll, { capture: true }); + window.removeEventListener('wheel', preventScroll, { capture: true }); + window.removeEventListener('touchmove', preventScroll, { capture: true }); + document.removeEventListener('scroll', preventScroll, { capture: true }); + + // Restore body styles + document.body.style.position = ''; + document.body.style.top = ''; + document.body.style.left = ''; + document.body.style.width = ''; + document.body.style.height = ''; + document.body.style.overflow = ''; + + // Ensure we're at the top + window.scrollTo(0, 0); + }, 8000); + + return () => { + clearTimeout(releaseTimer); + clearInterval(focusTimer); + clearInterval(scrollTimer); + + // Cleanup + window.removeEventListener('scroll', preventScroll, { capture: true }); + window.removeEventListener('wheel', preventScroll, { capture: true }); + window.removeEventListener('touchmove', preventScroll, { capture: true }); + document.removeEventListener('scroll', preventScroll, { capture: true }); + + document.body.style.position = ''; + document.body.style.top = ''; + document.body.style.left = ''; + document.body.style.width = ''; + document.body.style.height = ''; + document.body.style.overflow = ''; + }; + }, []); + + // Apply filter when selectedStates changes + useEffect(() => { + const applyFilter = async () => { + const fieldName = 'State/Province'; + const filterValue = selectedStates.length === 0 ? [] : selectedStates; + + const applyFilterToViz = async () => { + const viz = document.getElementById('executiveSummaryViz'); + + 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 (activeSheet.sheetType === 'dashboard') { + const worksheets = activeSheet.worksheets; + for (const worksheet of worksheets) { + if (filterValue.length === 0) { + await worksheet.clearFilterAsync(fieldName); + } else { + await worksheet.applyFilterAsync(fieldName, filterValue, 'replace'); + } + } + } else { + if (filterValue.length === 0) { + await activeSheet.clearFilterAsync(fieldName); + } else { + await activeSheet.applyFilterAsync(fieldName, filterValue, 'replace'); + } + } + } catch (error) { + // Filter application failed silently + } + }; + + await applyFilterToViz(); + }; + + applyFilter(); + }, [selectedStates]); + + // Function to get available state values from the viz + const getStatesFromViz = async (viz) => { + if (!viz || !viz.workbook) { + return; + } + + try { + const activeSheet = viz.workbook.activeSheet; + const worksheets = activeSheet.worksheets || []; + + for (const worksheet of worksheets) { + try { + const dataTable = await worksheet.getSummaryDataAsync(); + const stateColumn = dataTable.columns?.find(col => + col.fieldName === 'State' || col.fieldName === 'State/Province' || col.fieldName?.toLowerCase().includes('state') + ); + + if (stateColumn) { + const stateColumnIndex = stateColumn.index; + const stateValues = new Set(); + + dataTable.data?.forEach(row => { + const cell = row[stateColumnIndex]; + if (cell && cell.value) { + stateValues.add(cell.value); + } + }); + + if (stateValues.size > 0) { + const sortedStates = Array.from(stateValues).sort(); + setAvailableStates(sortedStates); + return; + } + } + } catch (error) { + console.error('Error getting summary data from worksheet:', error); + } + } + } catch (error) { + console.error('Error getting states from viz:', error); + } + }; + + // Listen for mark selection events + useEffect(() => { + const handleMarkSelectionChanged = (markSelectionChangedEvent) => { + markSelectionChangedEvent.detail.getMarksAsync().then((marks) => { + const marksData = []; + + for (let markIndex = 0; markIndex < marks.data[0].data.length; markIndex++) { + const columns = marks.data[0].columns; + const obj = {}; + + for (let colIndex = 0; colIndex < columns.length; colIndex++) { + obj[columns[colIndex].fieldName] = marks.data[0].data[markIndex][colIndex].formattedValue; + } + + marksData.push(obj); + } + + setSelectedMarks(marksData); + }).catch((error) => { + console.error('Error getting selected marks:', error); + }); + }; + + // Prevent multiple setups + if (listenersSetupRef.current) { + return; + } + + const setupListeners = () => { + const executiveSummaryViz = document.getElementById('executiveSummaryViz'); + + if (!executiveSummaryViz) { + return null; + } + + // Check if listener already attached + if (executiveSummaryViz.hasAttribute('data-listener-attached')) { + return { executiveSummaryViz }; + } + + const handleFirstInteractive = async (event) => { + executiveSummaryViz.addEventListener('markselectionchanged', handleMarkSelectionChanged); + await getStatesFromViz(executiveSummaryViz); + }; + + executiveSummaryViz.addEventListener('firstinteractive', handleFirstInteractive); + executiveSummaryViz.setAttribute('data-listener-attached', 'true'); + + return { executiveSummaryViz, handleFirstInteractive }; + }; + + // Delay setup to ensure DOM elements are available + const timer = setTimeout(() => { + const result = setupListeners(); + if (result) { + listenersSetupRef.current = true; + // Store refs for cleanup + window._vizRefs = { ...result, handleMarkSelectionChanged }; + } + }, 1000); + + // Cleanup + return () => { + clearTimeout(timer); + if (window._vizRefs) { + const { executiveSummaryViz, handleFirstInteractive, handleMarkSelectionChanged } = window._vizRefs; + if (executiveSummaryViz) { + if (handleFirstInteractive) { + executiveSummaryViz.removeEventListener('firstinteractive', handleFirstInteractive); + } + executiveSummaryViz.removeEventListener('markselectionchanged', handleMarkSelectionChanged); + executiveSummaryViz.removeAttribute('data-listener-attached'); + } + delete window._vizRefs; + listenersSetupRef.current = false; + } + }; + }, []); + + return ( + <> + +
+
+ {/* Header Section */} +
+
+

+ {t.title} +

+

+ {t.subtitle} +

+
+
+ + +
+
+
+ System Healthy +
+
+
+
+ + {/* Pulse Metrics */} + + + {/* Filter Buttons - Visible at Top */} +
+ {/* State Filter Button */} + + + {/* Selected Marks Display - Clickable to open message modal */} + {selectedMarks.length > 0 && ( + + )} +
+ + {/* Main Dashboard */} +
+ {/* Executive Summary */} +
+ + + + + {t.executiveSummary} + + + {t.executiveSummaryDesc} + + + +
+ +
+
+
+
+ + +
+
+ + {/* State Filter Popup Modal */} + {showStateFilterPopup && ( +
setShowStateFilterPopup(false)}> +
e.stopPropagation()}> +
+

+ + Filter by State +

+ +
+ +
+ {availableStates.length === 0 ? ( +
+

Loading state options from dashboard...

+

Please wait for the dashboard to load

+
+ ) : ( + ['All States', ...availableStates].map((state) => { + const isAll = state === 'All States'; + const isSelected = isAll + ? tempSelectedStates.length === 0 + : tempSelectedStates.includes(state); + + return ( + + ); + }) + )} +
+ +
+ + +
+
+
+ )} + + {/* Message Modal - Shows when mark selection button is clicked */} + {showMessageModal && ( +
setShowMessageModal(false)}> +
e.stopPropagation()}> +
+

+ + Selected Data ({selectedMarks.length} mark{selectedMarks.length > 1 ? 's' : ''}) +

+ +
+ +
+
+ +