refactor(js): migrate JavaScript files to module directories
Move 47 JS files from static/{admin,vendor,shared}/js/ to their
respective module directories app/modules/*/static/*/js/:
- Orders: orders.js, order-detail.js
- Catalog: products.js (renamed from vendor-products.js), product-*.js
- Inventory: inventory.js (admin & vendor)
- Customers: customers.js, users.js, user-*.js
- Billing: billing-history.js, subscriptions.js, subscription-tiers.js,
billing.js, invoices.js, feature-store.js, upgrade-prompts.js
- Messaging: messages.js, notifications.js, email-templates.js
- Marketplace: marketplace*.js, letzshop*.js, onboarding.js
- Monitoring: monitoring.js, background-tasks.js, imports.js, logs.js
- Dev Tools: testing-*.js, code-quality-*.js
Update 39 templates to reference new module static paths using
url_for('{module}_static', path='...') pattern.
Files staying in static/ (platform core):
- admin: dashboard, login, platforms, vendors, companies, admin-users,
settings, components, init-alpine, module-config
- vendor: dashboard, login, profile, settings, team, media, init-alpine
- shared: api-client, utils, money, icons, log-config, vendor-selector,
media-picker
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
292
app/modules/dev_tools/static/admin/js/code-quality-dashboard.js
Normal file
292
app/modules/dev_tools/static/admin/js/code-quality-dashboard.js
Normal file
@@ -0,0 +1,292 @@
|
||||
// noqa: js-006 - async init pattern is safe, loadData has try/catch
|
||||
/**
|
||||
* Code Quality Dashboard Component
|
||||
* Manages the unified code quality dashboard page
|
||||
* Supports multiple validator types: architecture, security, performance
|
||||
*/
|
||||
|
||||
// Use centralized logger
|
||||
const codeQualityLog = window.LogConfig.createLogger('CODE-QUALITY');
|
||||
|
||||
function codeQualityDashboard() {
|
||||
return {
|
||||
// Extend base data
|
||||
...data(),
|
||||
|
||||
// Set current page for navigation
|
||||
currentPage: 'code-quality',
|
||||
|
||||
// Validator type selection
|
||||
selectedValidator: 'all', // 'all', 'architecture', 'security', 'performance'
|
||||
validatorTypes: ['architecture', 'security', 'performance'],
|
||||
|
||||
// Dashboard-specific data
|
||||
loading: false,
|
||||
scanning: false,
|
||||
scanDropdownOpen: false,
|
||||
error: null,
|
||||
successMessage: null,
|
||||
scanProgress: null, // Progress message during scan
|
||||
runningScans: [], // Track running scan IDs
|
||||
pollInterval: null, // Polling interval ID
|
||||
stats: {
|
||||
total_violations: 0,
|
||||
errors: 0,
|
||||
warnings: 0,
|
||||
info: 0,
|
||||
open: 0,
|
||||
assigned: 0,
|
||||
resolved: 0,
|
||||
ignored: 0,
|
||||
technical_debt_score: 100,
|
||||
trend: [],
|
||||
by_severity: {},
|
||||
by_rule: {},
|
||||
by_module: {},
|
||||
top_files: [],
|
||||
last_scan: null,
|
||||
validator_type: null,
|
||||
by_validator: {}
|
||||
},
|
||||
|
||||
async init() {
|
||||
// Guard against multiple initialization
|
||||
if (window._adminCodeQualityDashboardInitialized) return;
|
||||
window._adminCodeQualityDashboardInitialized = true;
|
||||
|
||||
try {
|
||||
// Check URL for validator_type parameter
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const urlValidator = urlParams.get('validator_type');
|
||||
if (urlValidator && this.validatorTypes.includes(urlValidator)) {
|
||||
this.selectedValidator = urlValidator;
|
||||
} else {
|
||||
// Ensure 'all' is explicitly set as default
|
||||
this.selectedValidator = 'all';
|
||||
}
|
||||
|
||||
await this.loadStats();
|
||||
|
||||
// Check for any running scans on page load
|
||||
await this.checkRunningScans();
|
||||
} catch (error) {
|
||||
codeQualityLog.error('Failed to initialize code quality dashboard:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async checkRunningScans() {
|
||||
try {
|
||||
const runningScans = await apiClient.get('/admin/code-quality/scans/running');
|
||||
if (runningScans && runningScans.length > 0) {
|
||||
this.scanning = true;
|
||||
this.runningScans = runningScans.map(s => s.id);
|
||||
this.updateProgressMessage(runningScans);
|
||||
this.startPolling();
|
||||
}
|
||||
} catch (err) {
|
||||
codeQualityLog.error('Failed to check running scans:', err);
|
||||
}
|
||||
},
|
||||
|
||||
updateProgressMessage(scans) {
|
||||
const runningScans = scans.filter(s => s.status === 'running' || s.status === 'pending');
|
||||
if (runningScans.length === 0) {
|
||||
this.scanProgress = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Show progress from the first running scan
|
||||
const firstRunning = runningScans.find(s => s.status === 'running');
|
||||
if (firstRunning && firstRunning.progress_message) {
|
||||
this.scanProgress = firstRunning.progress_message;
|
||||
} else {
|
||||
const validatorNames = runningScans.map(s => this.capitalizeFirst(s.validator_type));
|
||||
this.scanProgress = `Running ${validatorNames.join(', ')} scan${runningScans.length > 1 ? 's' : ''}...`;
|
||||
}
|
||||
},
|
||||
|
||||
async loadStats() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
// Build URL with validator_type filter if not 'all'
|
||||
let url = '/admin/code-quality/stats';
|
||||
if (this.selectedValidator !== 'all') {
|
||||
url += `?validator_type=${this.selectedValidator}`;
|
||||
}
|
||||
|
||||
const stats = await apiClient.get(url);
|
||||
this.stats = stats;
|
||||
} catch (err) {
|
||||
codeQualityLog.error('Failed to load stats:', err);
|
||||
this.error = err.message;
|
||||
|
||||
// Redirect to login if unauthorized
|
||||
if (err.message.includes('Unauthorized')) {
|
||||
window.location.href = '/admin/login';
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async selectValidator(validatorType) {
|
||||
if (this.selectedValidator !== validatorType) {
|
||||
this.selectedValidator = validatorType;
|
||||
await this.loadStats();
|
||||
|
||||
// Update URL without reload
|
||||
const url = new URL(window.location);
|
||||
if (validatorType === 'all') {
|
||||
url.searchParams.delete('validator_type');
|
||||
} else {
|
||||
url.searchParams.set('validator_type', validatorType);
|
||||
}
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
},
|
||||
|
||||
async runScan(validatorType = 'all') {
|
||||
this.scanning = true;
|
||||
this.error = null;
|
||||
this.successMessage = null;
|
||||
this.scanProgress = 'Queuing scan...';
|
||||
|
||||
try {
|
||||
// Determine which validators to run
|
||||
const validatorTypesToRun = validatorType === 'all'
|
||||
? this.validatorTypes
|
||||
: [validatorType];
|
||||
|
||||
const result = await apiClient.post('/admin/code-quality/scan', {
|
||||
validator_types: validatorTypesToRun
|
||||
});
|
||||
|
||||
// Store running scan IDs for polling
|
||||
this.runningScans = result.scans.map(s => s.id);
|
||||
|
||||
// Show initial status message
|
||||
const validatorNames = validatorTypesToRun.map(v => this.capitalizeFirst(v));
|
||||
this.scanProgress = `Running ${validatorNames.join(', ')} scan${validatorNames.length > 1 ? 's' : ''}...`;
|
||||
|
||||
// Start polling for completion
|
||||
this.startPolling();
|
||||
|
||||
} catch (err) {
|
||||
codeQualityLog.error('Failed to run scan:', err);
|
||||
this.error = err.message;
|
||||
this.scanning = false;
|
||||
this.scanProgress = null;
|
||||
|
||||
// Redirect to login if unauthorized
|
||||
if (err.message.includes('Unauthorized')) {
|
||||
window.location.href = '/admin/login';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
startPolling() {
|
||||
// Clear any existing polling
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
}
|
||||
|
||||
// Poll every 3 seconds
|
||||
this.pollInterval = setInterval(async () => {
|
||||
await this.pollScanStatus();
|
||||
}, 3000);
|
||||
},
|
||||
|
||||
stopPolling() {
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
this.pollInterval = null;
|
||||
}
|
||||
},
|
||||
|
||||
async pollScanStatus() {
|
||||
if (this.runningScans.length === 0) {
|
||||
this.stopPolling();
|
||||
this.scanning = false;
|
||||
this.scanProgress = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const runningScans = await apiClient.get('/admin/code-quality/scans/running');
|
||||
|
||||
// Update progress message from running scans
|
||||
this.updateProgressMessage(runningScans);
|
||||
|
||||
// Check if our scans have completed
|
||||
const stillRunning = this.runningScans.filter(id =>
|
||||
runningScans.some(s => s.id === id)
|
||||
);
|
||||
|
||||
if (stillRunning.length === 0) {
|
||||
// All scans completed - get final results
|
||||
await this.handleScanCompletion();
|
||||
} else {
|
||||
// Update running scans list
|
||||
this.runningScans = stillRunning;
|
||||
}
|
||||
} catch (err) {
|
||||
codeQualityLog.error('Failed to poll scan status:', err);
|
||||
}
|
||||
},
|
||||
|
||||
async handleScanCompletion() {
|
||||
this.stopPolling();
|
||||
|
||||
// Get results for all completed scans
|
||||
let totalViolations = 0;
|
||||
let totalErrors = 0;
|
||||
let totalWarnings = 0;
|
||||
const completedScans = [];
|
||||
|
||||
for (const scanId of this.runningScans) {
|
||||
try {
|
||||
const scan = await apiClient.get(`/admin/code-quality/scans/${scanId}/status`);
|
||||
completedScans.push(scan);
|
||||
totalViolations += scan.total_violations || 0;
|
||||
totalErrors += scan.errors || 0;
|
||||
totalWarnings += scan.warnings || 0;
|
||||
} catch (err) {
|
||||
codeQualityLog.error(`Failed to get scan ${scanId} results:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Format success message based on number of validators run
|
||||
if (completedScans.length > 1) {
|
||||
this.successMessage = `Scan completed: ${totalViolations} total violations found (${totalErrors} errors, ${totalWarnings} warnings) across ${completedScans.length} validators`;
|
||||
} else if (completedScans.length === 1) {
|
||||
const scan = completedScans[0];
|
||||
this.successMessage = `${this.capitalizeFirst(scan.validator_type)} scan completed: ${scan.total_violations} violations found (${scan.errors} errors, ${scan.warnings} warnings)`;
|
||||
} else {
|
||||
this.successMessage = 'Scan completed';
|
||||
}
|
||||
|
||||
// Reload stats after scan
|
||||
await this.loadStats();
|
||||
|
||||
// Reset scanning state
|
||||
this.scanning = false;
|
||||
this.scanProgress = null;
|
||||
this.runningScans = [];
|
||||
|
||||
// Clear success message after 5 seconds
|
||||
setTimeout(() => {
|
||||
this.successMessage = null;
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
async refresh() {
|
||||
await this.loadStats();
|
||||
},
|
||||
|
||||
capitalizeFirst(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
};
|
||||
}
|
||||
196
app/modules/dev_tools/static/admin/js/code-quality-violations.js
Normal file
196
app/modules/dev_tools/static/admin/js/code-quality-violations.js
Normal file
@@ -0,0 +1,196 @@
|
||||
// noqa: js-006 - async init pattern is safe, loadData has try/catch
|
||||
/**
|
||||
* Code Quality Violations List Component
|
||||
* Manages the violations list page with filtering and pagination
|
||||
*/
|
||||
|
||||
// ✅ Use centralized logger
|
||||
const codeQualityViolationsLog = window.LogConfig.createLogger('CODE-QUALITY');
|
||||
|
||||
function codeQualityViolations() {
|
||||
return {
|
||||
// Extend base data
|
||||
...data(),
|
||||
|
||||
// Set current page for navigation
|
||||
currentPage: 'code-quality',
|
||||
|
||||
// Violations-specific data
|
||||
loading: false,
|
||||
error: null,
|
||||
violations: [],
|
||||
pagination: {
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
pages: 0
|
||||
},
|
||||
filters: {
|
||||
validator_type: '',
|
||||
severity: '',
|
||||
status: '',
|
||||
rule_id: '',
|
||||
file_path: ''
|
||||
},
|
||||
|
||||
async init() {
|
||||
// Guard against multiple initialization
|
||||
if (window._codeQualityViolationsInitialized) {
|
||||
codeQualityViolationsLog.warn('Already initialized, skipping');
|
||||
return;
|
||||
}
|
||||
window._codeQualityViolationsInitialized = true;
|
||||
|
||||
// Load platform settings for rows per page
|
||||
if (window.PlatformSettings) {
|
||||
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
|
||||
}
|
||||
|
||||
// Load filters from URL params
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
this.filters.validator_type = params.get('validator_type') || '';
|
||||
this.filters.severity = params.get('severity') || '';
|
||||
this.filters.status = params.get('status') || '';
|
||||
this.filters.rule_id = params.get('rule_id') || '';
|
||||
this.filters.file_path = params.get('file_path') || '';
|
||||
|
||||
await this.loadViolations();
|
||||
},
|
||||
|
||||
async loadViolations() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
// Build query params
|
||||
const params = {
|
||||
page: this.pagination.page.toString(),
|
||||
page_size: this.pagination.per_page.toString()
|
||||
};
|
||||
|
||||
if (this.filters.validator_type) params.validator_type = this.filters.validator_type;
|
||||
if (this.filters.severity) params.severity = this.filters.severity;
|
||||
if (this.filters.status) params.status = this.filters.status;
|
||||
if (this.filters.rule_id) params.rule_id = this.filters.rule_id;
|
||||
if (this.filters.file_path) params.file_path = this.filters.file_path;
|
||||
|
||||
const data = await apiClient.get('/admin/code-quality/violations', params);
|
||||
|
||||
this.violations = data.violations;
|
||||
this.pagination.total = data.total;
|
||||
this.pagination.pages = data.total_pages;
|
||||
|
||||
// Update URL with current filters (without reloading)
|
||||
this.updateURL();
|
||||
} catch (err) {
|
||||
codeQualityViolationsLog.error('Failed to load violations:', err);
|
||||
this.error = err.message;
|
||||
|
||||
// Redirect to login if unauthorized
|
||||
if (err.message.includes('Unauthorized')) {
|
||||
window.location.href = '/admin/login';
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
applyFilters() {
|
||||
// Reset to page 1 when filters change
|
||||
this.pagination.page = 1;
|
||||
this.loadViolations();
|
||||
},
|
||||
|
||||
async nextPage() {
|
||||
if (this.pagination.page < this.pagination.pages) {
|
||||
this.pagination.page++;
|
||||
await this.loadViolations();
|
||||
}
|
||||
},
|
||||
|
||||
// Computed: Total number of pages
|
||||
get totalPages() {
|
||||
return this.pagination.pages;
|
||||
},
|
||||
|
||||
// Computed: Start index for pagination display
|
||||
get startIndex() {
|
||||
if (this.pagination.total === 0) return 0;
|
||||
return (this.pagination.page - 1) * this.pagination.per_page + 1;
|
||||
},
|
||||
|
||||
// Computed: End index for pagination display
|
||||
get endIndex() {
|
||||
const end = this.pagination.page * this.pagination.per_page;
|
||||
return end > this.pagination.total ? this.pagination.total : end;
|
||||
},
|
||||
|
||||
// Computed: Generate page numbers array with ellipsis
|
||||
get pageNumbers() {
|
||||
const pages = [];
|
||||
const totalPages = this.totalPages;
|
||||
const current = this.pagination.page;
|
||||
|
||||
if (totalPages <= 7) {
|
||||
// Show all pages if 7 or fewer
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
// Always show first page
|
||||
pages.push(1);
|
||||
|
||||
if (current > 3) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
// Show pages around current page
|
||||
const start = Math.max(2, current - 1);
|
||||
const end = Math.min(totalPages - 1, current + 1);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
if (current < totalPages - 2) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
// Always show last page
|
||||
pages.push(totalPages);
|
||||
}
|
||||
|
||||
return pages;
|
||||
},
|
||||
|
||||
async previousPage() {
|
||||
if (this.pagination.page > 1) {
|
||||
this.pagination.page--;
|
||||
await this.loadViolations();
|
||||
}
|
||||
},
|
||||
|
||||
goToPage(pageNum) {
|
||||
if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) {
|
||||
this.pagination.page = pageNum;
|
||||
this.loadViolations();
|
||||
}
|
||||
},
|
||||
|
||||
updateURL() {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (this.filters.validator_type) params.set('validator_type', this.filters.validator_type);
|
||||
if (this.filters.severity) params.set('severity', this.filters.severity);
|
||||
if (this.filters.status) params.set('status', this.filters.status);
|
||||
if (this.filters.rule_id) params.set('rule_id', this.filters.rule_id);
|
||||
if (this.filters.file_path) params.set('file_path', this.filters.file_path);
|
||||
|
||||
const newURL = params.toString()
|
||||
? `${window.location.pathname}?${params.toString()}`
|
||||
: window.location.pathname;
|
||||
|
||||
window.history.replaceState({}, '', newURL);
|
||||
}
|
||||
};
|
||||
}
|
||||
249
app/modules/dev_tools/static/admin/js/testing-dashboard.js
Normal file
249
app/modules/dev_tools/static/admin/js/testing-dashboard.js
Normal file
@@ -0,0 +1,249 @@
|
||||
// noqa: js-006 - async init pattern is safe, loadData has try/catch
|
||||
/**
|
||||
* Testing Dashboard Component
|
||||
* Manages the pytest testing dashboard page
|
||||
*/
|
||||
|
||||
// Use centralized logger
|
||||
const testingDashboardLog = window.LogConfig.createLogger('TESTING-DASHBOARD');
|
||||
|
||||
function testingDashboard() {
|
||||
return {
|
||||
// Extend base data
|
||||
...data(),
|
||||
|
||||
// Set current page for navigation
|
||||
currentPage: 'testing',
|
||||
|
||||
// Dashboard-specific data
|
||||
loading: false,
|
||||
running: false,
|
||||
collecting: false,
|
||||
error: null,
|
||||
successMessage: null,
|
||||
activeRunId: null,
|
||||
pollInterval: null,
|
||||
elapsedTime: 0,
|
||||
elapsedTimer: null,
|
||||
|
||||
// Statistics
|
||||
stats: {
|
||||
total_tests: 0,
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
errors: 0,
|
||||
skipped: 0,
|
||||
pass_rate: 0,
|
||||
duration_seconds: 0,
|
||||
coverage_percent: null,
|
||||
last_run: null,
|
||||
last_run_status: null,
|
||||
total_test_files: 0,
|
||||
collected_tests: 0,
|
||||
unit_tests: 0,
|
||||
integration_tests: 0,
|
||||
performance_tests: 0,
|
||||
system_tests: 0,
|
||||
last_collected: null,
|
||||
trend: [],
|
||||
by_category: {},
|
||||
top_failing: []
|
||||
},
|
||||
|
||||
// Recent runs
|
||||
runs: [],
|
||||
|
||||
async init() {
|
||||
// Guard against multiple initialization
|
||||
if (window._adminTestingDashboardInitialized) return;
|
||||
window._adminTestingDashboardInitialized = true;
|
||||
|
||||
try {
|
||||
testingDashboardLog.info('Initializing testing dashboard');
|
||||
await this.loadStats();
|
||||
await this.loadRuns();
|
||||
// Check if there's a running test and resume polling
|
||||
await this.checkForRunningTests();
|
||||
} catch (error) {
|
||||
testingDashboardLog.error('Failed to initialize testing dashboard:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async checkForRunningTests() {
|
||||
// Check if there's already a test running
|
||||
const runningRun = this.runs.find(r => r.status === 'running');
|
||||
if (runningRun) {
|
||||
testingDashboardLog.info('Found running test:', runningRun.id);
|
||||
this.running = true;
|
||||
this.activeRunId = runningRun.id;
|
||||
|
||||
// Calculate elapsed time from when the run started
|
||||
const startTime = new Date(runningRun.timestamp);
|
||||
this.elapsedTime = Math.floor((Date.now() - startTime.getTime()) / 1000);
|
||||
|
||||
// Start elapsed time counter
|
||||
this.elapsedTimer = setInterval(() => {
|
||||
this.elapsedTime++;
|
||||
}, 1000);
|
||||
|
||||
// Start polling for status
|
||||
this.pollInterval = setInterval(() => this.pollRunStatus(), 2000);
|
||||
}
|
||||
},
|
||||
|
||||
async loadStats() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const stats = await apiClient.get('/admin/tests/stats');
|
||||
this.stats = stats;
|
||||
testingDashboardLog.info('Stats loaded:', stats);
|
||||
} catch (err) {
|
||||
testingDashboardLog.error('Failed to load stats:', err);
|
||||
this.error = err.message;
|
||||
|
||||
// Redirect to login if unauthorized
|
||||
if (err.message.includes('Unauthorized')) {
|
||||
window.location.href = '/admin/login';
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadRuns() {
|
||||
try {
|
||||
const runs = await apiClient.get('/admin/tests/runs?limit=10');
|
||||
this.runs = runs;
|
||||
testingDashboardLog.info('Runs loaded:', runs.length);
|
||||
} catch (err) {
|
||||
testingDashboardLog.error('Failed to load runs:', err);
|
||||
// Don't set error - stats are more important
|
||||
}
|
||||
},
|
||||
|
||||
async runTests(testPath = 'tests') {
|
||||
this.running = true;
|
||||
this.error = null;
|
||||
this.successMessage = null;
|
||||
this.elapsedTime = 0;
|
||||
|
||||
testingDashboardLog.info('Starting tests:', testPath);
|
||||
|
||||
try {
|
||||
// Start the test run (returns immediately)
|
||||
const result = await apiClient.post('/admin/tests/run', {
|
||||
test_path: testPath
|
||||
});
|
||||
|
||||
testingDashboardLog.info('Test run started:', result);
|
||||
this.activeRunId = result.id;
|
||||
|
||||
// Start elapsed time counter
|
||||
this.elapsedTimer = setInterval(() => {
|
||||
this.elapsedTime++;
|
||||
}, 1000);
|
||||
|
||||
// Start polling for status
|
||||
this.pollInterval = setInterval(() => this.pollRunStatus(), 2000);
|
||||
|
||||
Utils.showToast('Test run started...', 'info');
|
||||
|
||||
} catch (err) {
|
||||
testingDashboardLog.error('Failed to start tests:', err);
|
||||
this.error = err.message;
|
||||
this.running = false;
|
||||
Utils.showToast('Failed to start tests: ' + err.message, 'error');
|
||||
|
||||
// Redirect to login if unauthorized
|
||||
if (err.message.includes('Unauthorized')) {
|
||||
window.location.href = '/admin/login';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async pollRunStatus() {
|
||||
if (!this.activeRunId) return;
|
||||
|
||||
try {
|
||||
const run = await apiClient.get(`/admin/tests/runs/${this.activeRunId}`);
|
||||
|
||||
if (run.status !== 'running') {
|
||||
// Test run completed
|
||||
this.stopPolling();
|
||||
|
||||
testingDashboardLog.info('Test run completed:', run);
|
||||
|
||||
// Format success message
|
||||
const status = run.status === 'passed' ? 'All tests passed!' : 'Tests completed with failures.';
|
||||
this.successMessage = `${status} ${run.passed}/${run.total_tests} passed (${run.pass_rate.toFixed(1)}%) in ${this.formatDuration(run.duration_seconds)}`;
|
||||
|
||||
// Reload stats and runs
|
||||
await this.loadStats();
|
||||
await this.loadRuns();
|
||||
|
||||
// Show toast notification
|
||||
Utils.showToast(this.successMessage, run.status === 'passed' ? 'success' : 'warning');
|
||||
|
||||
// Clear success message after 10 seconds
|
||||
setTimeout(() => {
|
||||
this.successMessage = null;
|
||||
}, 10000);
|
||||
}
|
||||
} catch (err) {
|
||||
testingDashboardLog.error('Failed to poll run status:', err);
|
||||
// Don't stop polling on error, might be transient
|
||||
}
|
||||
},
|
||||
|
||||
stopPolling() {
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
this.pollInterval = null;
|
||||
}
|
||||
if (this.elapsedTimer) {
|
||||
clearInterval(this.elapsedTimer);
|
||||
this.elapsedTimer = null;
|
||||
}
|
||||
this.running = false;
|
||||
this.activeRunId = null;
|
||||
},
|
||||
|
||||
async collectTests() {
|
||||
this.collecting = true;
|
||||
this.error = null;
|
||||
|
||||
testingDashboardLog.info('Collecting tests');
|
||||
|
||||
try {
|
||||
const result = await apiClient.post('/admin/tests/collect');
|
||||
testingDashboardLog.info('Collection completed:', result);
|
||||
|
||||
Utils.showToast(`Collected ${result.total_tests} tests from ${result.total_files} files`, 'success');
|
||||
|
||||
// Reload stats
|
||||
await this.loadStats();
|
||||
} catch (err) {
|
||||
testingDashboardLog.error('Failed to collect tests:', err);
|
||||
Utils.showToast('Failed to collect tests: ' + err.message, 'error');
|
||||
} finally {
|
||||
this.collecting = false;
|
||||
}
|
||||
},
|
||||
|
||||
async refresh() {
|
||||
await this.loadStats();
|
||||
await this.loadRuns();
|
||||
},
|
||||
|
||||
formatDuration(seconds) {
|
||||
if (seconds === null || seconds === undefined) return 'N/A';
|
||||
if (seconds < 1) return `${Math.round(seconds * 1000)}ms`;
|
||||
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const secs = Math.round(seconds % 60);
|
||||
return `${minutes}m ${secs}s`;
|
||||
}
|
||||
};
|
||||
}
|
||||
125
app/modules/dev_tools/static/admin/js/testing-hub.js
Normal file
125
app/modules/dev_tools/static/admin/js/testing-hub.js
Normal file
@@ -0,0 +1,125 @@
|
||||
// app/modules/dev_tools/static/admin/js/testing-hub.js
|
||||
|
||||
// ✅ Use centralized logger - ONE LINE!
|
||||
// Create custom logger for testing hub
|
||||
const testingLog = window.LogConfig.createLogger('TESTING-HUB');
|
||||
|
||||
/**
|
||||
* Testing Hub Alpine.js Component
|
||||
* Central hub for all test suites and QA tools
|
||||
*/
|
||||
function adminTestingHub() {
|
||||
return {
|
||||
// ✅ CRITICAL: Inherit base layout functionality
|
||||
...data(),
|
||||
|
||||
// ✅ CRITICAL: Set page identifier
|
||||
currentPage: 'testing',
|
||||
|
||||
// Test suites data
|
||||
testSuites: [
|
||||
{
|
||||
id: 'auth-flow',
|
||||
name: 'Authentication Flow',
|
||||
description: 'Test login, logout, token expiration, redirects, and protected route access.',
|
||||
url: '/admin/test/auth-flow',
|
||||
icon: 'lock-closed',
|
||||
color: 'blue',
|
||||
testCount: 6,
|
||||
features: [
|
||||
'Login with valid/invalid credentials',
|
||||
'Token expiration handling',
|
||||
'Protected route access & redirects',
|
||||
'localStorage state monitoring'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'vendors-users',
|
||||
name: 'Data Migration & CRUD',
|
||||
description: 'Test vendor and user creation, listing, editing, deletion, and data migration scenarios.',
|
||||
url: '/admin/test/vendors-users-migration',
|
||||
icon: 'database',
|
||||
color: 'orange',
|
||||
testCount: 10,
|
||||
features: [
|
||||
'Vendor CRUD operations',
|
||||
'User management & roles',
|
||||
'Data migration validation',
|
||||
'Form validation & error handling'
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
// Stats
|
||||
stats: {
|
||||
totalSuites: 2,
|
||||
totalTests: 16,
|
||||
coverage: 'Auth, CRUD',
|
||||
avgDuration: '< 5 min'
|
||||
},
|
||||
|
||||
// Loading state
|
||||
loading: false,
|
||||
|
||||
// ✅ CRITICAL: Proper initialization with guard
|
||||
async init() {
|
||||
testingLog.info('=== TESTING HUB INITIALIZING ===');
|
||||
|
||||
// Prevent multiple initializations
|
||||
if (window._testingHubInitialized) {
|
||||
testingLog.warn('Testing hub already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._testingHubInitialized = true;
|
||||
|
||||
// Calculate stats
|
||||
this.calculateStats();
|
||||
|
||||
testingLog.info('=== TESTING HUB INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate test statistics
|
||||
*/
|
||||
calculateStats() {
|
||||
this.stats.totalSuites = this.testSuites.length;
|
||||
this.stats.totalTests = this.testSuites.reduce((sum, suite) => sum + suite.testCount, 0);
|
||||
testingLog.debug('Stats calculated:', this.stats);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get color classes for test suite cards
|
||||
*/
|
||||
getColorClasses(color) {
|
||||
const colorMap = {
|
||||
blue: {
|
||||
gradient: 'from-blue-500 to-blue-600',
|
||||
button: 'bg-blue-600 hover:bg-blue-700'
|
||||
},
|
||||
orange: {
|
||||
gradient: 'from-orange-500 to-orange-600',
|
||||
button: 'bg-orange-600 hover:bg-orange-700'
|
||||
},
|
||||
green: {
|
||||
gradient: 'from-green-500 to-green-600',
|
||||
button: 'bg-green-600 hover:bg-green-700'
|
||||
},
|
||||
purple: {
|
||||
gradient: 'from-purple-500 to-purple-600',
|
||||
button: 'bg-purple-600 hover:bg-purple-700'
|
||||
}
|
||||
};
|
||||
return colorMap[color] || colorMap.blue;
|
||||
},
|
||||
|
||||
/**
|
||||
* Navigate to test suite
|
||||
*/
|
||||
goToTest(url) {
|
||||
testingLog.info('Navigating to test suite:', url);
|
||||
window.location.href = url;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
testingLog.info('Testing hub module loaded');
|
||||
Reference in New Issue
Block a user