Skip to content
This repository has been archived by the owner on Mar 1, 2024. It is now read-only.

Commit

Permalink
add breadcrumbs to NavTitle
Browse files Browse the repository at this point in the history
  • Loading branch information
Alec Merdler committed Oct 27, 2017
1 parent f3f47d6 commit 2e65245
Show file tree
Hide file tree
Showing 12 changed files with 230 additions and 54 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-undef, no-unused-vars */

import * as React from 'react';
import { Link } from 'react-router-dom';
import { Link, match } from 'react-router-dom';
import { shallow, ShallowWrapper } from 'enzyme';
import * as _ from 'lodash';

Expand Down Expand Up @@ -383,7 +383,14 @@ describe(ClusterServiceVersionResourcesDetailsPage.displayName, () => {
let wrapper: ShallowWrapper<ClusterServiceVersionResourcesDetailsPageProps>;

beforeEach(() => {
wrapper = shallow(<ClusterServiceVersionResourcesDetailsPage kind={testResourceInstance.kind} namespace="default" name={testResourceInstance.metadata.name} />);
const match: match<any> = {
params: {appName: 'etcd', plural: 'etcdclusters', name: 'my-etcd'},
isExact: false,
url: '/ns/example/clusterserviceversion-v1s/etcd/etcdclusters/my-etcd',
path: '/ns/:ns/clusterserviceversion-v1s/:appName/:plural/:name',
};

wrapper = shallow(<ClusterServiceVersionResourcesDetailsPage kind={testResourceInstance.kind} namespace="default" name={testResourceInstance.metadata.name} match={match} />);
});

it('renders a `DetailsPage` with the correct subpages', () => {
Expand All @@ -401,6 +408,15 @@ describe(ClusterServiceVersionResourcesDetailsPage.displayName, () => {

expect(detailsPage.props().menuActions).toEqual(Cog.factory.common);
});

it('passes breadcrumbs to `DetailsPage`', () => {
const detailsPage = wrapper.find(DetailsPage);

expect(detailsPage.props().breadcrumbs).toEqual([
{name: 'etcd', path: '/ns/example/clusterserviceversion-v1s/etcd/details'},
{name: `${testResourceInstance.kind} Details`, path: '/ns/example/clusterserviceversion-v1s/etcd/etcdclusters/my-etcd/details'},
]);
});
});

describe(ClusterServiceVersionResourcesPage.displayName, () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ describe(ClusterServiceVersionsDetailsPage.displayName, () => {
let wrapper: ShallowWrapper<ClusterServiceVersionsDetailsPageProps>;

beforeEach(() => {
wrapper = shallow(<ClusterServiceVersionsDetailsPage kind={testClusterServiceVersion.kind} name={testClusterServiceVersion.metadata.name} namespace="default" />);
wrapper = shallow(<ClusterServiceVersionsDetailsPage kind={testClusterServiceVersion.kind} name={testClusterServiceVersion.metadata.name} namespace="default" match={null} />);
});

it('renders a `DetailsPage` with the correct subpages', () => {
Expand Down
74 changes: 74 additions & 0 deletions frontend/__tests__/components/utils/nav-title.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/* eslint-disable no-undef, no-unused-vars */

import * as React from 'react';
import { Link } from 'react-router-dom';
import { shallow, ShallowWrapper } from 'enzyme';

import { NavTitle, NavTitleProps, BreadCrumbs, BreadCrumbsProps } from '../../../public/components/utils/nav-title';
import { ResourceIcon } from '../../../public/components/utils';

describe(BreadCrumbs.displayName, () => {
let wrapper: ShallowWrapper<BreadCrumbsProps>;
let breadcrumbs: BreadCrumbsProps['breadcrumbs'];

beforeEach(() => {
breadcrumbs = [
{name: 'pods', path: '/pods'},
{name: 'containers', path: '/pods'},
];
wrapper = shallow(<BreadCrumbs breadcrumbs={breadcrumbs} />);
});

it('renders link for each given breadcrumb', () => {
const links: ShallowWrapper<any> = wrapper.find(Link);

expect(links.length).toEqual(breadcrumbs.length);

breadcrumbs.forEach((crumb, i) => {
expect(links.at(i).props().to).toEqual(crumb.path);
expect(links.at(i).childAt(0).text()).toEqual(crumb.name);
});
});

it('renders separator between each breadcrumb link', () => {
const separators = wrapper.find('.co-m-nav-title__breadcrumbs__seperator');

expect(separators.length).toEqual(breadcrumbs.length - 1);

separators.forEach((separator) => {
expect(separator.text()).toEqual('/');
});
});
});

describe(NavTitle.displayName, () => {
let wrapper: ShallowWrapper<NavTitleProps>;

beforeEach(() => {
wrapper = shallow(<NavTitle obj={null} />);
});

it('renders resource icon if given `kind`', () => {
const kind = 'Pod';
wrapper.setProps({kind});
const icon = wrapper.find(ResourceIcon);

expect(icon.exists()).toBe(true);
expect(icon.props().kind).toEqual(kind);
});

it('renders custom title component if given', () => {
const title = <span>My Custom Title</span>;
wrapper.setProps({title});

expect(wrapper.find('.co-m-page-title').contains(title)).toBe(true);
});

it('renders breadcrumbs if given `breadcrumbs`', () => {
const breadcrumbs = [];
wrapper.setProps({breadcrumbs});

expect(wrapper.find(BreadCrumbs).exists()).toBe(true);
expect(wrapper.find(BreadCrumbs).props().breadcrumbs).toEqual(breadcrumbs);
});
});
28 changes: 28 additions & 0 deletions frontend/public/components/_nav-title.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@
background-color: $color-grey-background;
}

.co-m-nav-title__breadcrumbs {
display: flex;
align-items: center;
font-size: 11px;
padding-top: 15px;
padding-bottom: 15px;
padding-left: 10px;
}

.co-m-nav-title__breadcrumbs__link {
margin: 5px;
}

.co-m-nav-title__breadcrumbs__link--end {
color: #999999;
}

.co-m-nav-title__breadcrumbs__seperator {
font-weight: 200;
font-size: 11px;
color: #333333;
}

.co-m-page-title {
display: flex;
justify-content: space-between;
Expand All @@ -23,3 +46,8 @@
padding-top: 15px;
padding-bottom: 15px;
}

.co-m-page-title--breadcrumbs {
padding-top: 0;
}

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-undef, no-unused-vars */

import * as React from 'react';
import { Link } from 'react-router-dom';
import { Link, match } from 'react-router-dom';
import * as _ from 'lodash';
import { connect } from 'react-redux';
import { Map as ImmutableMap } from 'immutable';
Expand Down Expand Up @@ -293,6 +293,11 @@ export const ClusterServiceVersionResourceDetails = connectToPlural(
export const ClusterServiceVersionResourcesDetailsPage: React.StatelessComponent<ClusterServiceVersionResourcesDetailsPageProps> = (props) => <DetailsPage
{...props}
menuActions={Cog.factory.common}
isList={false}
breadcrumbs={[
{name: props.match.params.appName, path: `${props.match.url.split('/').slice(0, 5).join('/')}/details`},
{name: `${props.kind} Details`, path: `${props.match.url}/details`},
]}
pages={[
navFactory.details((props) => <ClusterServiceVersionResourceDetails {...props} appName={props.match.params.appName} />),
navFactory.editYaml(),
Expand Down Expand Up @@ -373,6 +378,7 @@ export type ClusterServiceVersionResourcesDetailsPageProps = {
kind: string;
name: string;
namespace: string;
match: match<any>;
};

export type ClusterServiceVersionResourcesDetailsState = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-undef, no-unused-vars */

import * as React from 'react';
import { Link } from 'react-router-dom';
import { Link, match } from 'react-router-dom';
import * as _ from 'lodash';
import { Map as ImmutableMap } from 'immutable';
import { connect } from 'react-redux';
Expand Down Expand Up @@ -205,7 +205,7 @@ export const ClusterServiceVersionsDetailsPage: React.StatelessComponent<Cluster
<ClusterServiceVersionResourcesPage loaded={true} obj={obj} />
</div>;

return <DetailsPage {...props} pages={[details(ClusterServiceVersionDetails), editYaml(), {href: 'resources', name: 'Resources', component: Resources}]} />;
return <DetailsPage {...props} isList={false} pages={[details(ClusterServiceVersionDetails), editYaml(), {href: 'resources', name: 'Resources', component: Resources}]} />;
};

export type ClusterServiceVersionsPageProps = {
Expand All @@ -230,6 +230,7 @@ export type ClusterServiceVersionsDetailsPageProps = {
kind: string;
name: string;
namespace: string;
match: match<any>;
};

export type ClusterServiceVersionDetailsProps = {
Expand Down
14 changes: 11 additions & 3 deletions frontend/public/components/container.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as React from 'react';
import { Link } from 'react-router-dom';
import * as _ from 'lodash';

import {getContainerState, getContainerStatus, getPullPolicyLabel} from '../module/k8s/docker';
import { getContainerState, getContainerStatus, getPullPolicyLabel } from '../module/k8s/docker';
import * as k8sProbe from '../module/k8s/probe';
import {Firehose, Overflow, MsgBox, NavTitle, Timestamp, VertNav} from './utils';
import { Firehose, Overflow, MsgBox, NavTitle, Timestamp, VertNav } from './utils';

const getResourceLimitValue = container => {
const limits = _.get(container, 'resources.limits');
Expand Down Expand Up @@ -202,7 +203,14 @@ const Details = (props) => {
};

export const ContainersDetailsPage = (props) => <div>
<NavTitle detail={true} title={props.match.params.name} kind="Container" />
<NavTitle
detail={true}
title={props.match.params.name}
kind="Container"
breadcrumbs={[
{name: props.match.params.podName, path: `${props.match.url.split('/').slice(0, 5).join('/')}/details`},
{name: 'Container Details', path: `${props.match.url}/details`},
]} />
<Firehose resources={[{
name: props.match.params.podName,
namespace: props.match.params.ns,
Expand Down
17 changes: 0 additions & 17 deletions frontend/public/components/factory/details.jsx

This file was deleted.

32 changes: 32 additions & 0 deletions frontend/public/components/factory/details.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* eslint-disable no-undef, no-unused-vars */

import * as React from 'react';
import { match } from 'react-router-dom';

import { Firehose, VertNav, NavTitle } from '../utils';

export const DetailsPage: React.StatelessComponent<DetailsPageProps> = (props) => <Firehose resources={[{
kind: props.kind,
name: props.name,
namespace: props.namespace,
isList: false,
prop: 'obj',
}]}>
<NavTitle detail={true} title={props.name} menuActions={props.menuActions} kind={props.kind} breadcrumbs={props.breadcrumbs} />
<VertNav pages={props.pages} className={`co-m-${props.kind}`} match={props.match} label={props.label || props.kind.label}/>
</Firehose>;

export type DetailsPageProps = {
match: match<any>;
title?: string | JSX.Element;
menuActions?: any[];
pages: any[];
kind: string | any;
label?: string;
name?: string;
namespace?: string;
isList: boolean;
breadcrumbs?: {name: string, path: string}[];
};

DetailsPage.displayName = 'DetailsPage';
28 changes: 0 additions & 28 deletions frontend/public/components/utils/nav-title.jsx

This file was deleted.

56 changes: 56 additions & 0 deletions frontend/public/components/utils/nav-title.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* eslint-disable no-undef, no-unused-vars */

import * as React from 'react';
import { Link } from 'react-router-dom';
import * as classNames from'classnames';
import * as _ from 'lodash';

import { ActionsMenu, kindObj, ResourceIcon } from './index';
import { ClusterServiceVersionLogo, K8sResourceKind } from '../cloud-services';

export const BreadCrumbs: React.StatelessComponent<BreadCrumbsProps> = ({breadcrumbs}) => (
<div className="co-m-nav-title__breadcrumbs">
{ breadcrumbs.map((crumb, i, {length}) => {
const isLast = i === length - 1;

return <div key={i}>
<Link className={classNames('co-m-nav-title__breadcrumbs__link', {'co-m-nav-title__breadcrumbs__link--end': isLast})} to={crumb.path}>{crumb.name}</Link>
{ !isLast && <span className="co-m-nav-title__breadcrumbs__seperator">/</span> }
</div>;
}) }
</div>);

export const NavTitle: React.StatelessComponent<NavTitleProps> = ({kind, detail, title, menuActions, obj, breadcrumbs, children}) => {
const data = _.get<K8sResourceKind>(obj, 'data');
const hasLogo = !_.isEmpty(data) && _.has(data, 'spec.icon');
const logo = hasLogo
? <ClusterServiceVersionLogo icon={_.get(data, 'spec.icon', [])[0]} displayName={data.spec.displayName} version={data.spec.version} provider={data.spec.provider} />
: <div>{ kind && <ResourceIcon kind={kind} className="co-m-page-title__icon" /> } <span>{title}</span></div>;

return <div className={classNames('row', detail ? 'co-m-nav-title__detail' : 'co-m-nav-title')}>
<div className="col-xs-12">
{ breadcrumbs && <BreadCrumbs breadcrumbs={breadcrumbs} />}
<h1 className={classNames('co-m-page-title', {'co-m-page-title--detail': detail}, {'co-m-page-title--logo': hasLogo}, {'co-m-page-title--breadcrumbs': breadcrumbs})}>
{logo}
{ menuActions && !_.isEmpty(data) && !_.has(data.metadata, 'deletionTimestamp') && <ActionsMenu actions={menuActions.map(a => a(kindObj(kind), data))} /> }
</h1>
{children}
</div>
</div>;
};

export type NavTitleProps = {
kind?: string;
detail?: boolean;
title?: string | JSX.Element;
menuActions?: any[];
obj?: {data: K8sResourceKind};
breadcrumbs?: {name: string, path: string}[];
};

export type BreadCrumbsProps = {
breadcrumbs: {name: string, path: string}[];
};

NavTitle.displayName = 'NavTitle';
BreadCrumbs.displayName = 'BreadCrumbs';

0 comments on commit 2e65245

Please sign in to comment.