Skip to content

Commit

Permalink
Support schema-grouped descriptors on operand details page
Browse files Browse the repository at this point in the history
Implement descriptor groups based on shared root path segment.

Story: https://issues.redhat.com/browse/CONSOLE-2283
Sub-task: https://issues.redhat.com/browse/CONSOLE-2308
  • Loading branch information
TheRealJon committed Jul 24, 2020
1 parent 5fdd776 commit e1fa61d
Show file tree
Hide file tree
Showing 13 changed files with 489 additions and 370 deletions.
@@ -0,0 +1,66 @@
import * as _ from 'lodash';
import * as React from 'react';
import { DetailsItem, ResourceLink } from '@console/internal/components/utils';
import { CapabilityProps, SpecCapability, StatusCapability } from './types';
import { REGEXP_K8S_RESOURCE_SUFFIX } from './const';
import { YellowExclamationTriangleIcon } from '@console/shared';

export const DefaultCapability: React.FC<CommonCapabilityProps> = ({
description,
label,
obj,
fullPath,
value,
}) => {
const detail = React.useMemo(() => {
if (_.isEmpty(value) && !_.isFinite(value) && !_.isBoolean(value)) {
return <span className="text-muted">None</span>;
}
if (_.isObject(value) || _.isArray(value)) {
return <span className="text-muted">Unsupported</span>;
}
return _.toString(value);
}, [value]);

return (
<DetailsItem description={description} label={label} obj={obj} path={fullPath}>
{detail}
</DetailsItem>
);
};

export const K8sResourceLinkCapability: React.FC<CommonCapabilityProps> = ({
capability,
description,
descriptor,
fullPath,
label,
obj,
value,
}) => {
const detail = React.useMemo(() => {
if (!value) {
return <span className="text-muted">None</span>;
}

const [, suffix] = capability.match(REGEXP_K8S_RESOURCE_SUFFIX) ?? [];
const gvk = suffix?.replace(/:/g, '~');
if (!_.isString(value)) {
return (
<>
<YellowExclamationTriangleIcon /> Invalid descriptor: value at path &apos;
{descriptor.path}&apos; must be a {gvk} resource name.
</>
);
}

return <ResourceLink kind={gvk} name={value} namespace={obj.metadata.namespace} />;
}, [value, capability, obj.metadata.namespace, descriptor.path]);
return (
<DetailsItem description={description} label={label} obj={obj} path={fullPath}>
{detail}
</DetailsItem>
);
};

type CommonCapabilityProps = CapabilityProps<SpecCapability | StatusCapability>;
@@ -0,0 +1,159 @@
import * as React from 'react';
import * as _ from 'lodash';
import { DetailsItem } from '@console/internal/components/utils';
import { getSchemaAtPath } from '@console/shared';
import { Descriptor, DescriptorType } from './types';
import { K8sKind, K8sResourceKind } from '@console/internal/module/k8s';
import { JSONSchema6 } from 'json-schema';
import { SpecDescriptorDetailsItem } from './spec';
import { StatusDescriptorDetailsItem } from './status';
import { withFallback } from '@console/shared/src/components/error/error-boundary';

export const DescriptorDetailsItem = withFallback<DescriptorDetailsItemProps>(
({ descriptor, model, obj, onError, schema, type }) => {
const propertySchema = getSchemaAtPath(schema, descriptor.path);
const description = descriptor?.description || propertySchema?.description;
const fullPath = [type, ..._.toPath(descriptor.path)];
const label = descriptor.displayName || propertySchema?.title || _.startCase(_.last(fullPath));
const value = _.get(obj, fullPath, descriptor.value);
switch (type) {
case DescriptorType.spec:
return (
<SpecDescriptorDetailsItem
description={description}
descriptor={descriptor}
label={label}
model={model}
obj={obj}
onError={onError}
fullPath={fullPath}
value={value}
/>
);
case DescriptorType.status:
return (
<StatusDescriptorDetailsItem
description={description}
descriptor={descriptor}
label={label}
model={model}
obj={obj}
onError={onError}
fullPath={fullPath}
value={value}
/>
);
default:
return null;
}
},
);

const DescriptorDetailsItemGroup: React.FC<DescriptorDetailsItemGroupProps> = ({
descriptors,
groupName,
model,
obj,
onError,
schema,
type,
}) => {
const propertySchema = getSchemaAtPath(schema, groupName) ?? {};
const { root, descendants } = _.groupBy(descriptors, (descriptor) =>
descriptor.path === groupName ? 'root' : 'descendants',
);
const description = root?.[0]?.description || propertySchema?.description;
const label = root?.[0]?.displayName || propertySchema?.title || _.startCase(groupName);
return descendants?.length > 0 ? (
<DetailsItem
description={description}
label={label}
obj={obj}
path={`${type}.${groupName}`}
valueClassName="details-item__value--group"
>
<dl>
{descendants.map((descriptor) => (
<DescriptorDetailsItem
key={`${type}.${descriptor.path}`}
descriptor={descriptor}
model={model}
obj={obj}
onError={onError}
schema={schema}
type={type}
/>
))}
</dl>
</DetailsItem>
) : null;
};

