Skip to content

Commit

Permalink
Merge pull request #10137 from rebeccaalpert/applied-crq
Browse files Browse the repository at this point in the history
CONSOLE-922: Support AppliedClusterResourceQuota for normal users
  • Loading branch information
openshift-merge-robot authored Dec 1, 2021
2 parents ea0adcc + 84ec99b commit 9d0fedc
Show file tree
Hide file tree
Showing 15 changed files with 1,032 additions and 67 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 @@ -37,7 +37,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 9d0fedc

Please sign in to comment.