From 1023390e2ecc7e554b7b30199e44eee814d75a02 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Fri, 17 Jul 2026 09:21:48 +0300 Subject: [PATCH] fix(ux): add CSS so usage-history sparkline is visible in Opportunities table (#1417) The SVG inline sparkline rendered by renderUsageSparkline() had no matching CSS for .usage-sparkline or .usage-sparkline-cell, causing the SVG to be invisible or misaligned in some browsers. Add two rules to charts.css: - .usage-sparkline-cell: center-align with min-width so the column stays stable as data loads - .usage-sparkline: display:block with explicit color (--cudly-primary) so the SVG stroke/fill values inherited via currentColor are always visible regardless of ancestor color overrides Add integration tests verifying the SVG polyline appears in the rendered table HTML when a recommendation carries non-empty usage_history, and that the em-dash fallback is used when usage_history is null. Closes #1417 --- .../src/__tests__/recommendations.test.ts | 85 +++++++++++++++++++ frontend/src/styles/charts.css | 17 ++++ 2 files changed, 102 insertions(+) diff --git a/frontend/src/__tests__/recommendations.test.ts b/frontend/src/__tests__/recommendations.test.ts index 3ecae87f1..8a507ef15 100644 --- a/frontend/src/__tests__/recommendations.test.ts +++ b/frontend/src/__tests__/recommendations.test.ts @@ -7351,6 +7351,91 @@ describe('renderUsageSparkline (issue #239)', () => { }); }); +// ============================================================================ +// Issue #1417: sparkline integration — SVG renders inside the table +// ============================================================================ +// The unit tests above verify renderUsageSparkline in isolation. This block +// confirms the SVG actually appears inside the recommendations table HTML when +// a recommendation carries non-empty usage_history data. +describe('Issue #1417: usage-history sparkline renders in recommendations table', () => { + beforeEach(() => { + document.body.innerHTML = ` +
+
+
+
+ + `; + jest.clearAllMocks(); + jest.useFakeTimers(); + (state.getHiddenColumns as jest.Mock).mockReturnValue(new Set()); + (state.getRecommendationsSort as jest.Mock).mockReturnValue({ column: 'savings', direction: 'desc' }); + (state.getRecommendationsColumnFilters as jest.Mock).mockReturnValue({}); + (state.getCurrentUser as jest.Mock).mockReturnValue({ id: 'u-admin', email: 'admin@example.com', groups: ['00000000-0000-5000-8000-000000000001'] }); + (recsApi.getRecommendationsFreshness as jest.Mock).mockResolvedValue({ + last_collected_at: new Date(Date.now() - 60 * 60 * 1000).toISOString(), + last_collection_error: null, + }); + (recsApi.refreshRecommendations as jest.Mock).mockResolvedValue({}); + }); + afterEach(() => { + jest.useRealTimers(); + }); + + test('SVG polyline appears in table row when rec has 7-day usage_history', async () => { + (api.getRecommendations as jest.Mock).mockResolvedValue({ + summary: { total_count: 1, total_monthly_savings: 100, total_upfront_cost: 500, avg_payback_months: 5 }, + recommendations: [{ + id: 'rec-sparkline', provider: 'aws', cloud_account_id: 'acct-1', + service: 'ec2', resource_type: 't3.medium', region: 'us-east-1', + count: 1, term: 1, payment: 'all-upfront', savings: 100, upfront_cost: 500, + usage_history: [80, 85, 90, 70, 95, 100, 60], + }], + regions: [], + }); + + await loadRecommendations(); + + const list = document.getElementById('recommendations-list'); + const html = list?.innerHTML ?? ''; + // The sparkline SVG must be present in the rendered table. + expect(html).toContain('class="usage-sparkline"'); + // It must contain a polyline (7 points -> polyline, not circle). + expect(html).toContain(' + expect(html).toContain('usage-sparkline-cell" title="RI coverage last 7 days"> { + (api.getRecommendations as jest.Mock).mockResolvedValue({ + summary: {}, + recommendations: [{ + id: 'rec-no-sparkline', provider: 'aws', cloud_account_id: 'acct-1', + service: 'ec2', resource_type: 't3.medium', region: 'us-east-1', + count: 1, term: 1, payment: 'all-upfront', savings: 100, upfront_cost: 500, + usage_history: null, + }], + regions: [], + }); + + await loadRecommendations(); + + const list = document.getElementById('recommendations-list'); + const html = list?.innerHTML ?? ''; + // Null usage_history must degrade to the em-dash fallback, not an SVG. + expect(html).not.toContain('class="usage-sparkline"'); + expect(html).toContain('class="usage-sparkline-cell"'); + }); +}); + // ============================================================================ // Issue #163: Column filter localStorage persistence // ============================================================================ diff --git a/frontend/src/styles/charts.css b/frontend/src/styles/charts.css index acd465c82..732e9e17a 100644 --- a/frontend/src/styles/charts.css +++ b/frontend/src/styles/charts.css @@ -75,3 +75,20 @@ color: var(--cudly-text-muted); margin-top: 0.25rem; } + +/* Issue #1417 / #239: per-row usage-history sparkline in Opportunities table. + The SVG uses stroke="currentColor" and fill="currentColor" so an explicit + color ensures the sparkline is always visible regardless of ancestor color + overrides. display:block removes the inline descent gap so the 20px SVG + sits flush within the table cell. */ +.usage-sparkline-cell { + text-align: center; + white-space: nowrap; + min-width: 4rem; +} + +.usage-sparkline { + display: block; + margin: auto; + color: var(--cudly-primary, #2563eb); +}