export const DescriptorDetailsItemList: React.FC<DescriptorDetailsItemListProps> = ({
descriptors,
itemClassName,
model,
obj,
onError,
schema,
type,
}) => {
const groupedDescriptors = (descriptors ?? []).reduce((acc, descriptor) => {
const [key] = _.toPath(descriptor.path);
return {
...acc,
[key]: [...(acc?.[key] ?? []), descriptor],
};
}, {});
return (
<dl className={`olm-descriptors olm-descriptors--${type}`}>
{_.map(groupedDescriptors, (group: Descriptor[], groupName) => (
<div key={`${type}.${groupName}`} className={itemClassName}>
{group.length > 1 ? (
<DescriptorDetailsItemGroup
descriptors={group}
groupName={groupName}
model={model}
obj={obj}
onError={onError}
schema={schema}
type={type}
/>
) : (
<DescriptorDetailsItem
descriptor={group[0]}
model={model}
obj={obj}
onError={onError}
schema={schema}
type={type}
/>
)}
</div>
))}
</dl>
);
};

export type DescriptorDetailsItemProps = {
descriptor: Descriptor;
obj: K8sResourceKind;
model: K8sKind;
onError?: (e: Error) => void;
schema: JSONSchema6;
type: DescriptorType;
};

type DescriptorDetailsItemGroupProps = Omit<DescriptorDetailsItemProps, 'descriptor'> & {
descriptors: Descriptor[];
groupName: string;
type: DescriptorType;
};

type DescriptorDetailsItemListProps = Omit<DescriptorDetailsItemGroupProps, 'groupName'> & {
itemClassName?: string;
};

DescriptorDetailsItem.displayName = 'DescriptorDetailsItem';
DescriptorDetailsItemGroup.displayName = 'DescriptorDetailsItemGroup';
DescriptorDetailsItemList.displayName = 'DescriptorDetailsItemList';
Expand Up @@ -5,12 +5,12 @@ import { Router } from 'react-router';
import { mount, ReactWrapper } from 'enzyme';
import store from '@console/internal/redux';
import { ResourceLink, Selector, history } from '@console/internal/components/utils';
import { DescriptorProps, SpecCapability, Descriptor } from '../types';
import { SpecCapability, Descriptor, DescriptorType } from '../types';
import { testResourceInstance, testModel } from '../../../../mocks';
import { EndpointList } from './endpoint';
import { ResourceRequirementsModalLink } from './resource-requirements';
import * as configureSize from './configure-size';
import { SpecDescriptor } from '.';
import { DescriptorDetailsItem, DescriptorDetailsItemProps } from '..';

const OBJ = {
...testResourceInstance,
Expand All @@ -23,8 +23,8 @@ const OBJ = {
},
};

describe(SpecDescriptor.name, () => {
let wrapper: ReactWrapper<DescriptorProps>;
describe('Spec descriptors', () => {
let wrapper: ReactWrapper<DescriptorDetailsItemProps>;
let descriptor: Descriptor;

beforeEach(() => {
Expand All @@ -34,13 +34,22 @@ describe(SpecDescriptor.name, () => {
description: '',
'x-descriptors': [],
};
wrapper = mount(<SpecDescriptor model={testModel} obj={OBJ} descriptor={descriptor} />, {
wrappingComponent: (props) => (
<Router history={history}>
<Provider store={store} {...props} />
</Router>
),
});
wrapper = mount(
<DescriptorDetailsItem
descriptor={descriptor}
model={testModel}
obj={OBJ}
type={DescriptorType.spec}
schema={{}}
/>,
{
wrappingComponent: (props) => (
<Router history={history}>
<Provider store={store} {...props} />
</Router>
),
},
);
});

it('renders spec value as text if no matching capability component', () => {
Expand Down Expand Up @@ -155,7 +164,6 @@ describe(SpecDescriptor.name, () => {
'x-descriptors': [`${SpecCapability.selector}core:v1:Service`],
};
wrapper.setProps({ descriptor });

expect(wrapper.find(Selector).prop('selector')).toEqual(OBJ.spec.basicSelector);
expect(wrapper.find(Selector).prop('kind')).toEqual('core:v1:Service');
});
Expand Down

0 comments on commit e1fa61d

Please sign in to comment.