Skip to content

Commit

Permalink
CONSOLE-922: Support AppliedClusterResourceQuota for normal users
Browse files Browse the repository at this point in the history
Added support for list and details pages, as well as other pages like Search. We now also display ACRQs in the ResourceQuotas table for projects.

Fixes https://issues.redhat.com/browse/CONSOLE-922
  • Loading branch information
rebeccaalpert committed Nov 22, 2021
1 parent 54f679a commit 4d0dc2d
Show file tree
Hide file tree
Showing 14 changed files with 1,027 additions and 64 deletions.
98 changes: 97 additions & 1 deletion frontend/__tests__/components/resource-quota.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,56 @@
import * as React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { UsageIcon, ResourceUsageRow } from '../../public/components/resource-quota';
import {
UsageIcon,
ResourceUsageRow,
getACRQResourceUsage,
} from '../../public/components/resource-quota';

// We don't render ResourceUsageRows for cluster-only data, but use it in a Gauge chart
describe('Check getResourceUsage for AppliedClusterResourceQuota', () => {
const quota = {
apiVersion: 'quota.openshift.io/v1',
kind: 'AppliedClusterResourceQuota',
metadata: { name: 'example' },
spec: { quota: { hard: { 'limits.cpu': '2' } } },
status: {
namespaces: [
{
namespace: 'test-namespace',
status: { used: { 'limits.cpu': '0' }, hard: { 'limits.cpu': '2' } },
},
{
namespace: 'test-namespace2',
status: { used: { 'limits.cpu': '1' }, hard: { 'limits.cpu': '2' } },
},
],
total: { hard: { 'limits.cpu': '2' }, used: { 'limits.cpu': '1' } },
},
};

it('Provides correct cluster-only data', () => {
expect(getACRQResourceUsage(quota, 'limits.cpu')).toEqual({
used: { cluster: '1', namespace: 0 },
totalUsed: '1',
max: '2',
percent: { namespace: 0, otherNamespaces: 50, unused: 50 },
});
});
it('Provides correct namespaced data', () => {
expect(getACRQResourceUsage(quota, 'limits.cpu', 'test-namespace')).toEqual({
used: { cluster: '1', namespace: '0' },
totalUsed: '1',
max: '2',
percent: { namespace: 0, otherNamespaces: 50, unused: 50 },
});
expect(getACRQResourceUsage(quota, 'limits.cpu', 'test-namespace2')).toEqual({
used: { cluster: '1', namespace: '1' },
totalUsed: '1',
max: '2',
percent: { namespace: 50, otherNamespaces: 0, unused: 50 },
});
});
});

describe('UsageIcon', () => {
let wrapper: ShallowWrapper;
Expand Down Expand Up @@ -98,3 +148,49 @@ describe('Check cluster quota table columns by ResourceUsageRow', () => {
expect(col3.text()).toBe('2');
});
});

