diff --git a/dashboard/dashboard.css b/dashboard/dashboard.css new file mode 100644 index 0000000..86ef381 --- /dev/null +++ b/dashboard/dashboard.css @@ -0,0 +1,256 @@ +/* SpiderMonkey Dashboard Styles */ + +/* Import site fonts and variables */ +@font-face { + font-family: ZillaSlab-Regular; + src: url("/assets/fonts/ZillaSlab-Regular.ttf"); +} + +@font-face { + font-family: PT-Sans-S55; + src: url("/assets/fonts/pt-sans/PTS55F-webfont.woff"); +} + +/* Override styles to match site theme */ +.main-content { + font-family: PT-Sans-S55; + font-size: 1rem; +} + +h1 { + font-family: ZillaSlab-Regular; + font-size: 2.5rem; + margin-bottom: 1.5rem; +} + +h2 { + background-color: var(--color-primary); + color: var(--color-bg); + display: inline-block; + font-size: 1.5em; + padding: 0 10px; + margin-top: 2rem; + margin-bottom: 1rem; + font-family: ZillaSlab-Regular; +} + +h2 a { + color: var(--color-bg); + text-decoration: none; +} + +h2 a:hover { + color: var(--color-bg); + background-color: transparent; + text-decoration: underline; +} + +/* Quick Links Section */ +.quick-links { + background-color: var(--color-dim-bg); + padding: 12px 15px; + border-radius: 8px; + margin-bottom: 20px; + display: flex; + gap: 40px; + flex-wrap: wrap; +} + +.link-group { + flex: 1; + min-width: 250px; +} + +.link-group strong { + display: block; + margin-bottom: 8px; + color: var(--color-primary); + font-family: ZillaSlab-Regular; + font-size: 1rem; +} + +.link-group ul { + list-style-type: none; + padding-left: 0; + margin: 0; +} + +.link-group li { + margin-bottom: 4px; +} + +.link-group a { + color: var(--color-primary); + font-weight: 550; + padding: 2px; + font-size: 0.85rem; +} + +.link-group a:hover { + color: var(--color-bg); + background-color: var(--color-primary); + text-decoration: none; +} + +@media (max-width: 768px) { + .quick-links { + flex-direction: column; + gap: 20px; + } +} + +/* Bug Tables */ +.bug-table-container { + margin-bottom: 20px; + min-height: auto; +} + +.bug-table-container p { + margin: 10px 0; + color: var(--color-dim-primary); +} + +.bug-table { + width: 100%; + border-collapse: collapse; + font-family: PT-Sans-S55; + font-size: 0.85rem; +} + +.bug-table thead { + background-color: var(--color-primary); +} + +.bug-table thead th { + color: var(--color-bg); + font-family: ZillaSlab-Regular; + font-weight: 550; + font-size: 0.9rem; +} + +.bug-table thead th.sortable { + cursor: pointer; + user-select: none; +} + +.bug-table thead th.sortable:hover { + background-color: rgba(0, 0, 0, 0.1); +} + +.bug-table th, +.bug-table td { + padding: 8px; + text-align: left; + border-bottom: 1px solid var(--color-primary); +} + +.bug-table tbody tr { + cursor: pointer; +} + +.bug-table tbody tr:hover { + background-color: var(--color-dim-bg); +} + +.bug-table a { + color: var(--color-primary); + font-weight: 550; + padding: 2px; +} + +.bug-table a:hover { + color: var(--color-bg); + background-color: var(--color-primary); + text-decoration: none; +} + +.summary { + max-width: 500px; +} + +/* Priority Styling - removed coloring */ + +/* Severity Styling */ +.severity-s1 { + color: #ff4444; + font-weight: bold; +} + +.severity-s2 { + color: #ff4444; + font-weight: bold; +} + +.severity-s3 { + color: var(--color-primary); +} + +.severity-s4 { + color: var(--color-dim-primary); +} + +/* Loading and Error States */ +.loading { + text-align: center; + padding: 40px; + color: var(--color-dim-primary); + font-style: italic; + font-family: PT-Sans-S55; +} + +.error { + color: #ff4444; + text-align: center; + padding: 20px; + background-color: rgba(255, 68, 68, 0.1); + border-radius: 4px; + font-family: PT-Sans-S55; +} + +/* Last Updated */ +.last-updated { + text-align: right; + color: var(--color-dim-primary); + font-size: 0.85rem; + margin-top: 20px; + font-style: italic; + font-family: PT-Sans-S55; +} + +/* View More Button */ +.view-more-container { + text-align: center; + margin-top: 10px; +} + +.view-more-button { + background-color: var(--color-primary); + color: var(--color-bg); + border: none; + padding: 10px 20px; + font-family: ZillaSlab-Regular; + font-size: 1rem; + cursor: pointer; + border-radius: 4px; + transition: background-color 0.2s; +} + +.view-more-button:hover { + background-color: var(--color-dim-primary); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .bug-table { + font-size: 14px; + } + + .bug-table th, + .bug-table td { + padding: 8px; + } + + .summary { + max-width: 300px; + } +} \ No newline at end of file diff --git a/dashboard/dashboard.js b/dashboard/dashboard.js new file mode 100644 index 0000000..b791b75 --- /dev/null +++ b/dashboard/dashboard.js @@ -0,0 +1,497 @@ +// SpiderMonkey Dashboard - Bugzilla API Integration + +const BUGZILLA_API_BASE = 'https://bugzilla.mozilla.org/rest'; + +// Test API connectivity +async function testAPIConnection() { + try { + const response = await fetch(`${BUGZILLA_API_BASE}/version`); + const data = await response.json(); + return true; + } catch (error) { + console.error('Failed to connect to Bugzilla API:', error); + return false; + } +} + +// Define the SpiderMonkey components +const SPIDERMONKEY_COMPONENTS = [ + 'JavaScript Engine', + 'JavaScript Engine: JIT', + 'JavaScript Engine: GC', + 'JavaScript Engine: Internationalization API', + 'JavaScript Engine: Standard Library', + 'Javascript: Web Assembly', + 'js-ctypes' +]; + +// Helper function to format date +function formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); +} + +// Helper function to create bug link +function createBugLink(bugId) { + return `${bugId}`; +} + +// Helper function to format assignee +function formatAssignee(email) { + if (!email || email === 'nobody@mozilla.org') { + return 'Unassigned'; + } + // Extract username from email + return email.split('@')[0]; +} + +// Sort state tracking +const sortState = { + 'recently-opened': { column: 'severity', direction: 'asc' }, + 'recently-resolved': { column: 'severity', direction: 'asc' }, + 'high-severity': { column: 'severity', direction: 'asc' }, + 'assigned-open': { column: 'severity', direction: 'asc' } +}; + +// View state tracking (expanded or collapsed) +const viewState = { + 'recently-opened': { expanded: false, limit: 10 }, + 'recently-resolved': { expanded: false, limit: 10 }, + 'high-severity': { expanded: false, limit: null }, // no limit + 'assigned-open': { expanded: false, limit: 30 } // limit to 30 +}; + +// Helper function to compare values for sorting +function compareValues(a, b, column, direction) { + const severityOrder = { 'S1': 1, 'S2': 2, 'S3': 3, 'S4': 4, '--': 5, 'N/A': 5 }; + const priorityOrder = { 'P1': 1, 'P2': 2, 'P3': 3, 'P4': 4, 'P5': 5, '--': 6 }; + + let valA, valB; + + switch(column) { + case 'id': + valA = parseInt(a.id); + valB = parseInt(b.id); + break; + case 'summary': + valA = a.summary.toLowerCase(); + valB = b.summary.toLowerCase(); + break; + case 'assignee': + valA = formatAssignee(a.assigned_to).toLowerCase(); + valB = formatAssignee(b.assigned_to).toLowerCase(); + break; + case 'priority': + valA = priorityOrder[a.priority] || 999; + valB = priorityOrder[b.priority] || 999; + break; + case 'severity': + valA = severityOrder[a.severity] || 999; + valB = severityOrder[b.severity] || 999; + break; + } + + if (valA < valB) return direction === 'asc' ? -1 : 1; + if (valA > valB) return direction === 'asc' ? 1 : -1; + return 0; +} + +// Helper function to sort bugs by severity, priority, then assignee (default sort) +function sortBugs(bugs, tableId = null, column = null, direction = null) { + if (tableId && column) { + // Custom sort based on clicked column + return bugs.sort((a, b) => compareValues(a, b, column, direction)); + } else { + // Default sort + const severityOrder = { 'S1': 1, 'S2': 2, 'S3': 3, 'S4': 4, '--': 5, 'N/A': 5 }; + const priorityOrder = { 'P1': 1, 'P2': 2, 'P3': 3, 'P4': 4, 'P5': 5, '--': 6 }; + + return bugs.sort((a, b) => { + // First sort by severity + const severityA = severityOrder[a.severity] || 999; + const severityB = severityOrder[b.severity] || 999; + if (severityA !== severityB) { + return severityA - severityB; + } + + // Then by priority + const priorityA = priorityOrder[a.priority] || 999; + const priorityB = priorityOrder[b.priority] || 999; + if (priorityA !== priorityB) { + return priorityA - priorityB; + } + + // Finally by assignee + const assigneeA = formatAssignee(a.assigned_to); + const assigneeB = formatAssignee(b.assigned_to); + return assigneeA.localeCompare(assigneeB); + }); + } +} + +// Function to handle column header click +function sortTable(tableId, column) { + const state = sortState[tableId]; + + // Toggle direction if same column, otherwise default to ascending + if (state.column === column) { + state.direction = state.direction === 'asc' ? 'desc' : 'asc'; + } else { + state.column = column; + state.direction = 'asc'; + } + + // Re-fetch and display the data with new sort + const container = document.getElementById(tableId); + const bugs = window[`${tableId}Data`]; + if (bugs) { + const sortedBugs = sortBugs([...bugs], tableId, column, state.direction); + container.innerHTML = createBugTable(sortedBugs, tableId); + } +} + +// Function to toggle view more/less +function toggleViewMore(tableId) { + const state = viewState[tableId]; + state.expanded = !state.expanded; + + // Get the container + const container = document.getElementById(tableId); + + // Re-display the data with new view state + const bugs = window[`${tableId}Data`]; + if (bugs) { + const sortStateForTable = sortState[tableId]; + const sortedBugs = sortBugs([...bugs], tableId, sortStateForTable.column, sortStateForTable.direction); + container.innerHTML = createBugTable(sortedBugs, tableId); + + // Scroll to the heading anchor (the h2 link above the table) + const anchor = document.getElementById(`${tableId}-link`); + if (anchor) { + anchor.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + } else { + console.error(`No data found for table: ${tableId}`); + } +} + +// Helper function to create table HTML +function createBugTable(bugs, tableId = null) { + if (!bugs || bugs.length === 0) { + // Random celebration emojis + const celebrations = ['🎉', '🎊', '🥳', '🎈', '✨', '🌟', '🎆', '🎇', '🙌', '🤩']; + const randomEmoji = celebrations[Math.floor(Math.random() * celebrations.length)]; + return `

No bugs found ${randomEmoji}

`; + } + + // Get current sort state if tableId provided + const state = tableId ? sortState[tableId] : null; + + // Sort bugs + const sortedBugs = tableId && state + ? sortBugs([...bugs], tableId, state.column, state.direction) + : sortBugs(bugs); + + let html = ` + + + + `; + + // Add sortable headers if tableId is provided + if (tableId) { + const arrow = (col) => { + if (state.column === col) { + return state.direction === 'asc' ? ' ↑' : ' ↓'; + } + return ''; + }; + + html += ` + + + + + + `; + } else { + html += ` + + + + + + `; + } + + html += ` + + + + `; + + // Check if we need to limit the display + const vState = tableId ? viewState[tableId] : null; + const hasLimit = vState && vState.limit && sortedBugs.length > vState.limit; + const displayBugs = (hasLimit && !vState.expanded) + ? sortedBugs.slice(0, vState.limit) + : sortedBugs; + + displayBugs.forEach(bug => { + html += ` + + + + + + + + `; + }); + + html += ` + +
Bug${arrow('id')}Summary${arrow('summary')}Assignee${arrow('assignee')}Priority${arrow('priority')}Severity${arrow('severity')}BugSummaryAssigneePrioritySeverity
${createBugLink(bug.id)}${bug.summary}${formatAssignee(bug.assigned_to)}${bug.priority}${bug.severity}
+ `; + + // Add view more/less button if needed + if (hasLimit) { + const buttonText = vState.expanded + ? 'View Less' + : `View More (${sortedBugs.length - vState.limit} more)`; + html += ` +
+ +
+ `; + } + + return html; +} + +// Helper function to build Bugzilla query URL +function buildBugzillaQueryURL(params) { + const baseURL = 'https://bugzilla.mozilla.org/buglist.cgi'; + return `${baseURL}?${params}`; +} + +// Function to fetch recently opened bugs +async function fetchRecentlyOpenedBugs() { + // Build component parameter manually to handle multiple components + const componentParams = SPIDERMONKEY_COMPONENTS.map(c => `component=${encodeURIComponent(c)}`).join('&'); + + const params = new URLSearchParams({ + product: 'Core', + chfield: '[Bug creation]', + chfieldfrom: '-30d', + chfieldto: 'Now', + order: 'opendate DESC', + include_fields: 'id,summary,assigned_to,priority,severity,creation_time' + }); + + // Set the query link + const queryParams = new URLSearchParams({ + product: 'Core', + chfield: '[Bug creation]', + chfieldfrom: '-30d', + chfieldto: 'Now', + order: 'opendate DESC' + }); + const queryURL = buildBugzillaQueryURL(`${queryParams}&${componentParams}`); + document.getElementById('recently-opened-link').href = queryURL; + + const url = `${BUGZILLA_API_BASE}/bug?${params}&${componentParams}`; + + try { + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + // Store data for re-sorting + window['recently-openedData'] = data.bugs; + + const container = document.getElementById('recently-opened'); + container.innerHTML = createBugTable(data.bugs, 'recently-opened'); + } catch (error) { + console.error('Error fetching recently opened bugs:', error); + document.getElementById('recently-opened').innerHTML = `

Error loading recently opened bugs: ${error.message}

`; + } +} + +// Function to fetch recently resolved bugs +async function fetchRecentlyResolvedBugs() { + // Build component parameter manually to handle multiple components + const componentParams = SPIDERMONKEY_COMPONENTS.map(c => `component=${encodeURIComponent(c)}`).join('&'); + + const params = new URLSearchParams({ + product: 'Core', + chfield: 'resolution', + chfieldfrom: '-30d', + chfieldto: 'Now', + order: 'changeddate DESC', + include_fields: 'id,summary,assigned_to,priority,severity,cf_last_resolved' + }); + + // Add resolutions manually + const resolutions = ['FIXED', 'INVALID', 'WONTFIX', 'DUPLICATE', 'WORKSFORME']; + const resolutionParams = resolutions.map(r => `resolution=${r}`).join('&'); + + // Set the query link + const queryParams = new URLSearchParams({ + product: 'Core', + chfield: 'resolution', + chfieldfrom: '-30d', + chfieldto: 'Now', + order: 'changeddate DESC' + }); + const queryURL = buildBugzillaQueryURL(`${queryParams}&${componentParams}&${resolutionParams}`); + document.getElementById('recently-resolved-link').href = queryURL; + + const url = `${BUGZILLA_API_BASE}/bug?${params}&${componentParams}&${resolutionParams}`; + + try { + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + // Store data for re-sorting + window['recently-resolvedData'] = data.bugs; + + const container = document.getElementById('recently-resolved'); + container.innerHTML = createBugTable(data.bugs, 'recently-resolved'); + } catch (error) { + console.error('Error fetching recently resolved bugs:', error); + document.getElementById('recently-resolved').innerHTML = `

Error loading recently resolved bugs: ${error.message}

`; + } +} + +// Function to fetch high severity bugs +async function fetchHighSeverityBugs() { + // Build component parameter manually to handle multiple components + const componentParams = SPIDERMONKEY_COMPONENTS.map(c => `component=${encodeURIComponent(c)}`).join('&'); + + const params = new URLSearchParams({ + product: 'Core', + resolution: '---', + include_fields: 'id,summary,assigned_to,priority,severity' + }); + + // Add severities manually + const severityParams = 'bug_severity=S1&bug_severity=S2'; + + // Set the query link + const queryParams = new URLSearchParams({ + product: 'Core', + resolution: '---' + }); + const queryURL = buildBugzillaQueryURL(`${queryParams}&${componentParams}&${severityParams}`); + document.getElementById('high-severity-link').href = queryURL; + + const url = `${BUGZILLA_API_BASE}/bug?${params}&${componentParams}&${severityParams}`; + + try { + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + // Store data for re-sorting + window['high-severityData'] = data.bugs; + + const container = document.getElementById('high-severity'); + container.innerHTML = createBugTable(data.bugs, 'high-severity'); + } catch (error) { + console.error('Error fetching high severity bugs:', error); + document.getElementById('high-severity').innerHTML = `

Error loading high severity bugs: ${error.message}

`; + } +} + +// Function to fetch assigned open bugs +async function fetchAssignedOpenBugs() { + // Build component parameter manually to handle multiple components + const componentParams = SPIDERMONKEY_COMPONENTS.map(c => `component=${encodeURIComponent(c)}`).join('&'); + + const params = new URLSearchParams({ + product: 'Core', + resolution: '---', + emailtype1: 'notequals', + email1: 'nobody@mozilla.org', + emailassigned_to1: '1', + include_fields: 'id,summary,assigned_to,priority,severity' + }); + + // Set the query link + const queryParams = new URLSearchParams({ + product: 'Core', + resolution: '---', + emailtype1: 'notequals', + email1: 'nobody@mozilla.org', + emailassigned_to1: '1' + }); + const queryURL = buildBugzillaQueryURL(`${queryParams}&${componentParams}`); + document.getElementById('assigned-open-link').href = queryURL; + + const url = `${BUGZILLA_API_BASE}/bug?${params}&${componentParams}`; + + try { + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + // Store data for re-sorting + window['assigned-openData'] = data.bugs; + + const container = document.getElementById('assigned-open'); + container.innerHTML = createBugTable(data.bugs, 'assigned-open'); + } catch (error) { + console.error('Error fetching assigned open bugs:', error); + document.getElementById('assigned-open').innerHTML = `

Error loading assigned open bugs: ${error.message}

`; + } +} + +// Make functions available globally for onclick handlers +window.sortTable = sortTable; +window.toggleViewMore = toggleViewMore; + +// Initialize dashboard on page load +document.addEventListener('DOMContentLoaded', async () => { + // Check if we're running locally (file://) which might have CORS issues + if (window.location.protocol === 'file:') { + console.warn('Running from file:// protocol. You may experience CORS issues. Consider running a local web server.'); + } + + // Test API connection first + const apiWorking = await testAPIConnection(); + if (!apiWorking) { + console.error('Cannot connect to Bugzilla API. This might be a CORS issue if running locally.'); + } + + fetchRecentlyOpenedBugs(); + fetchRecentlyResolvedBugs(); + fetchHighSeverityBugs(); + fetchAssignedOpenBugs(); + + // Add last updated timestamp + const now = new Date(); + const footer = document.createElement('div'); + footer.className = 'last-updated'; + footer.textContent = `Last updated: ${now.toLocaleString()}`; + document.querySelector('.bug-table-container:last-of-type').after(footer); +}); \ No newline at end of file diff --git a/dashboard/index.md b/dashboard/index.md new file mode 100644 index 0000000..33cc858 --- /dev/null +++ b/dashboard/index.md @@ -0,0 +1,62 @@ +--- +layout: default +--- + + + +# SpiderMonkey Dashboard + +## Quick Links + + + +## High Severity Bugs (S1 & S2) + +
+
Loading...
+
+ +## Recently Opened Bugs + +
+
Loading...
+
+ +## Recently Resolved Bugs + +
+
Loading...
+
+ +## Assigned Open Bugs + +
+
Loading...
+
+ + \ No newline at end of file