diff --git a/web/cypress/e2e/org-list.cy.ts b/web/cypress/e2e/org-list.cy.ts index eb63a0cf0..71f8f36df 100644 --- a/web/cypress/e2e/org-list.cy.ts +++ b/web/cypress/e2e/org-list.cy.ts @@ -135,4 +135,111 @@ describe('Org List Page', () => { cy.get('td[data-label="Name"]').should('have.length', 20); cy.contains('1 - 20 of 30').should('exist'); }); + + it('Superuser displays quota consumed column (PROJQUAY-9641)', () => { + // This test verifies that superusers can see quota consumed data + // for organizations and user namespaces in the organizations list + + // Mock config with quota features enabled + cy.fixture('config.json').then((config) => { + config.features.QUOTA_MANAGEMENT = true; + config.features.EDIT_QUOTA = true; + config.features.SUPER_USERS = true; + config.features.SUPERUSERS_FULL_ACCESS = true; + cy.intercept('GET', '/config', config).as('getConfig'); + }); + + // Mock superuser + cy.fixture('superuser.json').then((user) => { + cy.intercept('GET', '/api/v1/user/', user).as('getSuperUser'); + }); + + // Mock superuser organizations with quota_report data + cy.fixture('superuser-organizations.json').then((orgsData) => { + // Add quota_report to organizations + orgsData.organizations[0].quota_report = { + quota_bytes: 10737418240, + configured_quota: 53687091200, + }; + orgsData.organizations[1].quota_report = { + quota_bytes: 5368709120, + configured_quota: 21474836480, + }; + cy.intercept('GET', '/api/v1/superuser/organizations/', orgsData).as( + 'getSuperuserOrganizations', + ); + }); + + // Mock superuser users with quota_report data + cy.fixture('superuser-users.json').then((usersData) => { + // Add quota_report to users + usersData.users[0].quota_report = { + quota_bytes: 2147483648, + configured_quota: 10737418240, + }; + cy.intercept('GET', '/api/v1/superuser/users/', usersData).as( + 'getSuperuserUsers', + ); + }); + + // Mock organization details + cy.intercept('GET', '/api/v1/organization/testorg', { + statusCode: 200, + body: { + name: 'testorg', + email: 'testorg@example.com', + teams: {owners: 'admin'}, + }, + }); + + cy.intercept('GET', '/api/v1/organization/projectquay', { + statusCode: 200, + body: { + name: 'projectquay', + email: 'projectquay@example.com', + teams: {}, + }, + }); + + cy.intercept('GET', '/api/v1/organization/coreos', { + statusCode: 200, + body: { + name: 'coreos', + email: 'coreos@example.com', + teams: {owners: 'admin'}, + }, + }); + + // Mock robots/members/repositories for all organizations + cy.intercept('GET', '/api/v1/organization/*/robots', { + statusCode: 200, + body: {robots: []}, + }); + + cy.intercept('GET', '/api/v1/organization/*/members', { + statusCode: 200, + body: {members: []}, + }); + + cy.intercept('GET', '/api/v1/repository?namespace=*', { + statusCode: 200, + body: {repositories: []}, + }); + + cy.visit('/organization'); + cy.wait('@getConfig'); + cy.wait('@getSuperUser'); + cy.wait('@getSuperuserOrganizations'); + cy.wait('@getSuperuserUsers'); + + // Verify the Size column header exists for superusers + cy.contains('th', 'Size').should('exist'); + + // Verify quota data cells are visible and contain actual data + cy.get('td[data-label="Size"]').should('exist'); + + // Verify at least one organization shows quota consumed data (not "—") + // The quota should be displayed as "10.0 GiB / 50.0 GiB" format + cy.get('td[data-label="Size"]').first().should('not.contain.text', '—'); + }); }); diff --git a/web/src/hooks/UseOrganizations.ts b/web/src/hooks/UseOrganizations.ts index 49cfcdbcb..1d114c767 100644 --- a/web/src/hooks/UseOrganizations.ts +++ b/web/src/hooks/UseOrganizations.ts @@ -6,6 +6,7 @@ import { searchOrgsState, } from 'src/atoms/OrganizationListState'; import {SearchState} from 'src/components/toolbar/SearchTypes'; +import {IQuotaReport} from 'src/libs/quotaUtils'; import { bulkDeleteOrganizations, createOrg, @@ -19,6 +20,7 @@ export type OrganizationDetail = { isUser: boolean; userEnabled?: boolean; userSuperuser?: boolean; + quota_report?: IQuotaReport; }; export function useOrganizations() { @@ -68,19 +70,25 @@ export function useOrganizations() { const organizationsTableDetails = [] as OrganizationDetail[]; for (const orgname of orgnames) { + // Find the organization object to get quota_report + const orgObj = (superUserOrganizations || []).find( + (o) => o.name === orgname, + ); organizationsTableDetails.push({ name: orgname, isUser: false, + quota_report: orgObj?.quota_report, }); } for (const username of usernames) { - // Find the user's enabled status from superUserUsers + // Find the user's enabled status and quota_report from superUserUsers const userObj = (superUserUsers || []).find((u) => u.username === username); organizationsTableDetails.push({ name: username, isUser: true, userEnabled: userObj?.enabled, userSuperuser: userObj?.super_user, + quota_report: userObj?.quota_report, }); } diff --git a/web/src/routes/OrganizationsList/OrganizationsList.tsx b/web/src/routes/OrganizationsList/OrganizationsList.tsx index 7e88c634d..85ec4b98d 100644 --- a/web/src/routes/OrganizationsList/OrganizationsList.tsx +++ b/web/src/routes/OrganizationsList/OrganizationsList.tsx @@ -480,6 +480,7 @@ export default function OrganizationsList() { userEnabled={org.userEnabled} userSuperuser={org.userSuperuser} userEmail={org.isUser ? userEmailMap[org.name] : undefined} + quota_report={org.quota_report} > ))} diff --git a/web/src/routes/OrganizationsList/OrganizationsListTableData.tsx b/web/src/routes/OrganizationsList/OrganizationsListTableData.tsx index ce46e8ec2..db6eb548e 100644 --- a/web/src/routes/OrganizationsList/OrganizationsListTableData.tsx +++ b/web/src/routes/OrganizationsList/OrganizationsListTableData.tsx @@ -43,6 +43,7 @@ function RepoLastModifiedDate(props: RepoLastModifiedDateProps) { interface OrgTableDataProps extends OrganizationsTableItem { userEmail?: string; + quota_report?: import('src/libs/quotaUtils').IQuotaReport; } // Get and assemble data from multiple endpoints to show in Org table @@ -182,13 +183,24 @@ export default function OrgTableData(props: OrgTableDataProps) { config?.features?.EDIT_QUOTA && ( {props.isUser ? ( - + props.quota_report ? ( + renderQuotaConsumed(props.quota_report, { + showPercentage: true, + showTotal: true, + showBackfill: true, + }) + ) : ( + + ) ) : ( - renderQuotaConsumed(organization?.quota_report, { - showPercentage: true, - showTotal: true, - showBackfill: true, - }) + renderQuotaConsumed( + props.quota_report || organization?.quota_report, + { + showPercentage: true, + showTotal: true, + showBackfill: true, + }, + ) )} )}