describe('Check applied cluster quota table columns by ResourceUsageRow', () => {
let wrapper: ShallowWrapper;
const quota = {
apiVersion: 'quota.openshift.io/v1',
kind: 'AppliedClusterResourceQuota',
metadata: { name: 'example' },
spec: { quota: { hard: { 'limits.cpu': 2 } } },
status: {
namespaces: [
{
namespace: 'test-namespace',
status: { used: { 'limits.cpu': 0 }, hard: { 'limits.cpu': 2 } },
},
{
namespace: 'test-namespace2',
status: { used: { 'limits.cpu': 1 }, hard: { 'limits.cpu': 2 } },
},
],
total: { hard: { 'limits.cpu': 2 }, used: { 'limits.cpu': 1 } },
},
};

beforeEach(() => {
wrapper = shallow(
<ResourceUsageRow resourceType={'limits.cpu'} quota={quota} namespace="test-namespace" />,
);
});

it('renders ResourceUsageRow for each columns', () => {
const col0 = wrapper.childAt(0);
expect(col0.text()).toBe('limits.cpu');

const col1 = wrapper.childAt(1);
expect(col1.find('.co-resource-quota-icon').exists()).toBe(true);

const col2 = wrapper.childAt(2);
expect(col2.text()).toBe('0');

const col3 = wrapper.childAt(3);
expect(col3.text()).toBe('1');

const col4 = wrapper.childAt(4);
expect(col4.text()).toBe('2');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,11 @@ describe('Project Dashboard', () => {
it('shows Resource Quotas', async () => {
expect(projectDashboardView.resourceQuotasCard.isDisplayed()).toBe(true);
expect(
await projectDashboardView.resourceQuotasCard.$('.co-dashboard-card__body').getText(),
).toEqual('No resource quotas');
await projectDashboardView.resourceQuotasCard
.$('.co-dashboard-card__body')
.$('.co-dashboard-card__body--top-margin')
.getText(),
).toEqual('No ResourceQuotas');
createResource(resourceQuota);
addLeakableResource(leakedResources, resourceQuota);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"There are no ongoing activities.": "There are no ongoing activities.",
"Ongoing": "Ongoing",
"Not available": "Not available",
"No resource quotas": "No resource quotas",
"No ResourceQuotas": "No ResourceQuotas",
"View details": "View details",
"Alerts could not be loaded.": "Alerts could not be loaded.",
"({{operatorStatusLength}} installed)": "({{operatorStatusLength}} installed)",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as React from 'react';
import {
getQuotaResourceTypes,
QuotaScopesInline,
QuotaGaugeCharts,
} from '@console/internal/components/resource-quota';
import { ResourceLink } from '@console/internal/components/utils/resource-link';
import { AppliedClusterResourceQuotaModel } from '@console/internal/models';
import { referenceForModel, AppliedClusterResourceQuotaKind } from '@console/internal/module/k8s';

import './resource-quota-card.scss';

const AppliedClusterResourceQuotaItem: React.FC<AppliedClusterResourceQuotaItemProps> = ({
resourceQuota,
namespace,
}) => {
const resourceTypes = getQuotaResourceTypes(resourceQuota);
const scopes = resourceQuota?.spec?.quota?.scopes;
return (
<>
<div>
<ResourceLink
kind={referenceForModel(AppliedClusterResourceQuotaModel)}
name={resourceQuota.metadata.name}
className="co-resource-item--truncate co-resource-quota-card__item-title"
namespace={namespace}
inline
/>
{scopes && <QuotaScopesInline scopes={scopes} />}
</div>
<QuotaGaugeCharts
quota={resourceQuota}
resourceTypes={resourceTypes}
namespace={namespace}
chartClassName="co-resource-quota-card__chart"
/>
</>
);
};

export default AppliedClusterResourceQuotaItem;

type AppliedClusterResourceQuotaItemProps = {
resourceQuota: AppliedClusterResourceQuotaKind;
namespace: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ import { useTranslation } from 'react-i18next';

import './resource-quota-card.scss';

const ResourceQuotaBody: React.FC<ResourceQuotaBodyProps> = ({ error, isLoading, children }) => {
const ResourceQuotaBody: React.FC<ResourceQuotaBodyProps> = ({
error,
isLoading,
noText,
children,
}) => {
let body: React.ReactNode;
const { t } = useTranslation();
if (error) {
body = <div className="text-secondary">{t('console-shared~Not available')}</div>;
} else if (isLoading) {
body = <div className="skeleton-quota" />;
} else if (!React.Children.count(children)) {
body = <div className="text-secondary">{t('console-shared~No resource quotas')}</div>;
body = <div className="text-secondary">{noText || t('console-shared~No ResourceQuotas')}</div>;
}

return <div className="co-dashboard-card__body--top-margin">{body || children}</div>;
Expand All @@ -22,4 +27,5 @@ export default ResourceQuotaBody;
type ResourceQuotaBodyProps = {
error: boolean;
isLoading: boolean;
noText?: string;
};
140 changes: 140 additions & 0 deletions frontend/packages/integration-tests-cypress/tests/crud/quotas.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { safeLoad, safeDump } from 'js-yaml';
import * as _ from 'lodash';
import { checkErrors, testName } from '../../support';
import { projectDropdown } from '../../views/common';
import { detailsPage } from '../../views/details-page';
import { errorMessage } from '../../views/form';
import { listPage } from '../../views/list-page';
import { modal } from '../../views/modal';
import { nav } from '../../views/nav';
import * as yamlEditor from '../../views/yaml-editor';

const quotaName = 'example-resource-quota';
const clusterQuotaName = 'example-cluster-resource-quota';
const allProjectsDropdownLabel = 'All Projects';

const createExampleQuotas = () => {
cy.log('create quota instance');
nav.sidenav.clickNavLink(['Administration', 'ResourceQuotas']);
projectDropdown.selectProject(testName);
projectDropdown.shouldContain(testName);
listPage.clickCreateYAMLbutton();
// sidebar needs to be fully loaded, else it sometimes overlays the Create button
cy.byTestID('resource-sidebar').should('exist');
yamlEditor.isLoaded();
let newContent;
yamlEditor.getEditorContent().then((content) => {
newContent = _.defaultsDeep({}, { metadata: { name: quotaName } }, safeLoad(content));
yamlEditor.setEditorContent(safeDump(newContent)).then(() => {
yamlEditor.clickSaveCreateButton();
cy.get(errorMessage).should('not.exist');
});
});
detailsPage.breadcrumb(0).click();

cy.log('create cluster quota instance');
listPage.clickCreateYAMLbutton();
cy.byTestID('resource-sidebar').should('exist');
yamlEditor.isLoaded();
yamlEditor.getEditorContent().then((content) => {
newContent = _.defaultsDeep(
{},
{
kind: 'ClusterResourceQuota',
apiVersion: 'quota.openshift.io/v1',
metadata: { name: clusterQuotaName },
spec: {
quota: {
hard: {
pods: '10',
secrets: '10',
},
},
selector: {
labels: {
matchLabels: {
'kubernetes.io/metadata.name': testName,
},
},
},
},
},
safeLoad(content),
);
yamlEditor.setEditorContent(safeDump(newContent)).then(() => {
yamlEditor.clickSaveCreateButton();
cy.get(errorMessage).should('not.exist');
});
});
};

const deleteClusterExamples = () => {
cy.log('delete ClusterResourceQuota instance');
projectDropdown.selectProject(allProjectsDropdownLabel);
nav.sidenav.clickNavLink(['Administration', 'ResourceQuotas']);
listPage.rows.shouldBeLoaded();
listPage.filter.byName(clusterQuotaName);
listPage.rows.clickRowByName(clusterQuotaName);
detailsPage.isLoaded();
detailsPage.clickPageActionFromDropdown('Delete ClusterResourceQuota');
modal.shouldBeOpened();
modal.submit();
modal.shouldBeClosed();
detailsPage.isLoaded();
};

describe('Quotas', () => {
before(() => {
cy.login();
cy.createProject(testName);
createExampleQuotas();
});

afterEach(() => {
checkErrors();
});

after(() => {
deleteClusterExamples();
cy.deleteProject(testName);
cy.logout();
});

it(`'All Projects' shows ResourceQuotas`, () => {
nav.sidenav.clickNavLink(['Administration', 'ResourceQuotas']);
projectDropdown.selectProject(allProjectsDropdownLabel);
listPage.rows.shouldBeLoaded();
listPage.filter.byName(quotaName);
listPage.rows.shouldExist(quotaName);
});

it(`'All Projects' shows ClusterResourceQuotas`, () => {
nav.sidenav.clickNavLink(['Administration', 'ResourceQuotas']);
projectDropdown.selectProject(allProjectsDropdownLabel);
listPage.rows.shouldBeLoaded();
listPage.filter.byName(clusterQuotaName);
listPage.rows.shouldExist(clusterQuotaName);
listPage.rows.clickRowByName(clusterQuotaName);
detailsPage.isLoaded();
detailsPage.breadcrumb(0).contains('ClusterResourceQuota');
});

it(`Test namespace shows ResourceQuotas`, () => {
nav.sidenav.clickNavLink(['Administration', 'ResourceQuotas']);
projectDropdown.selectProject(testName);
listPage.rows.shouldBeLoaded();
listPage.filter.byName(quotaName);
listPage.rows.shouldExist(quotaName);
});

it(`Test namespace shows AppliedClusterResourceQuotas`, () => {
nav.sidenav.clickNavLink(['Administration', 'ResourceQuotas']);
projectDropdown.selectProject(testName);
listPage.rows.shouldBeLoaded();
listPage.filter.byName(clusterQuotaName);
listPage.rows.shouldExist(clusterQuotaName);
listPage.rows.clickRowByName(clusterQuotaName);
detailsPage.isLoaded();
detailsPage.breadcrumb(0).contains('AppliedClusterResourceQuota');
});
});
Loading

0 comments on commit 4d0dc2d

Please sign in to comment.