Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion frontend/src/__tests__/inventory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,10 @@ describe('loadCoverageBreakdown — fetch + render flow', () => {
expect(rows[1]!.textContent).toContain('100.0%');
});

test('renders "No usage detected" for providers with null services', async () => {
test('renders "No usage detected" for providers with null services when no chip is active', async () => {
// No chip active: genuinely no usage -> original message.
(state.getCurrentProvider as jest.Mock).mockReturnValue('');
(state.getCurrentAccountIDs as jest.Mock).mockReturnValue([]);
(api.getCoverageBreakdown as jest.Mock).mockResolvedValue({
providers: [
makeProviderSection('aws', null, null),
Expand All @@ -450,6 +453,47 @@ describe('loadCoverageBreakdown — fetch + render flow', () => {
const empties = container.querySelectorAll('.empty');
expect(empties.length).toBe(3);
expect(empties[0]!.textContent).toContain('AWS');
expect(empties[0]!.textContent).toMatch(/no usage detected/i);
});

test('renders filter-aware empty state when provider chip is active and services are empty', async () => {
// Provider chip set -> chip filtered all rows; message must not claim "No usage detected".
(state.getCurrentProvider as jest.Mock).mockReturnValue('gcp');
(state.getCurrentAccountIDs as jest.Mock).mockReturnValue([]);
(api.getCoverageBreakdown as jest.Mock).mockResolvedValue({
providers: [
makeProviderSection('gcp', null, null),
],
});

await loadCoverageBreakdown();

const container = document.getElementById('coverage-providers')!;
const empty = container.querySelector('.empty');
expect(empty).not.toBeNull();
expect(empty!.textContent).toContain('GCP');
expect(empty!.textContent).toMatch(/selected account\/filter/i);
expect(empty!.textContent).not.toMatch(/no usage detected/i);
});

test('renders filter-aware empty state when account chip is active and services are empty', async () => {
// Account chip set (single account) -> message must reference selected filter.
(state.getCurrentProvider as jest.Mock).mockReturnValue('');
(state.getCurrentAccountIDs as jest.Mock).mockReturnValue(['acct-99']);
(api.getCoverageBreakdown as jest.Mock).mockResolvedValue({
providers: [
makeProviderSection('aws', null, null),
],
});

await loadCoverageBreakdown();

const container = document.getElementById('coverage-providers')!;
const empty = container.querySelector('.empty');
expect(empty).not.toBeNull();
expect(empty!.textContent).toContain('AWS');
expect(empty!.textContent).toMatch(/selected account\/filter/i);
expect(empty!.textContent).not.toMatch(/no usage detected/i);
});

test('renders N/A for null coverage_pct (no usage signal on that service)', async () => {
Expand Down
43 changes: 37 additions & 6 deletions frontend/src/inventory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,26 @@ function buildActiveCommitmentsEmptyMessage(provider?: string, accountID?: strin
return 'No active commitments found across your registered accounts.';
}

/**
* Build a context-aware empty-state message for a per-provider Coverage card.
* When chip filters are active the message names the scope so the user knows
* the result is filtered rather than genuinely absent usage.
*
* @param providerLabel - Display name of the provider (e.g. "AWS").
* @param activeProvider - The provider chip value, if one is set.
* @param activeAccountID - The account chip value, if exactly one is selected.
*/
function buildCoverageEmptyMessage(
providerLabel: string,
activeProvider?: string,
activeAccountID?: string,
): string {
if (activeProvider || activeAccountID) {
return `No ${providerLabel} usage for the selected account/filter.`;
}
return `No usage detected for ${providerLabel}.`;
}

/**
* Render the per-commitment table into `container`. Empty list yields
* an inline `.empty` paragraph instead of an empty table so the user
Expand Down Expand Up @@ -302,7 +322,7 @@ export async function loadCoverageBreakdown(): Promise<void> {

try {
const data = await api.getCoverageBreakdown({ provider: provider || undefined, accountID });
renderCoverageBreakdown(container, data.providers);
renderCoverageBreakdown(container, data.providers, provider || undefined, accountID);
} catch (error) {
teardownSkeleton(container);
const err = error as Error;
Expand All @@ -326,7 +346,12 @@ function wireCoverageRefreshButton(): void {
* textContent -- no innerHTML -- so no escaping helper is needed (XSS posture
* matches the active-commitments section per issue #340).
*/
function renderCoverageBreakdown(container: HTMLElement, providers: ProviderCoverageSection[]): void {
function renderCoverageBreakdown(
container: HTMLElement,
providers: ProviderCoverageSection[],
activeProvider?: string,
activeAccountID?: string,
): void {
clearChildren(container);

if (!providers || providers.length === 0) {
Expand All @@ -335,20 +360,26 @@ function renderCoverageBreakdown(container: HTMLElement, providers: ProviderCove
}

for (const section of providers) {
container.appendChild(buildProviderSection(section));
container.appendChild(buildProviderSection(section, activeProvider, activeAccountID));
}
}

function buildProviderSection(section: ProviderCoverageSection): HTMLElement {
function buildProviderSection(
section: ProviderCoverageSection,
activeProvider?: string,
activeAccountID?: string,
): HTMLElement {
const card = document.createElement('section');
card.className = 'card coverage-provider-card';

// Header row: provider name + overall coverage badge.
const header = document.createElement('div');
header.className = 'section-header';

const providerLabel = PROVIDER_DISPLAY_NAMES[section.provider] ?? section.provider.toUpperCase();

const title = document.createElement('h3');
title.textContent = PROVIDER_DISPLAY_NAMES[section.provider] ?? section.provider.toUpperCase();
title.textContent = providerLabel;
header.appendChild(title);

if (section.overall_coverage_pct !== null && section.overall_coverage_pct !== undefined) {
Expand All @@ -363,7 +394,7 @@ function buildProviderSection(section: ProviderCoverageSection): HTMLElement {
if (!section.services || section.services.length === 0) {
const empty = document.createElement('p');
empty.className = 'empty';
empty.textContent = `No usage detected for ${PROVIDER_DISPLAY_NAMES[section.provider] ?? section.provider}.`;
empty.textContent = buildCoverageEmptyMessage(providerLabel, activeProvider, activeAccountID);
card.appendChild(empty);
return card;
}
Expand Down
Loading