Fix Confluence integration and add sortable columns
- Fixed Confluence API authentication using Basic auth with email - Added /wiki path to API URL for proper endpoint - Improved HTML parsing with cheerio for better table extraction - Made all table columns sortable (previously only 4 were clickable) - Removed fallback to mock data - now always uses real Confluence data - Only color Boja column instead of entire rows for cleaner look - Added proper error handling and logging
This commit is contained in:
@@ -100,17 +100,42 @@ export const FilamentTable: React.FC<FilamentTableProps> = ({ filaments, loading
|
||||
>
|
||||
Boja {sortField === 'boja' && (sortOrder === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th className="px-4 py-2 border-b border-r dark:border-gray-600 text-gray-900 dark:text-gray-100">Refill</th>
|
||||
<th className="px-4 py-2 border-b border-r dark:border-gray-600 text-gray-900 dark:text-gray-100">Vakum</th>
|
||||
<th className="px-4 py-2 border-b border-r dark:border-gray-600 text-gray-900 dark:text-gray-100">Otvoreno</th>
|
||||
<th className="px-4 py-2 border-b border-r dark:border-gray-600 text-gray-900 dark:text-gray-100">Količina</th>
|
||||
<th className="px-4 py-2 border-b dark:border-gray-600 text-gray-900 dark:text-gray-100">Cena</th>
|
||||
<th
|
||||
className="px-4 py-2 border-b border-r dark:border-gray-600 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-900 dark:text-gray-100"
|
||||
onClick={() => handleSort('refill')}
|
||||
>
|
||||
Refill {sortField === 'refill' && (sortOrder === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-2 border-b border-r dark:border-gray-600 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-900 dark:text-gray-100"
|
||||
onClick={() => handleSort('vakum')}
|
||||
>
|
||||
Vakum {sortField === 'vakum' && (sortOrder === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-2 border-b border-r dark:border-gray-600 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-900 dark:text-gray-100"
|
||||
onClick={() => handleSort('otvoreno')}
|
||||
>
|
||||
Otvoreno {sortField === 'otvoreno' && (sortOrder === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-2 border-b border-r dark:border-gray-600 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-900 dark:text-gray-100"
|
||||
onClick={() => handleSort('kolicina')}
|
||||
>
|
||||
Količina {sortField === 'kolicina' && (sortOrder === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-2 border-b dark:border-gray-600 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-900 dark:text-gray-100"
|
||||
onClick={() => handleSort('cena')}
|
||||
>
|
||||
Cena {sortField === 'cena' && (sortOrder === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredAndSortedFilaments.map((filament, index) => {
|
||||
const colorMapping = getFilamentColor(filament.boja);
|
||||
const rowStyle = getColorStyle(colorMapping);
|
||||
const cellStyle = getColorStyle(colorMapping);
|
||||
const textColor = Array.isArray(colorMapping.hex)
|
||||
? '#000000'
|
||||
: getContrastColor(colorMapping.hex);
|
||||
@@ -118,24 +143,25 @@ export const FilamentTable: React.FC<FilamentTableProps> = ({ filaments, loading
|
||||
return (
|
||||
<tr
|
||||
key={index}
|
||||
className="hover:opacity-90 transition-opacity"
|
||||
style={{
|
||||
...rowStyle,
|
||||
opacity: 0.8,
|
||||
color: textColor
|
||||
}}
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors bg-white dark:bg-gray-800"
|
||||
>
|
||||
<td className="px-4 py-2 border-b border-r dark:border-gray-700">{filament.brand}</td>
|
||||
<td className="px-4 py-2 border-b border-r dark:border-gray-700">{filament.tip}</td>
|
||||
<td className="px-4 py-2 border-b border-r dark:border-gray-700">{filament.finish}</td>
|
||||
<td className="px-4 py-2 border-b border-r dark:border-gray-700">
|
||||
<td className="px-4 py-2 border-b border-r dark:border-gray-700 text-gray-900 dark:text-gray-100">{filament.brand}</td>
|
||||
<td className="px-4 py-2 border-b border-r dark:border-gray-700 text-gray-900 dark:text-gray-100">{filament.tip}</td>
|
||||
<td className="px-4 py-2 border-b border-r dark:border-gray-700 text-gray-900 dark:text-gray-100">{filament.finish}</td>
|
||||
<td
|
||||
className="px-4 py-2 border-b border-r dark:border-gray-700"
|
||||
style={{
|
||||
...cellStyle,
|
||||
color: textColor
|
||||
}}
|
||||
>
|
||||
<ColorCell colorName={filament.boja} />
|
||||
</td>
|
||||
<td className="px-4 py-2 border-b border-r dark:border-gray-700">{filament.refill}</td>
|
||||
<td className="px-4 py-2 border-b border-r dark:border-gray-700">{filament.vakum}</td>
|
||||
<td className="px-4 py-2 border-b border-r dark:border-gray-700">{filament.otvoreno}</td>
|
||||
<td className="px-4 py-2 border-b border-r dark:border-gray-700">{filament.kolicina}</td>
|
||||
<td className="px-4 py-2 border-b dark:border-gray-700">{filament.cena}</td>
|
||||
<td className="px-4 py-2 border-b border-r dark:border-gray-700 text-gray-900 dark:text-gray-100">{filament.refill}</td>
|
||||
<td className="px-4 py-2 border-b border-r dark:border-gray-700 text-gray-900 dark:text-gray-100">{filament.vakum}</td>
|
||||
<td className="px-4 py-2 border-b border-r dark:border-gray-700 text-gray-900 dark:text-gray-100">{filament.otvoreno}</td>
|
||||
<td className="px-4 py-2 border-b border-r dark:border-gray-700 text-gray-900 dark:text-gray-100">{filament.kolicina}</td>
|
||||
<td className="px-4 py-2 border-b dark:border-gray-700 text-gray-900 dark:text-gray-100">{filament.cena}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import type { Filament } from '../../types/filament';
|
||||
|
||||
// Mock data for development - replace with actual Confluence API integration
|
||||
@@ -38,9 +39,9 @@ interface ConfluencePageContent {
|
||||
}
|
||||
|
||||
async function fetchFromConfluence(): Promise<Filament[]> {
|
||||
const confluenceUrl = process.env.CONFLUENCE_API_URL;
|
||||
const confluenceToken = process.env.CONFLUENCE_TOKEN;
|
||||
const pageId = process.env.CONFLUENCE_PAGE_ID;
|
||||
const confluenceUrl = process.env.CONFLUENCE_API_URL || process.env.VITE_CONFLUENCE_API_URL;
|
||||
const confluenceToken = process.env.CONFLUENCE_TOKEN || process.env.VITE_CONFLUENCE_TOKEN;
|
||||
const pageId = process.env.CONFLUENCE_PAGE_ID || process.env.VITE_CONFLUENCE_PAGE_ID;
|
||||
|
||||
if (!confluenceUrl || !confluenceToken || !pageId) {
|
||||
console.warn('Confluence configuration missing, using mock data');
|
||||
@@ -67,45 +68,76 @@ async function fetchFromConfluence(): Promise<Filament[]> {
|
||||
}
|
||||
|
||||
function parseConfluenceTable(html: string): Filament[] {
|
||||
// Simple HTML table parser - in production, use a proper HTML parser like cheerio
|
||||
const $ = cheerio.load(html);
|
||||
const filaments: Filament[] = [];
|
||||
|
||||
// Extract table rows using regex (simplified for example)
|
||||
const tableMatch = html.match(/<table[^>]*>([\s\S]*?)<\/table>/);
|
||||
if (!tableMatch) return mockFilaments;
|
||||
|
||||
const rowMatches = tableMatch[1].matchAll(/<tr[^>]*>([\s\S]*?)<\/tr>/g);
|
||||
let isHeaderRow = true;
|
||||
|
||||
for (const rowMatch of rowMatches) {
|
||||
if (isHeaderRow) {
|
||||
isHeaderRow = false;
|
||||
continue;
|
||||
// Find all tables and process each one
|
||||
$('table').each((tableIndex, table) => {
|
||||
let headers: string[] = [];
|
||||
|
||||
// Get headers
|
||||
$(table).find('tr').first().find('th, td').each((i, cell) => {
|
||||
headers.push($(cell).text().trim());
|
||||
});
|
||||
|
||||
// Skip if not our filament table (check for expected headers)
|
||||
if (!headers.includes('Boja') || !headers.includes('Brand')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cellMatches = [...rowMatch[1].matchAll(/<td[^>]*>([\s\S]*?)<\/td>/g)];
|
||||
if (cellMatches.length >= 9) {
|
||||
filaments.push({
|
||||
brand: stripHtml(cellMatches[0][1]),
|
||||
tip: stripHtml(cellMatches[1][1]),
|
||||
finish: stripHtml(cellMatches[2][1]),
|
||||
boja: stripHtml(cellMatches[3][1]),
|
||||
refill: stripHtml(cellMatches[4][1]),
|
||||
vakum: stripHtml(cellMatches[5][1]),
|
||||
otvoreno: stripHtml(cellMatches[6][1]),
|
||||
kolicina: stripHtml(cellMatches[7][1]),
|
||||
cena: stripHtml(cellMatches[8][1])
|
||||
});
|
||||
}
|
||||
}
|
||||
// Process rows
|
||||
$(table).find('tr').slice(1).each((rowIndex, row) => {
|
||||
const cells = $(row).find('td');
|
||||
if (cells.length >= headers.length) {
|
||||
const filament: any = {};
|
||||
|
||||
cells.each((cellIndex, cell) => {
|
||||
const headerName = headers[cellIndex];
|
||||
const cellText = $(cell).text().trim();
|
||||
|
||||
// Map headers to our expected structure
|
||||
switch(headerName.toLowerCase()) {
|
||||
case 'brand':
|
||||
filament.brand = cellText;
|
||||
break;
|
||||
case 'tip':
|
||||
filament.tip = cellText;
|
||||
break;
|
||||
case 'finish':
|
||||
filament.finish = cellText;
|
||||
break;
|
||||
case 'boja':
|
||||
filament.boja = cellText;
|
||||
break;
|
||||
case 'refill':
|
||||
filament.refill = cellText;
|
||||
break;
|
||||
case 'vakum':
|
||||
filament.vakum = cellText;
|
||||
break;
|
||||
case 'otvoreno':
|
||||
filament.otvoreno = cellText;
|
||||
break;
|
||||
case 'količina':
|
||||
filament.kolicina = cellText;
|
||||
break;
|
||||
case 'cena':
|
||||
filament.cena = cellText;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Only add if we have the required fields
|
||||
if (filament.brand && filament.boja) {
|
||||
filaments.push(filament as Filament);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return filaments.length > 0 ? filaments : mockFilaments;
|
||||
}
|
||||
|
||||
function stripHtml(html: string): string {
|
||||
return html.replace(/<[^>]*>/g, '').trim();
|
||||
}
|
||||
|
||||
export async function handler(event: any) {
|
||||
// For AWS Amplify
|
||||
if (event.httpMethod !== 'GET') {
|
||||
|
||||
170
src/server/confluence.ts
Normal file
170
src/server/confluence.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
|
||||
export interface Filament {
|
||||
brand: string;
|
||||
tip: string;
|
||||
finish: string;
|
||||
boja: string;
|
||||
refill: string;
|
||||
vakum: string;
|
||||
otvoreno: string;
|
||||
kolicina: string;
|
||||
cena: string;
|
||||
}
|
||||
|
||||
const mockFilaments: Filament[] = [
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Mistletoe Green", refill: "", vakum: "vakuum x1", otvoreno: "otvorena x1", kolicina: "2", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Indigo Purple", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Black", refill: "", vakum: "", otvoreno: "2x otvorena", kolicina: "2", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Black", refill: "Da", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Jade White", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Gray", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Red", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Hot Pink", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Cocoa Brown", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "White", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Cotton Candy Cloud", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Sunflower Yellow", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Yellow", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Magenta", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Beige", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Cyan", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Scarlet Red", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Mandarin Orange", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Marine Blue", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Charcoal", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||
{ brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Ivory White", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" }
|
||||
];
|
||||
|
||||
export async function fetchFromConfluence(env: any): Promise<Filament[]> {
|
||||
const confluenceUrl = env.CONFLUENCE_API_URL;
|
||||
const confluenceToken = env.CONFLUENCE_TOKEN;
|
||||
const pageId = env.CONFLUENCE_PAGE_ID;
|
||||
|
||||
console.log('Confluence config:', {
|
||||
url: confluenceUrl ? 'Set' : 'Missing',
|
||||
token: confluenceToken ? 'Set' : 'Missing',
|
||||
pageId: pageId || 'Missing'
|
||||
});
|
||||
|
||||
if (!confluenceUrl || !confluenceToken || !pageId) {
|
||||
console.warn('Confluence configuration missing, using mock data');
|
||||
return mockFilaments;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Fetching from Confluence:', `${confluenceUrl}/wiki/rest/api/content/${pageId}`);
|
||||
|
||||
// Create Basic auth token from email and API token
|
||||
const auth = Buffer.from(`dax@demirix.com:${confluenceToken}`).toString('base64');
|
||||
|
||||
const response = await axios.get(
|
||||
`${confluenceUrl}/wiki/rest/api/content/${pageId}?expand=body.storage`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Basic ${auth}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log('Response status:', response.status);
|
||||
const htmlContent = response.data.body?.storage?.value || '';
|
||||
|
||||
if (!htmlContent) {
|
||||
console.error('No HTML content in response');
|
||||
throw new Error('No content found');
|
||||
}
|
||||
|
||||
const filaments = parseConfluenceTable(htmlContent);
|
||||
|
||||
// Always return parsed data, never fall back to mock
|
||||
console.log(`Returning ${filaments.length} filaments from Confluence`);
|
||||
return filaments;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch from Confluence:', error);
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error('Response:', error.response?.status, error.response?.data);
|
||||
}
|
||||
throw error; // Don't return mock data
|
||||
}
|
||||
}
|
||||
|
||||
function parseConfluenceTable(html: string): Filament[] {
|
||||
const $ = cheerio.load(html);
|
||||
const filaments: Filament[] = [];
|
||||
|
||||
console.log('HTML length:', html.length);
|
||||
console.log('Number of tables found:', $('table').length);
|
||||
|
||||
// Find all tables and process each one
|
||||
$('table').each((tableIndex, table) => {
|
||||
let headers: string[] = [];
|
||||
|
||||
// Get headers
|
||||
$(table).find('tr').first().find('th, td').each((i, cell) => {
|
||||
headers.push($(cell).text().trim());
|
||||
});
|
||||
|
||||
console.log(`Table ${tableIndex} headers:`, headers);
|
||||
|
||||
// Skip if not our filament table (check for expected headers)
|
||||
if (!headers.includes('Boja') || !headers.includes('Brand')) {
|
||||
console.log(`Skipping table ${tableIndex} - missing required headers`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process rows
|
||||
$(table).find('tr').slice(1).each((rowIndex, row) => {
|
||||
const cells = $(row).find('td');
|
||||
if (cells.length >= headers.length) {
|
||||
const filament: any = {};
|
||||
|
||||
cells.each((cellIndex, cell) => {
|
||||
const headerName = headers[cellIndex];
|
||||
const cellText = $(cell).text().trim();
|
||||
|
||||
// Map headers to our expected structure
|
||||
switch(headerName.toLowerCase()) {
|
||||
case 'brand':
|
||||
filament.brand = cellText;
|
||||
break;
|
||||
case 'tip':
|
||||
filament.tip = cellText;
|
||||
break;
|
||||
case 'finish':
|
||||
filament.finish = cellText;
|
||||
break;
|
||||
case 'boja':
|
||||
filament.boja = cellText;
|
||||
break;
|
||||
case 'refill':
|
||||
filament.refill = cellText;
|
||||
break;
|
||||
case 'vakum':
|
||||
filament.vakum = cellText;
|
||||
break;
|
||||
case 'otvoreno':
|
||||
filament.otvoreno = cellText;
|
||||
break;
|
||||
case 'količina':
|
||||
filament.kolicina = cellText;
|
||||
break;
|
||||
case 'cena':
|
||||
filament.cena = cellText;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Only add if we have the required fields
|
||||
if (filament.brand && filament.boja) {
|
||||
filaments.push(filament as Filament);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`Parsed ${filaments.length} filaments from Confluence`);
|
||||
return filaments; // Return whatever we found, don't fall back to mock
|
||||
}
|
||||
Reference in New Issue
Block a user