diff --git a/frontend/__tests__/components/cloud-services/clusterserviceversion-resource.spec.tsx b/frontend/__tests__/components/cloud-services/clusterserviceversion-resource.spec.tsx index 21b47764ba7..63d3ec26c00 100644 --- a/frontend/__tests__/components/cloud-services/clusterserviceversion-resource.spec.tsx +++ b/frontend/__tests__/components/cloud-services/clusterserviceversion-resource.spec.tsx @@ -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'; @@ -383,7 +383,14 @@ describe(ClusterServiceVersionResourcesDetailsPage.displayName, () => { let wrapper: ShallowWrapper; beforeEach(() => { - wrapper = shallow(); + const match: match = { + 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(); }); it('renders a `DetailsPage` with the correct subpages', () => { @@ -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, () => { diff --git a/frontend/__tests__/components/cloud-services/clusterserviceversion.spec.tsx b/frontend/__tests__/components/cloud-services/clusterserviceversion.spec.tsx index 6f7c1caa784..fdd88420634 100644 --- a/frontend/__tests__/components/cloud-services/clusterserviceversion.spec.tsx +++ b/frontend/__tests__/components/cloud-services/clusterserviceversion.spec.tsx @@ -273,7 +273,7 @@ describe(ClusterServiceVersionsDetailsPage.displayName, () => { let wrapper: ShallowWrapper; beforeEach(() => { - wrapper = shallow(); + wrapper = shallow(); }); it('renders a `DetailsPage` with the correct subpages', () => { diff --git a/frontend/__tests__/components/utils/nav-title.spec.tsx b/frontend/__tests__/components/utils/nav-title.spec.tsx new file mode 100644 index 00000000000..2fda9908481 --- /dev/null +++ b/frontend/__tests__/components/utils/nav-title.spec.tsx @@ -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; + let breadcrumbs: BreadCrumbsProps['breadcrumbs']; + + beforeEach(() => { + breadcrumbs = [ + {name: 'pods', path: '/pods'}, + {name: 'containers', path: '/pods'}, + ]; + wrapper = shallow(); + }); + + it('renders link for each given breadcrumb', () => { + const links: ShallowWrapper = 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; + + beforeEach(() => { + wrapper = shallow(); + }); + + 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 = My Custom Title; + 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); + }); +}); diff --git a/frontend/public/components/_nav-title.scss b/frontend/public/components/_nav-title.scss index f0067932d07..a8a5de04c7e 100644 --- a/frontend/public/components/_nav-title.scss +++ b/frontend/public/components/_nav-title.scss @@ -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; @@ -23,3 +46,8 @@ padding-top: 15px; padding-bottom: 15px; } + +.co-m-page-title--breadcrumbs { + padding-top: 0; +} + diff --git a/frontend/public/components/cloud-services/clusterserviceversion-resource.tsx b/frontend/public/components/cloud-services/clusterserviceversion-resource.tsx index 03fdd8cd5c4..2092b73a665 100644 --- a/frontend/public/components/cloud-services/clusterserviceversion-resource.tsx +++ b/frontend/public/components/cloud-services/clusterserviceversion-resource.tsx @@ -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'; @@ -293,6 +293,11 @@ export const ClusterServiceVersionResourceDetails = connectToPlural( export const ClusterServiceVersionResourcesDetailsPage: React.StatelessComponent = (props) => ), navFactory.editYaml(), @@ -373,6 +378,7 @@ export type ClusterServiceVersionResourcesDetailsPageProps = { kind: string; name: string; namespace: string; + match: match; }; export type ClusterServiceVersionResourcesDetailsState = { diff --git a/frontend/public/components/cloud-services/clusterserviceversion.tsx b/frontend/public/components/cloud-services/clusterserviceversion.tsx index d0a9029263b..a7a665d2fa4 100644 --- a/frontend/public/components/cloud-services/clusterserviceversion.tsx +++ b/frontend/public/components/cloud-services/clusterserviceversion.tsx @@ -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'; @@ -205,7 +205,7 @@ export const ClusterServiceVersionsDetailsPage: React.StatelessComponent ; - return ; + return ; }; export type ClusterServiceVersionsPageProps = { @@ -230,6 +230,7 @@ export type ClusterServiceVersionsDetailsPageProps = { kind: string; name: string; namespace: string; + match: match; }; export type ClusterServiceVersionDetailsProps = { diff --git a/frontend/public/components/container.jsx b/frontend/public/components/container.jsx index 614800aa330..9b221c285ae 100644 --- a/frontend/public/components/container.jsx +++ b/frontend/public/components/container.jsx @@ -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'); @@ -202,7 +203,14 @@ const Details = (props) => { }; export const ContainersDetailsPage = (props) =>
- + } */ -export const DetailsPage = (props) => - - -; - -// DetailsPage. diff --git a/frontend/public/components/factory/details.tsx b/frontend/public/components/factory/details.tsx new file mode 100644 index 00000000000..7c778fc5e8f --- /dev/null +++ b/frontend/public/components/factory/details.tsx @@ -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 = (props) => + + +; + +export type DetailsPageProps = { + match: match; + 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'; diff --git a/frontend/public/components/factory/index.jsx b/frontend/public/components/factory/index.tsx similarity index 100% rename from frontend/public/components/factory/index.jsx rename to frontend/public/components/factory/index.tsx diff --git a/frontend/public/components/utils/nav-title.jsx b/frontend/public/components/utils/nav-title.jsx deleted file mode 100644 index 5e04acddc33..00000000000 --- a/frontend/public/components/utils/nav-title.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react'; -import * as classNames from'classnames'; -import * as _ from 'lodash'; - -import { ActionsMenu, kindObj, ResourceIcon } from './index'; -import { ClusterServiceVersionLogo } from '../cloud-services'; - -/** @type {React.StatelessComponent.<{kind?: string, detail?: boolean, title?: string, menuActions?: any[], data?: any[] | any, children?: any[] | any}}> */ -export const NavTitle = ({kind, detail, title, menuActions, obj, children}) => { - const data = _.get(obj, 'data'); - const hasLogo = !_.isEmpty(data) && _.has(data, 'spec.icon'); - const logo = hasLogo - ? - :
{ kind && } {title}
; - - return
-
-

- {logo} - { menuActions && !_.isEmpty(data) && !_.get(data.metadata, 'deletionTimestamp') && a(kindObj(kind), data))} /> } -

- { children } -
-
; -}; - -NavTitle.displayName = 'NavTitle'; - diff --git a/frontend/public/components/utils/nav-title.tsx b/frontend/public/components/utils/nav-title.tsx new file mode 100644 index 00000000000..b1035a5ae46 --- /dev/null +++ b/frontend/public/components/utils/nav-title.tsx @@ -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 = ({breadcrumbs}) => ( +
+ { breadcrumbs.map((crumb, i, {length}) => { + const isLast = i === length - 1; + + return
+ {crumb.name} + { !isLast && / } +
; + }) } +
); + +export const NavTitle: React.StatelessComponent = ({kind, detail, title, menuActions, obj, breadcrumbs, children}) => { + const data = _.get(obj, 'data'); + const hasLogo = !_.isEmpty(data) && _.has(data, 'spec.icon'); + const logo = hasLogo + ? + :
{ kind && } {title}
; + + return
+
+ { breadcrumbs && } +

+ {logo} + { menuActions && !_.isEmpty(data) && !_.has(data.metadata, 'deletionTimestamp') && a(kindObj(kind), data))} /> } +

+ {children} +
+
; +}; + +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';