Skip to content

Commit

Permalink
[redhat-3.10] ui: breadcrumbs on new ui (PROJQUAY-5452) (#2490)
Browse files Browse the repository at this point in the history
* ui: breadcrumbs on new ui (PROJQUAY-5452) (#1893)

* UI: Breadcrumbs fix (PROJQUAY-5452)

* minor fixes

* using a generalized function to fetch the next breadcrumb item

* remove building breadcrumbs from browser histroy

* add tests

* fixing eslint

* fetching element from test-id instead of patternfly class

* removing overview list from navigation routes
  • Loading branch information
Sunandadadi committed Dec 5, 2023
1 parent 4d3aa4d commit fea7c2c
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 118 deletions.
125 changes: 125 additions & 0 deletions web/cypress/e2e/breadcrumbs.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/// <reference types="cypress" />

describe('Tests for Breadcrumbs', () => {
beforeEach(() => {
cy.exec('npm run quay:seed');
cy.request('GET', `${Cypress.env('REACT_QUAY_APP_API_URL')}/csrf_token`)
.then((response) => response.body.csrf_token)
.then((token) => {
cy.loginByCSRF(token);
});
});

it('Organization list page', () => {
cy.visit('/organization');
cy.get('nav[test-id="page-breadcrumbs-list"]').should('not.exist');
});

it('Repository list page', () => {
cy.visit('/repository');
cy.get('nav[test-id="page-breadcrumbs-list"]').should('not.exist');
});

it('Organization page', () => {
cy.visit('/organization/projectquay');
cy.get('nav[test-id="page-breadcrumbs-list"]').within(() => {
cy.get('li')
.each(($el, index) => {
switch (index) {
case 0:
cy.wrap($el).should('have.text', 'organization');
cy.wrap($el)
.children('a')
.should('have.attr', 'href', '/organization');
break;
case 1:
cy.wrap($el).should('have.text', 'projectquay');
cy.wrap($el).children('a').should('have.class', 'disabled-link');
cy.wrap($el)
.children('a')
.should('have.attr', 'href', '/organization/projectquay');
break;
}
})
.then(($lis) => {
expect($lis).to.have.length(2);
});
});
});

it('Repository page', () => {
cy.visit('/repository/projectquay/repo1');
cy.get('nav[test-id="page-breadcrumbs-list"]').within(() => {
cy.get('li')
.each(($el, index) => {
switch (index) {
case 0:
cy.wrap($el).should('have.text', 'repository');
cy.wrap($el)
.children('a')
.should('have.attr', 'href', '/repository');
break;
case 1:
cy.wrap($el).should('have.text', 'projectquay');
cy.wrap($el)
.children('a')
.should('have.attr', 'href', '/organization/projectquay');
break;
case 2:
cy.wrap($el).should('have.text', 'repo1');
cy.wrap($el).children('a').should('have.class', 'disabled-link');
cy.wrap($el)
.children('a')
.should('have.attr', 'href', '/repository/projectquay/repo1');
break;
}
})
.then(($lis) => {
expect($lis).to.have.length(3);
});
});
});

it('Tags list page', () => {
cy.visit('/repository/user1/hello-world/tag/latest');
cy.get('nav[test-id="page-breadcrumbs-list"]').within(() => {
cy.get('li')
.each(($el, index) => {
switch (index) {
case 0:
cy.wrap($el).should('have.text', 'repository');
cy.wrap($el)
.children('a')
.should('have.attr', 'href', '/repository');
break;
case 1:
cy.wrap($el).should('have.text', 'user1');
cy.wrap($el)
.children('a')
.should('have.attr', 'href', '/organization/user1');
break;
case 2:
cy.wrap($el).should('have.text', 'hello-world');
cy.wrap($el)
.children('a')
.should('have.attr', 'href', '/repository/user1/hello-world');
break;
case 3:
cy.wrap($el).should('have.text', 'latest');
cy.wrap($el).children('a').should('have.class', 'disabled-link');
cy.wrap($el)
.children('a')
.should(
'have.attr',
'href',
'/repository/user1/hello-world/tag/latest',
);
break;
}
})
.then(($lis) => {
expect($lis).to.have.length(4);
});
});
});
});
10 changes: 0 additions & 10 deletions web/src/atoms/BrowserHistoryState.ts

This file was deleted.

154 changes: 54 additions & 100 deletions web/src/components/breadcrumb/Breadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import React, {useEffect, useState} from 'react';
import useBreadcrumbs, {
BreadcrumbComponentType,
} from 'use-react-router-breadcrumbs';
import {useRecoilState} from 'recoil';
import {BrowserHistoryState} from 'src/atoms/BrowserHistoryState';
import {
parseOrgNameFromUrl,
parseRepoNameFromUrl,
parseTagNameFromUrl,
} from 'src/libs/utils';

export function QuayBreadcrumb() {
const [browserHistory, setBrowserHistoryState] =
useRecoilState(BrowserHistoryState);
const location = useLocation();

const [breadcrumbItems, setBreadcrumbItems] = useState<QuayBreadcrumbItem[]>(
[],
Expand All @@ -30,130 +32,82 @@ export function QuayBreadcrumb() {

const resetBreadCrumbs = () => {
setBreadcrumbItems([]);
setBrowserHistoryState([]);
};

const fetchRepoName = (route) => {
const re = new RegExp(urlParams.organizationName + '/(.*)', 'i');
const result = route.match(re);
return result[1];
};

const buildBreadCrumbFromPrevRoute = (object) => {
const prevObj = {};
prevObj['pathname'] = object.match.pathname;
prevObj['title'] = fetchRepoName(prevObj['pathname']);
prevObj['active'] =
prevObj['pathname'].localeCompare(window.location.pathname) === 0;
return prevObj;
const fetchBreadcrumb = (existingBreadcrumbs, nextBreadcrumb) => {
// existingBreadcrumbs is a list of breadcrumbs on the page
// first breadcrumb is either organization or repository
if (existingBreadcrumbs.length == 0) {
nextBreadcrumb['title'] = nextBreadcrumb['pathname'].split('/').at(-1);
}
// second breadcrumb is organization name
else if (existingBreadcrumbs.length == 1) {
nextBreadcrumb['title'] = parseOrgNameFromUrl(location.pathname);
nextBreadcrumb['pathname'] =
location.pathname.split(/repository|organization/)[0] +
'organization/' +
nextBreadcrumb['title'];
}
// third breadcrumb is repo name
else if (existingBreadcrumbs.length == 2) {
nextBreadcrumb['title'] = parseRepoNameFromUrl(location.pathname);
nextBreadcrumb['pathname'] =
location.pathname.split(nextBreadcrumb['title'])[0] +
nextBreadcrumb['title'];
}
// fourth breadcrumb is tag name
else if (existingBreadcrumbs.length == 3) {
nextBreadcrumb['title'] = parseTagNameFromUrl(location.pathname);
nextBreadcrumb['pathname'] =
existingBreadcrumbs[2]['pathname'] + '/tag/' + nextBreadcrumb['title'];
}
if (
location.pathname.replace(/.*\/organization|.*\/repository/g, '') ==
nextBreadcrumb['pathname'].replace(/.*\/organization|.*\/repository/g, '')
) {
nextBreadcrumb['active'] = true;
}
return nextBreadcrumb;
};

const buildFromRoute = () => {
const result = [];
const history = [];
let prevItem = null;

for (let i = 0; i < routerBreadcrumbs.length; i++) {
const newObj = {};
if (result.length == 4) {
break;
}
let newObj = {};
const object = routerBreadcrumbs[i];

newObj['pathname'] = object.match.pathname;
if (object.match.route.Component.type.name == 'RepositoryDetails') {
prevItem = object;
// Continuing till we find the last RepositoryDetails route for nested repo paths
continue;
} else {
newObj['title'] = object.match.pathname.split('/').slice(-1)[0];
if (object.key != '') {
newObj['title'] = object.key.replace(/\//, '');
}
newObj['active'] =
object.match.pathname.localeCompare(window.location.pathname) === 0;

if (prevItem) {
const prevObj = buildBreadCrumbFromPrevRoute(prevItem);
result.push(prevObj);
history.push(prevObj);
prevItem = null;
}

newObj = fetchBreadcrumb(result, newObj);
result.push(newObj);
history.push(newObj);
}

// If prevItem was not pushed in the for loop
if (prevItem) {
const prevObj = buildBreadCrumbFromPrevRoute(prevItem);
result.push(prevObj);
history.push(prevObj);
prevItem = null;
}

setBreadcrumbItems(result);
setBrowserHistoryState(history);
};

const currentBreadcrumbItem = () => {
const newItem = {};
const lastItem = routerBreadcrumbs[routerBreadcrumbs.length - 1];

newItem['pathname'] = lastItem.location.pathname;
// Form QuayBreadcrumbItem for the current path
if (lastItem.match.route.Component.type.name == 'RepositoryDetails') {
newItem['title'] = fetchRepoName(newItem['pathname']);
} else {
newItem['title'] = newItem['pathname'].split('/').slice(-1)[0];
}

newItem['active'] = true;
return newItem;
};

const buildFromBrowserHistory = () => {
const result = [];
const history = [];
const newItem = currentBreadcrumbItem();

for (const value of Array.from(browserHistory.values())) {
const newObj = {};
newObj['pathname'] = value['pathname'];
if (typeof value['title'] === 'string') {
newObj['title'] = value['title'];
} else if (value.title?.props?.children) {
newObj['title'] = value['title']['props']['children'];
}
newObj['active'] =
value['pathname'].localeCompare(window.location.pathname) === 0;
if (newItem['pathname'] == newObj['pathname']) {
newItem['title'] = newObj['title'];
if (newObj['active']) {
break;
}
result.push(newObj);
history.push(newObj);
}
result.push(newItem);
history.push(newItem);

setBreadcrumbItems(result);
setBrowserHistoryState(history);
};

useEffect(() => {
// urlParams has atleast one item - {*: 'endpoint'}
// If size = 1, no params are defined in the url, so we reset breadcrumb history
// urlParams has at least one item - Eg: root of the page => {*: 'organization'/'repository'}
// If size = 1, no params are defined in the url and therefore no breadcrumbs exist for the page. So, we set breadcrumb items as an empty list
if (Object.keys(urlParams).length <= 1) {
resetBreadCrumbs();
return;
}

if (browserHistory.length > 0) {
buildFromBrowserHistory();
} else {
buildFromRoute();
}
buildFromRoute();
}, [window.location.pathname]);

return (
<div>
{breadcrumbItems.length > 0 ? (
<PageBreadcrumb>
<Breadcrumb>
<Breadcrumb test-id="page-breadcrumbs-list">
{breadcrumbItems.map((object, i) => (
<BreadcrumbItem
render={(props) => (
Expand Down
12 changes: 9 additions & 3 deletions web/src/libs/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,20 @@ export function parseRepoNameFromUrl(url: string): string {
}

export function parseOrgNameFromUrl(url: string): string {
//url is in the format of <prefix>/repository/<org>/<repo>
//url is in the format of <prefix>/repository/<org>/<repo> or <prefix>/organization/<org>/<repo>
//or <prefix>/repository/<org>/<repo>/tag/<tag>
const urlParts = url.split('/');
const repoKeywordIndex = urlParts.indexOf('repository');
if (repoKeywordIndex === -1) {
const orgKeywordIndex = urlParts.indexOf('organization');
if (repoKeywordIndex != -1) {
return urlParts[repoKeywordIndex + 1];
}

if (orgKeywordIndex === -1) {
return '';
}
return urlParts[repoKeywordIndex + 1];

return urlParts[orgKeywordIndex + 1];
}

export function parseTagNameFromUrl(url: string): string {
Expand Down

0 comments on commit fea7c2c

Please sign in to comment.