diff --git a/api-catalog-ui/frontend/cypress/integration/e2e/detail-page/service-version-compare.test.js b/api-catalog-ui/frontend/cypress/integration/e2e/detail-page/service-version-compare.test.js index f003e58898..65059a48e4 100644 --- a/api-catalog-ui/frontend/cypress/integration/e2e/detail-page/service-version-compare.test.js +++ b/api-catalog-ui/frontend/cypress/integration/e2e/detail-page/service-version-compare.test.js @@ -39,8 +39,10 @@ describe('>>> Service version compare Test', () => { it('Should show compare tab', () => { // Location of the compare has changed, it's no longer a specific tab cy.get('.tabs-container').should('not.exist'); - cy.get('.nav-tabs').should('exist'); - cy.get('.nav-tabs').should('have.length', 12); + cy.get('div.MuiTabs-root.custom-tabs.MuiTabs-vertical > div.MuiTabs-scroller.MuiTabs-scrollable > div').should('exist'); + cy.get('div.MuiTabs-flexContainer.MuiTabs-flexContainerVertical') // Select the parent div + .find('a.MuiTab-root') // Find all the anchor elements within the div + .should('have.length', 12); // Check if there are 12 anchor elements within the div cy.get('.version-text').should('exist'); cy.get('.version-text').should('contain.text', 'Compare'); }); diff --git a/api-catalog-ui/frontend/src/components/App/App.css b/api-catalog-ui/frontend/src/components/App/App.css index 16ded3f72c..385390398b 100644 --- a/api-catalog-ui/frontend/src/components/App/App.css +++ b/api-catalog-ui/frontend/src/components/App/App.css @@ -4,3 +4,8 @@ height: 100vh; } +@media only screen and (max-width: 1120px) { + .content { + width: fit-content; + } +} diff --git a/api-catalog-ui/frontend/src/components/Dashboard/Dashboard.css b/api-catalog-ui/frontend/src/components/Dashboard/Dashboard.css index 98250eb326..6b0364a2fc 100644 --- a/api-catalog-ui/frontend/src/components/Dashboard/Dashboard.css +++ b/api-catalog-ui/frontend/src/components/Dashboard/Dashboard.css @@ -126,6 +126,29 @@ a { border: 1px solid #C9C8C5; } +#search .MuiInputBase-root { + width: 100%; +} + +#search > div > div > div > #search-icon { + margin-left: 168px; +} + +#search-input > svg { + margin-left: 168px; +} + +/* For Firefox */ +@-moz-document url-prefix() { + #search > div > div > div > #search-icon { + margin-left: 125px; + } + #search-input > svg { + margin-left: 125px; + } +} + + #search_no_results { color: #1d5bbf; } @@ -262,7 +285,7 @@ a { display: contents; } -@media only screen and (max-width: 600px) { +@media only screen and (max-width: 1120px) { #dash-buttons { margin: 0; float: none; diff --git a/api-catalog-ui/frontend/src/components/Dashboard/_dashboard.scss b/api-catalog-ui/frontend/src/components/Dashboard/_dashboard.scss index 16a8192448..c3c56b3f1b 100644 --- a/api-catalog-ui/frontend/src/components/Dashboard/_dashboard.scss +++ b/api-catalog-ui/frontend/src/components/Dashboard/_dashboard.scss @@ -4,6 +4,9 @@ .header, .header > * { align-items: center; } + #search .MuiFormControl-root { + display: contents; + } h1 { margin: var( --spaceMedium ) 0; padding-bottom: var( --spaceMedium ); @@ -21,6 +24,7 @@ left: calc(50% - #{calc(var( --headerLineWidth ) / 2)}); } } + #search { height: 50px; width: 422px; @@ -28,8 +32,24 @@ margin-right: auto; background: #FFFFFF; border: 1px solid #C9C8C5; - .MuiFormControl-root { - display: flex; + + display: flex; + + .MuiInput-root { + width: 100%; + } + >div { + >div { + >input { + margin-left: auto; + position: static; + } + >div { + #search-icon { + margin-left: 156px; + } + } + } } } @@ -68,7 +88,20 @@ color: var( --text20 ); position: relative; left: var( --spaceSmaller ); + margin-top: 0px; + margin-left: 156px; } + + /* For Firefox */ + @-moz-document url-prefix() { + .MuiSvgIcon-root { + margin-left: 130px; + } + #search #search-icon { + margin-left: 130px; + } + } + .MuiInputBase-input { margin-top: auto; margin-left: 10px; @@ -90,4 +123,3 @@ width: 99%; display: block; } - diff --git a/api-catalog-ui/frontend/src/components/DetailPage/DetailPage.css b/api-catalog-ui/frontend/src/components/DetailPage/DetailPage.css index 49e4b25c7d..1e5bdf2324 100644 --- a/api-catalog-ui/frontend/src/components/DetailPage/DetailPage.css +++ b/api-catalog-ui/frontend/src/components/DetailPage/DetailPage.css @@ -3,6 +3,8 @@ } .main-content2 detail-content { + display: flex; + height: 100vh; text-align: left; background-color: #EFEFEF; } @@ -44,14 +46,12 @@ } .detailed-description-container { - margin-top: 2rem; text-align: left; overflow: hidden; - width: 100%; } .api-description-container { - margin-left: 397px; + margin-left: 375px; padding: 2.2rem; } @@ -87,7 +87,6 @@ } #title { - padding: 2rem .3rem 1.3rem 0; color: #58606e; font-size: 28px; font-weight: 700; @@ -130,12 +129,13 @@ div.content-description-container > div > div:nth-child(2) > div { } .nav-bar { - min-height: 84vh; + max-width: 380px; + min-height: 40vh; flex-direction: column; align-items: center; min-width: 368px; box-shadow: 0 0 6px hsl(210 14% 90%); - padding: 10px 5px 10px 20px; + padding: 10px 5px 16px 20px; flex: 1 0 0; width: auto; height: auto; @@ -146,9 +146,7 @@ div.content-description-container > div > div:nth-child(2) > div { } .paragraph-description-container { - /*change width via templating mechanism to create space for the right menu (width 80%) */ width: auto; - /*width: 80%;*/ margin-left: 56px; } @@ -167,3 +165,7 @@ div.content-description-container > div > div:nth-child(2) > div { width: 600px; } } + +.MuiTabs-root.custom-tabs.MuiTabs-vertical { + margin-right: 12px; +} diff --git a/api-catalog-ui/frontend/src/components/DetailPage/DetailPage.jsx b/api-catalog-ui/frontend/src/components/DetailPage/DetailPage.jsx index fe9f0e52c4..7b2c30b49d 100644 --- a/api-catalog-ui/frontend/src/components/DetailPage/DetailPage.jsx +++ b/api-catalog-ui/frontend/src/components/DetailPage/DetailPage.jsx @@ -18,15 +18,20 @@ import PageNotFound from '../PageNotFound/PageNotFound'; import BigShield from '../ErrorBoundary/BigShield/BigShield'; import ServicesNavigationBarContainer from '../ServicesNavigationBar/ServicesNavigationBarContainer'; import Shield from '../ErrorBoundary/Shield/Shield'; -import { customUIStyle, isAPIPortal } from '../../utils/utilFunctions'; +import countAdditionalContents, { customUIStyle, isAPIPortal } from '../../utils/utilFunctions'; export default class DetailPage extends Component { componentDidMount() { - const { fetchTilesStart, currentTileId, fetchNewTiles } = this.props; + const { fetchTilesStart, currentTileId, fetchNewTiles, history } = this.props; fetchNewTiles(); if (currentTileId) { fetchTilesStart(currentTileId); } + if (!localStorage.getItem('serviceId')) { + const id = history.location.pathname.split('/service/')[1]; + localStorage.setItem('serviceId', id); + } + localStorage.removeItem('selectedTab'); } componentWillUnmount() { @@ -40,9 +45,16 @@ export default class DetailPage extends Component { history.push('/dashboard'); }; + handleLinkClick = (e, id) => { + e.preventDefault(); + const elementToView = document.querySelector(id); + if (elementToView) { + elementToView.scrollIntoView(); + } + }; + render() { const { - tiles, isLoading, clearService, fetchTilesStop, @@ -55,6 +67,7 @@ export default class DetailPage extends Component { currentTileId, fetchNewTiles, } = this.props; + let { tiles } = this.props; const iconBack = ; let error = null; if (fetchTilesError !== undefined && fetchTilesError !== null) { @@ -65,9 +78,19 @@ export default class DetailPage extends Component { fetchTilesStop(); fetchNewTiles(); fetchTilesStart(currentTileId); + } else if (services && services.length > 0 && !currentTileId) { + const id = history.location.pathname.split('/service/')[1]; + if (id) { + const correctTile = services.find((tile) => tile.services.some((service) => service.serviceId === id)); + if (correctTile) { + tiles = [correctTile]; + } + } } const apiPortalEnabled = isAPIPortal(); const hasTiles = !fetchTilesError && tiles && tiles.length > 0; + const { useCasesCounter, tutorialsCounter, videosCounter } = countAdditionalContents(services); + const onlySwaggerPresent = tutorialsCounter === 0 && videosCounter === 0 && useCasesCounter === 0; if (hasTiles && 'customStyleConfig' in tiles[0] && tiles[0].customStyleConfig) { customUIStyle(tiles[0].customStyleConfig); } @@ -101,16 +124,18 @@ export default class DetailPage extends Component { {!isLoading && !fetchTilesError && (
- - {iconBack} - Back - + {!apiPortalEnabled && ( + + {iconBack} + Back + + )}
{tiles !== undefined && tiles.length === 1 && ( @@ -127,23 +152,29 @@ export default class DetailPage extends Component { )}
- {apiPortalEnabled && ( + {apiPortalEnabled && !onlySwaggerPresent && (
On this page - + this.handleLinkClick(e, '#swagger-label')}> Swagger - - Use cases + this.handleLinkClick(e, '#use-cases-label')} + > + Use cases ({useCasesCounter}) - - Tutorials + this.handleLinkClick(e, '#tutorials-label')} + > + Tutorials ({tutorialsCounter}) - - Videos + this.handleLinkClick(e, '#videos-label')}> + Videos ({videosCounter})
@@ -167,7 +198,12 @@ export default class DetailPage extends Component { path={`${match.path}/:serviceId`} render={() => (
- +
)} /> diff --git a/api-catalog-ui/frontend/src/components/DetailPage/DetailPage.test.jsx b/api-catalog-ui/frontend/src/components/DetailPage/DetailPage.test.jsx index 39b36b2528..37d146185e 100644 --- a/api-catalog-ui/frontend/src/components/DetailPage/DetailPage.test.jsx +++ b/api-catalog-ui/frontend/src/components/DetailPage/DetailPage.test.jsx @@ -38,14 +38,17 @@ const match = { path: '/service', }; +const history = { + push: jest.fn(), + location: { + pathname: '/service/serviceId', + }, +}; + describe('>>> Detailed Page component tests', () => { it('should start epic on mount', () => { const fetchTilesStart = jest.fn(); const fetchNewTiles = jest.fn(); - const history = { - push: jest.fn(), - pathname: jest.fn(), - }; const wrapper = shallow( >> Detailed Page component tests', () => { it('should stop epic on unmount', () => { const fetchTilesStop = jest.fn(); - const history = { - push: jest.fn(), - pathname: jest.fn(), - }; const wrapper = shallow( >> Detailed Page component tests', () => { }); it('should handle a back button click', () => { - const historyMock = { push: jest.fn() }; const wrapper = shallow( >> Detailed Page component tests', () => { fetchTilesStart={jest.fn()} fetchNewTiles={jest.fn()} fetchTilesStop={jest.fn()} - history={historyMock} + history={history} match={match} /> ); wrapper.find('[data-testid="go-back-button"]').simulate('click'); - expect(historyMock.push.mock.calls[0]).toEqual(['/dashboard']); + expect(history.push.mock.calls[0]).toEqual(['/dashboard']); }); it('should load spinner when waiting for data', () => { @@ -119,7 +117,6 @@ describe('>>> Detailed Page component tests', () => { }); it('should display tile title', () => { - const historyMock = { push: jest.fn() }; const isLoading = false; const wrapper = shallow( >> Detailed Page component tests', () => { fetchTilesStart={jest.fn()} fetchNewTiles={jest.fn()} fetchTilesStop={jest.fn()} - history={historyMock} + history={history} match={match} isLoading={isLoading} /> @@ -139,7 +136,6 @@ describe('>>> Detailed Page component tests', () => { }); it('should display tile description', () => { - const historyMock = { push: jest.fn() }; const isLoading = false; const wrapper = shallow( >> Detailed Page component tests', () => { fetchTilesStart={jest.fn()} fetchNewTiles={jest.fn()} fetchTilesStop={jest.fn()} - history={historyMock} + history={history} match={match} isLoading={isLoading} /> @@ -159,7 +155,6 @@ describe('>>> Detailed Page component tests', () => { }); it('should set comms failed message when there is a Tile fetch 404 or 500 error', () => { - const historyMock = { push: jest.fn() }; const isLoading = false; const fetchTilesStop = jest.fn(); const fetchTilesError = { @@ -171,7 +166,7 @@ describe('>>> Detailed Page component tests', () => { fetchTilesStart={jest.fn()} fetchNewTiles={jest.fn()} fetchTilesStop={fetchTilesStop} - history={historyMock} + history={history} fetchTilesError={fetchTilesError} match={match} isLoading={isLoading} @@ -181,7 +176,6 @@ describe('>>> Detailed Page component tests', () => { }); it('should set comms failed message when there is a Tile fetch 404 or 500 error', () => { - const historyMock = { push: jest.fn() }; const isLoading = false; const fetchTilesStop = jest.fn(); const fetchTilesError = { @@ -193,7 +187,7 @@ describe('>>> Detailed Page component tests', () => { fetchTilesStart={jest.fn()} fetchNewTiles={jest.fn()} fetchTilesStop={fetchTilesStop} - history={historyMock} + history={history} fetchTilesError={fetchTilesError} match={match} isLoading={isLoading} @@ -203,7 +197,6 @@ describe('>>> Detailed Page component tests', () => { }); it('should clear the selected service, stop and restart fetching if a different tile is selected ', () => { - const historyMock = { push: jest.fn() }; const isLoading = false; const fetchTilesError = null; const fetchTilesStop = jest.fn(); @@ -217,7 +210,7 @@ describe('>>> Detailed Page component tests', () => { fetchTilesStart={fetchTilesStart} fetchNewTiles={jest.fn()} fetchTilesStop={fetchTilesStop} - history={historyMock} + history={history} fetchTilesError={fetchTilesError} match={match} isLoading={isLoading} @@ -228,4 +221,91 @@ describe('>>> Detailed Page component tests', () => { expect(clearService).toHaveBeenCalled(); expect(fetchTilesStart).toHaveBeenCalled(); }); + + it('should display nav right menu', () => { + process.env.REACT_APP_API_PORTAL = true; + const fetchTilesStart = jest.fn(); + const fetchNewTiles = jest.fn(); + tile.services[0].videos = ['video1', 'video2']; + tile.services[0].tutorials = ['tutorial1', 'tutorial2']; + tile.services[0].useCases = ['useCase1', 'useCase2']; + const wrapper = shallow( + + ); + expect(wrapper.find('#right-resources-menu').exists()).toEqual(true); + }); + + it('should click on the links', () => { + process.env.REACT_APP_API_PORTAL = true; + const fetchTilesStart = jest.fn(); + const fetchNewTiles = jest.fn(); + const mockHandleLinkClick = jest.fn(); + const mockEvent = { preventDefault: jest.fn() }; + const mockElementToView = { scrollIntoView: jest.fn() }; + document.querySelector = jest.fn().mockReturnValue(mockElementToView); + const wrapper = shallow( + + ); + // Simulate a click event on the Link component, providing the id as the second argument + wrapper.instance().handleLinkClick(mockEvent, '#swagger-label'); + expect(mockEvent.preventDefault).toHaveBeenCalled(); + expect(document.querySelector).toHaveBeenCalledWith('#swagger-label'); + expect(mockElementToView.scrollIntoView).toHaveBeenCalled(); + + wrapper.instance().handleLinkClick(mockEvent, '#use-cases-label'); + expect(mockEvent.preventDefault).toHaveBeenCalled(); + expect(document.querySelector).toHaveBeenCalledWith('#use-cases-label'); + expect(mockElementToView.scrollIntoView).toHaveBeenCalled(); + + wrapper.instance().handleLinkClick(mockEvent, '#videos-label'); + expect(mockEvent.preventDefault).toHaveBeenCalled(); + expect(document.querySelector).toHaveBeenCalledWith('#videos-label'); + expect(mockElementToView.scrollIntoView).toHaveBeenCalled(); + + wrapper.instance().handleLinkClick(mockEvent, '#tutorials-label'); + expect(mockEvent.preventDefault).toHaveBeenCalled(); + expect(document.querySelector).toHaveBeenCalledWith('#tutorials-label'); + expect(mockElementToView.scrollIntoView).toHaveBeenCalled(); + }); + + it('should get correct service tile if currentTileId not defined', () => { + process.env.REACT_APP_API_PORTAL = true; + const fetchTilesStart = jest.fn(); + const fetchNewTiles = jest.fn(); + const mockHandleLinkClick = jest.fn(); + const wrapper = shallow( + + ); + const instance = wrapper.instance(); + instance.componentDidMount(); + }); }); diff --git a/api-catalog-ui/frontend/src/components/DetailPage/DetailPageContainer.test.jsx b/api-catalog-ui/frontend/src/components/DetailPage/DetailPageContainer.test.jsx index 234aaf43df..87ce93230e 100644 --- a/api-catalog-ui/frontend/src/components/DetailPage/DetailPageContainer.test.jsx +++ b/api-catalog-ui/frontend/src/components/DetailPage/DetailPageContainer.test.jsx @@ -53,7 +53,7 @@ describe('DetailPage Container', () => { const history = { location: { - pathname: {}, + pathname: '/service/serviceId', }, push: jest.fn(), listen: jest.fn(), diff --git a/api-catalog-ui/frontend/src/components/DetailPage/_detailPage.scss b/api-catalog-ui/frontend/src/components/DetailPage/_detailPage.scss index 7962d05c42..2f8f902529 100644 --- a/api-catalog-ui/frontend/src/components/DetailPage/_detailPage.scss +++ b/api-catalog-ui/frontend/src/components/DetailPage/_detailPage.scss @@ -113,13 +113,13 @@ body .detail-content { box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); padding: 14px; margin-right: 30px; - margin-top: -119px; + margin-top: -60px; } - /* Extra small devices (phones, 600px and down) */ - @media only screen and (max-width: 600px) { + /* Extra small devices (phones, 1361px and down) */ + @media only screen and (max-width: 1361px) { #right-resources-menu { - display: contents; + display: none; } } @@ -163,7 +163,6 @@ body .detail-content { font-weight: initial; } #go-back-button { - margin-left: 12px; display: inline; margin-top: 20px; font-size: 13px; @@ -204,5 +203,14 @@ body .detail-content { .detailed-description-container { display: flow-root; } + .no-tiles-container { + margin-left: 20px; + } + + .btn > h4 { + padding-right: 10px; + padding-left: 10px; + font-size: 13px; + } } diff --git a/api-catalog-ui/frontend/src/components/ErrorBoundary/BigShield/BigShield.jsx b/api-catalog-ui/frontend/src/components/ErrorBoundary/BigShield/BigShield.jsx index 29156a6f32..7506b68c0c 100644 --- a/api-catalog-ui/frontend/src/components/ErrorBoundary/BigShield/BigShield.jsx +++ b/api-catalog-ui/frontend/src/components/ErrorBoundary/BigShield/BigShield.jsx @@ -60,7 +60,13 @@ export default class BigShield extends Component {
{!disableButton && (
-
diff --git a/api-catalog-ui/frontend/src/components/Header/_header.scss b/api-catalog-ui/frontend/src/components/Header/_header.scss index c7c0de93bf..faf06ac4a8 100644 --- a/api-catalog-ui/frontend/src/components/Header/_header.scss +++ b/api-catalog-ui/frontend/src/components/Header/_header.scss @@ -68,8 +68,8 @@ body { #img-internal-link { margin-right: 20px; margin-left: 12px; - height: 16px; - width: 16px; + height: 15px; + width: 15px; } }//end header } diff --git a/api-catalog-ui/frontend/src/components/Login/Login.css b/api-catalog-ui/frontend/src/components/Login/Login.css index c27522ead8..bf75e309a8 100644 --- a/api-catalog-ui/frontend/src/components/Login/Login.css +++ b/api-catalog-ui/frontend/src/components/Login/Login.css @@ -8,6 +8,12 @@ right: 6rem; } +#spinner { + position: absolute; + z-index: 1; + margin-left: -64px; +} + .login-object { overflow: hidden; background-image: url("../../assets/images/zowe-background.jpg"); @@ -15,7 +21,7 @@ background-repeat: no-repeat; display: flex; flex: auto; - height: 100%; + height: 100vh; padding-bottom: 0; } @@ -140,8 +146,7 @@ form#login-form { } .susp-card { - padding-top: 200px; - padding-left: 600px; + margin: auto; } .MuiFormControl-root { @@ -160,4 +165,4 @@ form#login-form { #warn-first-line { display: flex; padding-bottom: 10px; -} \ No newline at end of file +} diff --git a/api-catalog-ui/frontend/src/components/Login/Login.jsx b/api-catalog-ui/frontend/src/components/Login/Login.jsx index 76f8c147fd..5848baf7cf 100644 --- a/api-catalog-ui/frontend/src/components/Login/Login.jsx +++ b/api-catalog-ui/frontend/src/components/Login/Login.jsx @@ -185,6 +185,7 @@ function Login(props) { return (
+
)} -
diff --git a/api-catalog-ui/frontend/src/components/Search/SearchCriteria.jsx b/api-catalog-ui/frontend/src/components/Search/SearchCriteria.jsx index 4cd5bf0afe..89e79e97e2 100644 --- a/api-catalog-ui/frontend/src/components/Search/SearchCriteria.jsx +++ b/api-catalog-ui/frontend/src/components/Search/SearchCriteria.jsx @@ -56,7 +56,11 @@ export default class SearchCriteria extends Component { data-testid="search-bar" InputProps={{ disableUnderline: true, - endAdornment: {icon}, + endAdornment: ( + + {icon} + + ), }} placeholder={placeholder} value={criteria} diff --git a/api-catalog-ui/frontend/src/components/Search/_search.scss b/api-catalog-ui/frontend/src/components/Search/_search.scss index caac33e26a..75d5876056 100644 --- a/api-catalog-ui/frontend/src/components/Search/_search.scss +++ b/api-catalog-ui/frontend/src/components/Search/_search.scss @@ -36,8 +36,9 @@ } } } + position: absolute; font-style: italic; - margin-top: 7px; + right: 10px; } .MuiFormControl-root.MuiTextField-root.search-bar { display: contents; @@ -46,17 +47,14 @@ >div { >div { >input { - font-style: italic; - width: auto; + padding-left: 16px; + padding-right: 40px; + padding-top: 16px; } } } } -svg.MuiSvgIcon-root.clear-text-search { - margin-left: 142px; -} #search-icon { - margin-left: 142px; position: relative; float: left; width: auto; diff --git a/api-catalog-ui/frontend/src/components/Search/search.css b/api-catalog-ui/frontend/src/components/Search/search.css index 0ccc34eb3c..6778b26a8f 100644 --- a/api-catalog-ui/frontend/src/components/Search/search.css +++ b/api-catalog-ui/frontend/src/components/Search/search.css @@ -23,10 +23,10 @@ } #search > div > div > input { - margin-left: 40px; - margin-top: 6px; font-style: italic; - width: auto; + padding-left: 10px; + padding-right: 40px; + padding-top: 15px; } svg.MuiSvgIcon-root.clear-text-search { @@ -48,16 +48,13 @@ svg.MuiSvgIcon-root.clear-text-search { } #search-input { + position: absolute; + right: 5px; font-style: italic; - margin-top: 7px; } #search-icon { - margin-left: 142px; - position: relative; - float: left; margin-top: 10px; - width: auto; } @media only screen and (max-width: 530px) { diff --git a/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.css b/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.css index d4fadac66d..e316906b7d 100644 --- a/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.css +++ b/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.css @@ -115,3 +115,19 @@ p.MuiTypography-root.version-text.MuiTypography-body1 { margin: 0; } } + +@media only screen and (max-width: 1208px) { + #compare-button { + margin-left: 54px; + margin-top: 10px; + } +} + +#no-tiles-error { + font-size: 22px; + color: #CC092F; +} + +#no-tiles-error > p { + margin-left: 88px; +} diff --git a/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.jsx b/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.jsx index 7a33064153..6492da1c50 100644 --- a/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.jsx +++ b/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.jsx @@ -12,13 +12,14 @@ import { Component } from 'react'; import Shield from '../ErrorBoundary/Shield/Shield'; import SwaggerContainer from '../Swagger/SwaggerContainer'; import ServiceVersionDiffContainer from '../ServiceVersionDiff/ServiceVersionDiffContainer'; -import countAdditionalContents, { isAPIPortal } from '../../utils/utilFunctions'; +import { isAPIPortal } from '../../utils/utilFunctions'; export default class ServiceTab extends Component { constructor(props) { super(props); this.state = { selectedVersion: null, + previousVersion: null, isDialogOpen: false, }; this.handleDialogClose = this.handleDialogClose.bind(this); @@ -59,7 +60,7 @@ export default class ServiceTab extends Component { currentTileId, tiles, } = this.props; - if (tiles && tiles.length > 0) { + if (tiles && tiles.length > 0 && tiles[0] && tiles[0].services) { tiles[0].services.forEach((service) => { if (service.serviceId === serviceId) { if (service.serviceId !== selectedService.serviceId || selectedTile !== currentTileId) { @@ -118,8 +119,18 @@ export default class ServiceTab extends Component { return apiVersions; } - handleDialogOpen = () => { - this.setState({ isDialogOpen: true, selectedVersion: 'diff' }); + handleDialogOpen = (currentService) => { + const { selectedVersion } = this.state; + if (selectedVersion === null) { + this.setState({ previousVersion: currentService.defaultApiVersion }); + } else { + this.setState({ previousVersion: selectedVersion }); + } + this.setState({ + isDialogOpen: true, + selectedVersion: 'diff', + previousVersion: selectedVersion ?? currentService.defaultApiVersion, + }); }; handleDialogClose = () => { @@ -133,6 +144,9 @@ export default class ServiceTab extends Component { }, tiles, selectedService, + useCasesCounter, + tutorialsCounter, + videosCounter, } = this.props; if (tiles === null || tiles === undefined || tiles.length === 0) { throw new Error('No tile is selected.'); @@ -145,21 +159,24 @@ export default class ServiceTab extends Component { const { containsVersion } = this; const message = 'The API documentation was retrieved but could not be displayed.'; const sso = selectedService.ssoAllInstances ? 'supported' : 'not supported'; - const { useCasesCounter, tutorialsCounter, videosCounter } = countAdditionalContents(currentService); + const apiPortalEnabled = isAPIPortal(); + const additionalContentsPresent = useCasesCounter !== 0 && tutorialsCounter !== 0 && videosCounter !== 0; return ( <> {currentService === null && ( - -

This tile does not contain service "{serviceId}"

+ +

The service ID "{serviceId}" does not match any registered service

)}
- - {selectedService.title} - - {hasHomepage && ( + {!apiPortalEnabled && ( + + {selectedService.title} + + )} + {hasHomepage && !apiPortalEnabled && ( <> {selectedService.status === 'UP' && ( - + Service Homepage )} )} -
- - - {/* eslint-disable-next-line jsx-a11y/label-has-for */} - - {basePath} - - - - - {/* eslint-disable-next-line jsx-a11y/label-has-for */} - - {selectedService.serviceId} - - - - - {/* eslint-disable-next-line jsx-a11y/label-has-for */} - - {sso} - - -
+ {!apiPortalEnabled && ( +
+ + + {/* eslint-disable-next-line jsx-a11y/label-has-for */} + + {basePath} + + + + + {/* eslint-disable-next-line jsx-a11y/label-has-for */} + + {selectedService.serviceId} + + + + + {/* eslint-disable-next-line jsx-a11y/label-has-for */} + + {sso} + + +
+ )}
-
Swagger - - Version - -
-
{containsVersion && currentService && ( + + Version + + )} +
+ {containsVersion && currentService && ( +
- )} - -
+ +
+ )} {selectedVersion !== 'diff' && } {selectedVersion === 'diff' && isDialogOpen && containsVersion && ( )} - {isAPIPortal() && ( + {isAPIPortal() && additionalContentsPresent && ( )}
diff --git a/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.test.jsx b/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.test.jsx index f4d987ea53..45522486ba 100644 --- a/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.test.jsx +++ b/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.test.jsx @@ -33,6 +33,22 @@ const selectedService = { apis: { 'org.zowe v1': { gatewayUrl: 'api/v1' } }, }; +const selectedServiceDown = { + serviceId: 'gateway', + title: 'API Gateway', + description: + 'API Gateway service to route requests to services registered in the API Mediation Layer and provides an API for mainframe security.', + status: 'DOWN', + baseUrl: 'https://localhost:6000', + homePageUrl: 'https://localhost:10010/', + basePath: '/gateway/api/v1', + apiDoc: null, + apiVersions: ['org.zowe v1', 'org.zowe v2'], + defaultApiVersion: ['org.zowe v1'], + ssoAllInstances: true, + apis: { 'org.zowe v1': { gatewayUrl: 'api/v1' } }, +}; + const tiles = { version: '1.0.0', id: 'apimediationlayer', @@ -42,8 +58,10 @@ const tiles = { 'The API Mediation Layer for z/OS internal API services. The API Mediation Layer provides a single point of access to mainframe REST APIs and offers enterprise cloud-like features such as high-availability, scalability, dynamic API discovery, and documentation.', services: [selectedService], }; - describe('>>> ServiceTab component tests', () => { + beforeEach(() => { + process.env.REACT_APP_API_PORTAL = false; + }); it('should display service tab information', () => { const selectService = jest.fn(); const serviceTab = shallow( @@ -97,4 +115,161 @@ describe('>>> ServiceTab component tests', () => { dropDownMenu.children().at(1).simulate('click'); expect(serviceTab.find('[data-testid="version-menu"]').first().text()).toBe('org.zowe v1org.zowe v2'); }); + + it('should throw error when tiles are null', () => { + const selectService = jest.fn(); + expect(() => + shallow( + + ) + ).toThrow('No tile is selected.'); + }); + + it('should throw error when tiles are undefined', () => { + const selectService = jest.fn(); + expect(() => + shallow( + + ) + ).toThrow('No tile is selected.'); + }); + + it('should throw error when tiles are empty', () => { + const selectService = jest.fn(); + expect(() => + shallow( + + ) + ).toThrow('No tile is selected.'); + }); + + it('should display default footer for custom portal in case of additional content', () => { + process.env.REACT_APP_API_PORTAL = true; + const selectService = jest.fn(); + const serviceTab = shallow( + + ); + expect(serviceTab.find('.footer-labels').exists()).toEqual(true); + expect(serviceTab.find('#detail-footer').exists()).toEqual(true); + }); + + it('should not display default footer for custom portal in case there is not additional content', () => { + process.env.REACT_APP_API_PORTAL = true; + const selectService = jest.fn(); + const serviceTab = shallow( + + ); + expect(serviceTab.find('.footer-labels').exists()).toEqual(false); + expect(serviceTab.find('#detail-footer').exists()).toEqual(false); + }); + + it('should display home page link if service down', () => { + process.env.REACT_APP_API_PORTAL = false; + const selectService = jest.fn(); + const serviceTab = shallow( + + ); + expect(serviceTab.find('[data-testid="red-homepage"]').exists()).toEqual(true); + }); + + it('should update state correctly when selectedVersion is null', () => { + process.env.REACT_APP_API_PORTAL = false; + const selectService = jest.fn(); + const wrapper = shallow( + + ); + wrapper.setState({ selectedVersion: null }); + + wrapper.instance().handleDialogOpen(selectedService); + + expect(wrapper.state().isDialogOpen).toEqual(true); + expect(wrapper.state().selectedVersion).toEqual('diff'); + expect(wrapper.state().previousVersion).toEqual(selectedService.defaultApiVersion); + }); + + it('should call handleDialogOpen on button click', () => { + const selectService = jest.fn(); + const wrapper = shallow( + + ); + const handleDialogOpenSpy = jest.spyOn(wrapper.instance(), 'handleDialogOpen'); + + const button = wrapper.find('#compare-button'); + button.simulate('click'); + + expect(handleDialogOpenSpy).toHaveBeenCalledTimes(1); + }); + + it('should disable the button if apiVersions length is less than 2', () => { + const selectService = jest.fn(); + const apiVersions = ['1.0.0']; + selectedService.apiVersions = apiVersions; + const wrapper = shallow( + + ); + wrapper.setState({ apiVersions }); + + const button = wrapper.find('#compare-button'); + + expect(button.prop('disabled')).toEqual(true); + expect(button.prop('style')).toEqual({ + backgroundColor: '#e4e4e4', + color: '#6b6868', + opacity: '0.5', + }); + }); + + it('should enable the button if apiVersions length is greater than or equal to 2', () => { + const selectService = jest.fn(); + const wrapper = shallow( + + ); + const apiVersions = ['org.zowe v1', 'org.zowe v2']; + selectedService.apiVersions = apiVersions; + wrapper.setState({ apiVersions }); + + const button = wrapper.find('#compare-button'); + + expect(button.prop('disabled')).toEqual(false); + expect(button.prop('style')).toEqual({ + backgroundColor: '#fff', + color: '#0056B3', + }); + }); }); diff --git a/api-catalog-ui/frontend/src/components/ServiceTab/_serviceTab.scss b/api-catalog-ui/frontend/src/components/ServiceTab/_serviceTab.scss index 804a61a398..c12ab60b2f 100644 --- a/api-catalog-ui/frontend/src/components/ServiceTab/_serviceTab.scss +++ b/api-catalog-ui/frontend/src/components/ServiceTab/_serviceTab.scss @@ -25,7 +25,6 @@ a.nav-tab { } } p.MuiTypography-root.version-text.MuiTypography-body1 { - color: $color_2; font-size: 14px; font-weight: bold; } @@ -88,8 +87,24 @@ p.MuiTypography-root.version-text.MuiTypography-body1 { } .footer-labels { + margin-bottom: -2px; color: $color_3; font-size: 28px; letter-spacing: 0.5px; } +@media only screen and (max-width: 1208px) { + #compare-button { + margin-left: 54px; + margin-top: 10px; + } +} + +#no-tiles-error { + font-size: 22px; + color: $color_3; +} + +#no-tiles-error > p { + margin-left: 11px; +} diff --git a/api-catalog-ui/frontend/src/components/ServiceVersionDiff/ServiceVersionDiff.css b/api-catalog-ui/frontend/src/components/ServiceVersionDiff/ServiceVersionDiff.css index c471466a80..86819a11a0 100644 --- a/api-catalog-ui/frontend/src/components/ServiceVersionDiff/ServiceVersionDiff.css +++ b/api-catalog-ui/frontend/src/components/ServiceVersionDiff/ServiceVersionDiff.css @@ -6,13 +6,13 @@ .api-diff-form { margin: auto; display: flex; - width: fit-content; + width: auto; } .api-diff-form>p { margin-top: auto; padding-left: 8px; - padding-right: 8px; + padding-right: 18px; font-size: x-large; color: #58606e; } @@ -21,6 +21,10 @@ margin-left: 8px; } +#close-dialog > span > svg { + fill: black; +} + .article { padding: 20px; max-width: 960px; @@ -142,3 +146,7 @@ s, del, .missing, del.comment { .MuiSelect-select { width: 110px; } + +#dialog-title { + margin-top: -8px; +} diff --git a/api-catalog-ui/frontend/src/components/ServiceVersionDiff/ServiceVersionDiff.jsx b/api-catalog-ui/frontend/src/components/ServiceVersionDiff/ServiceVersionDiff.jsx index 4ea1b8a530..a53a4e1c4e 100644 --- a/api-catalog-ui/frontend/src/components/ServiceVersionDiff/ServiceVersionDiff.jsx +++ b/api-catalog-ui/frontend/src/components/ServiceVersionDiff/ServiceVersionDiff.jsx @@ -22,23 +22,28 @@ import { Divider, } from '@material-ui/core'; import CloseIcon from '@material-ui/icons/Close'; -import './ServiceVersionDiff.css'; export default class ServiceVersionDiff extends Component { constructor(props) { - const { version1, version2 } = props; + const { version1, version2, selectedVersion } = props; super(props); this.state = { selectedVersion1: version1 ? { text: version1 } : undefined, selectedVersion2: version2 ? { text: version2 } : undefined, open: props.isDialogOpen, + defaultVersion: selectedVersion, }; this.handleVersion1Change = this.handleVersion1Change.bind(this); this.handleVersion2Change = this.handleVersion2Change.bind(this); } + componentDidMount() { + this.setState({ selectedVersion1: null, selectedVersion2: null }); + } + handleVersion1Change = (event) => { + this.setState({ defaultVersion: null }); this.setState({ selectedVersion1: event.target.value }); }; @@ -71,7 +76,9 @@ export default class ServiceVersionDiff extends Component { { - getDiff(serviceId, selectedVersion1, selectedVersion2); + getDiff(serviceId, this.state.defaultVersion ?? selectedVersion1, selectedVersion2); }} > Show diff --git a/api-catalog-ui/frontend/src/components/ServiceVersionDiff/ServiceVersionDiff.test.jsx b/api-catalog-ui/frontend/src/components/ServiceVersionDiff/ServiceVersionDiff.test.jsx index 03ede6eee9..d571a0fd7a 100644 --- a/api-catalog-ui/frontend/src/components/ServiceVersionDiff/ServiceVersionDiff.test.jsx +++ b/api-catalog-ui/frontend/src/components/ServiceVersionDiff/ServiceVersionDiff.test.jsx @@ -53,4 +53,25 @@ describe('>>> ServiceVersionDiff component tests', () => { serviceVersionDiff.find('[data-testid="diff-button"]').first().simulate('click'); expect(getDiff.mock.calls.length).toBe(1); }); + + it('Should call getDiff when default version', () => { + const getDiff = jest.fn(); + const serviceVersionDiff = shallow( + + ); + + serviceVersionDiff.find('[data-testid="diff-button"]').first().simulate('click'); + expect(getDiff.mock.calls.length).toBe(1); + }); + + it('should set current tile id with default version', () => { + const getDiff = jest.fn(); + const serviceVersionDiff = shallow( + + ); + serviceVersionDiff.setState({ defaultVersion: 'v1' }); + + serviceVersionDiff.find('[data-testid="diff-button"]').first().simulate('click'); + expect(getDiff.mock.calls.length).toBe(1); + }); }); diff --git a/api-catalog-ui/frontend/src/components/ServiceVersionDiff/_serviceVersionDiff.scss b/api-catalog-ui/frontend/src/components/ServiceVersionDiff/_serviceVersionDiff.scss index f5e0cb5c2d..a025fcca80 100644 --- a/api-catalog-ui/frontend/src/components/ServiceVersionDiff/_serviceVersionDiff.scss +++ b/api-catalog-ui/frontend/src/components/ServiceVersionDiff/_serviceVersionDiff.scss @@ -27,7 +27,7 @@ $border-color_1: rgb(200, 209, 224); >p { margin-top: auto; padding-left: 8px; - padding-right: 8px; + padding-right: 18px; font-size: x-large; color: $color_1; } @@ -221,3 +221,13 @@ del.comment { .MuiSelect-select { width: 110px; } + +#dialog-title { + margin-top: -70px; + color: var(--controlText15); +} + +.select-diff { + padding-right: 7px; + margin-left: -4px; +} diff --git a/api-catalog-ui/frontend/src/components/ServicesNavigationBar/ServiceNavigationBar.test.jsx b/api-catalog-ui/frontend/src/components/ServicesNavigationBar/ServiceNavigationBar.test.jsx index 02e6c4bebb..cffa23ed5f 100644 --- a/api-catalog-ui/frontend/src/components/ServicesNavigationBar/ServiceNavigationBar.test.jsx +++ b/api-catalog-ui/frontend/src/components/ServicesNavigationBar/ServiceNavigationBar.test.jsx @@ -33,21 +33,15 @@ const tile = { createdTimestamp: '2018-08-22T08:31:22.948+0000', }; -describe('>>> ServiceNavigationBar component tests', () => { - it('should clear when unmounting', () => { - const clear = jest.fn(); - const serviceNavigationBar = shallow( - - ); - const instance = serviceNavigationBar.instance(); - instance.componentWillUnmount(); - expect(clear).toHaveBeenCalled(); - }); +const match = { + url: '/service', +}; +describe('>>> ServiceNavigationBar component tests', () => { it('should clear when unmounting', () => { const clear = jest.fn(); const serviceNavigationBar = shallow( - + ); const instance = serviceNavigationBar.instance(); instance.componentWillUnmount(); @@ -88,4 +82,21 @@ describe('>>> ServiceNavigationBar component tests', () => { expect(serviceNavigationBar.find('#serviceIdTabs')).toExist(); expect(serviceNavigationBar.find('#serviceIdTabs').text()).toEqual('Product APIs'); }); + + it('should set current tile id', () => { + localStorage.setItem('serviceId', 'apicatalog'); + const storeCurrentTileId = jest.fn(); + const serviceNavigationBar = shallow( + + ); + const instance = serviceNavigationBar.instance(); + instance.handleTabClick('apicatalog'); + expect(storeCurrentTileId).toHaveBeenCalled(); + localStorage.removeItem('serviceId'); + }); }); diff --git a/api-catalog-ui/frontend/src/components/ServicesNavigationBar/ServicesNavigationBar.css b/api-catalog-ui/frontend/src/components/ServicesNavigationBar/ServicesNavigationBar.css index 28419324de..3250f2bee6 100644 --- a/api-catalog-ui/frontend/src/components/ServicesNavigationBar/ServicesNavigationBar.css +++ b/api-catalog-ui/frontend/src/components/ServicesNavigationBar/ServicesNavigationBar.css @@ -1,19 +1,3 @@ -.nav-tabs { - width: auto; - border: 3px solid #F7F9FA; - overflow-y: auto; - display: flex; - align-items: cover; - cursor: pointer; -} - -.nav-tabs:hover { - border-color: #50b7f5; - background-color: #EFEFEF; - color: #50b7f5; - transition: color 100ms ease-out; -} - #search_no_results { color: #1d5bbf; } @@ -25,19 +9,25 @@ align-items: center; gap: 10px; width: auto; + background: var(--white-ffffff, #FFF); + box-shadow: 2px 2px 4px 1px rgba(0, 0, 0, 0.25); } #search2 { height: 47px; background: #FFFFFF; border: 1px solid #C9C8C5; - margin-right: 15px; - margin-bottom: 9px; + margin: 5px 14px 9px -1px; +} + +#search2 .MuiInputBase-root { + width: 100%; } #search2 > div > div > input { - margin-left: 13px; - margin-top: 6px; + padding-left: 10px; + padding-right: 40px; + padding-top: 14px; } #search2> div> div> div> #search-icon { @@ -46,11 +36,55 @@ #serviceIdTabs { margin-bottom: 10px; + margin-left: -4px; } .MuiTab-root :hover { - color: inherit; /* Use the default color on hover */ - background-color: transparent; /* Remove background color on hover */ - font-weight: bold; + background-color: #efefef; + color: #0056B3; + transition: color 100ms ease-out; } +span.MuiTab-wrapper { + display: inline-block; + width: 500px; +} + +.MuiTabs-root { + margin-left: -10px; +} +.MuiTabs-root > div:nth-child(2) { + padding-top: 2px; +} + +.MuiTabs-scroller.MuiTabs-scrollable > div > a { + margin-left: 8px; + margin-right: 2px; + max-width: 370px; + margin-bottom: 5px; + text-transform: none; + font-size: 16px; +} + +.MuiTab-wrapped { + height: 60px; +} +.tab.MuiTab-root { + color: #000; +} + +.custom-tabs .MuiTabs-indicator { + display: none; +} + +.custom-tabs .tabs:hover { + border: 4px solid #0056B3; +} + +.custom-tabs .tabs.Mui-selected { + border: 4px solid #0056B3; +} + +svg.MuiSvgIcon-root.clear-text-search { + margin-bottom: 3px; +} diff --git a/api-catalog-ui/frontend/src/components/ServicesNavigationBar/ServicesNavigationBar.jsx b/api-catalog-ui/frontend/src/components/ServicesNavigationBar/ServicesNavigationBar.jsx index d75e8510af..e472db0cf4 100644 --- a/api-catalog-ui/frontend/src/components/ServicesNavigationBar/ServicesNavigationBar.jsx +++ b/api-catalog-ui/frontend/src/components/ServicesNavigationBar/ServicesNavigationBar.jsx @@ -9,8 +9,8 @@ */ import { Component } from 'react'; -import { Typography } from '@material-ui/core'; -import SideBarLinks from './SideBarLinks'; +import { Tab, Tabs, Tooltip, Typography, withStyles } from '@material-ui/core'; +import { Link as RouterLink } from 'react-router-dom'; import Shield from '../ErrorBoundary/Shield/Shield'; import SearchCriteria from '../Search/SearchCriteria'; @@ -25,10 +25,51 @@ export default class ServicesNavigationBar extends Component { filterText(value); }; + handleTabChange = (event, selectedTab) => { + localStorage.removeItem('serviceId'); + localStorage.setItem('selectedTab', selectedTab); + }; + + handleTabClick = (id) => { + const { storeCurrentTileId, services } = this.props; + const correctTile = services.find((tile) => tile.services.some((service) => service.serviceId === id)); + if (correctTile) { + storeCurrentTileId(correctTile.id); + } + }; + + styles = () => ({ + truncatedTabLabel: { + maxWidth: '323px', + width: 'max-content', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + }); + render() { - const { match, services, searchCriteria, storeCurrentTileId } = this.props; + const { match, services, searchCriteria } = this.props; const hasTiles = services && services.length > 0; const hasSearchCriteria = searchCriteria !== undefined && searchCriteria !== null && searchCriteria.length > 0; + let selectedTab = Number(localStorage.getItem('selectedTab')); + let allServices; + let allServiceIds; + if (hasTiles) { + allServices = services.flatMap((tile) => tile.services); + allServiceIds = allServices.map((service) => service.serviceId); + if (localStorage.getItem('serviceId')) { + const id = localStorage.getItem('serviceId'); + if (allServiceIds.includes(id)) { + selectedTab = allServiceIds.indexOf(id); + } + } + } + const TruncatedTabLabel = withStyles(this.styles)(({ classes, label }) => ( + +
{label}
+
+ )); return (
@@ -43,25 +84,34 @@ export default class ServicesNavigationBar extends Component { Product APIs - {services.map((tile) => - tile.services.map((service) => ( -
- -
- )) - )} {!hasTiles && hasSearchCriteria && ( No services found matching search criteria )} + {hasTiles && ( + + {allServices.map((service, serviceIndex) => ( + this.handleTabClick(service.serviceId)} + key={service.serviceId} + className="tabs" + component={RouterLink} + to={`${match.url}/${service.serviceId}`} + value={serviceIndex} + label={} + wrapped + /> + ))} + + )}
); } diff --git a/api-catalog-ui/frontend/src/components/ServicesNavigationBar/SideBarLinks.jsx b/api-catalog-ui/frontend/src/components/ServicesNavigationBar/SideBarLinks.jsx deleted file mode 100644 index 748d74ce2f..0000000000 --- a/api-catalog-ui/frontend/src/components/ServicesNavigationBar/SideBarLinks.jsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ -import { Tab, Tabs } from '@material-ui/core'; -import { Link as RouterLink } from 'react-router-dom'; - -function SideBarLinks({ storeCurrentTileId, originalTiles, text, match, services }) { - const handleTabClick = (value) => { - const correctTile = originalTiles.find((tile) => tile.services.some((service) => service.serviceId === value)); - if (correctTile) { - storeCurrentTileId(correctTile.id); - } - }; - return ( - - handleTabClick(services)} - value={text} - className="tabs" - component={RouterLink} - to={`${match.url}/${services}`} - label={text} - wrapped - /> - - ); -} -export default SideBarLinks; diff --git a/api-catalog-ui/frontend/src/components/ServicesNavigationBar/SideBarLinks.test.jsx b/api-catalog-ui/frontend/src/components/ServicesNavigationBar/SideBarLinks.test.jsx deleted file mode 100644 index d6e94f48ef..0000000000 --- a/api-catalog-ui/frontend/src/components/ServicesNavigationBar/SideBarLinks.test.jsx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -import { shallow } from 'enzyme'; -import { Tab } from '@material-ui/core'; -import { Link as RouterLink } from 'react-router-dom'; -import SideBarLinks from './SideBarLinks'; - -const tile = { - version: '1.0.0', - id: 'apicatalog', - title: 'API Mediation Layer for z/OS internal API services', - status: 'UP', - description: 'lkajsdlkjaldskj', - services: [ - { - serviceId: 'apicatalog', - title: 'API Catalog', - description: - 'API ML Microservice to locate and display API documentation for API ML discovered microservices', - status: 'UP', - secured: false, - }, - { - serviceId: 'gateway', - title: 'API Gateway', - description: - 'API ML Microservice to locate and display API documentation for API ML discovered microservices', - status: 'UP', - secured: false, - }, - ], - totalServices: 1, - activeServices: 1, - lastUpdatedTimestamp: '2018-08-22T08:32:03.110+0000', - createdTimestamp: '2018-08-22T08:31:22.948+0000', -}; - -describe('>>> SideBarLinks component tests', () => { - it('should call storeCurrentTileId function when a tab is clicked', () => { - const storeCurrentTileIdMock = jest.fn(); - const matchMock = { url: '/example' }; - const servicesMock = 'apicatalog'; - const wrapper = shallow( - - ); - - wrapper.find(Tab).simulate('click'); - - expect(storeCurrentTileIdMock).toHaveBeenCalledWith('apicatalog'); - }); - - it('should render a Tab component with the correct props', () => { - const storeCurrentTileIdMock = jest.fn(); - const matchMock = { url: '/example' }; - const servicesMock = 'service1'; - const wrapper = shallow( - - ); - - expect(wrapper.find(Tab)).toHaveLength(1); - - expect(wrapper.find(Tab).props()).toMatchObject({ - onClick: expect.any(Function), - value: 'Tab Text', - className: 'tabs', - component: RouterLink, - to: '/example/service1', - label: 'Tab Text', - wrapped: true, - }); - }); -}); diff --git a/api-catalog-ui/frontend/src/components/ServicesNavigationBar/_serviceNavigationBar.scss b/api-catalog-ui/frontend/src/components/ServicesNavigationBar/_serviceNavigationBar.scss index 6b660b14bf..bb20c768f6 100644 --- a/api-catalog-ui/frontend/src/components/ServicesNavigationBar/_serviceNavigationBar.scss +++ b/api-catalog-ui/frontend/src/components/ServicesNavigationBar/_serviceNavigationBar.scss @@ -2,65 +2,37 @@ $color_1: #50b7f5; $background-color_1: #EFEFEF; $color_2: #CC092F; -.MuiTab-root { - font-weight: normal; // Set the default font weight - transition: font-weight 0.3s; // Add transition for smooth effect - - &:hover { - color: inherit; /* Use the default color on hover */ - background-color: transparent; /* Remove background color on hover */ - font-weight: bold; // Set font weight to bold on hover - } -} -.MuiTab-root .MuiTab-wrapper:hover { - color: inherit; /* Use the default color on hover */ - text-decoration: none; /* Remove underline on hover */ -} - -.nav-tabs { - background: var(--white-ffffff, #FFF); - box-shadow: 2px 2px 4px 0px rgba(0, 0, 0, 0.25); - .tabs { - text-transform: none; - font-size: 16px; - color: black; - &:hover { - color: black; - } - } - width: auto; - border: 3px solid #F7F9FA; - overflow-y: auto; - display: flex; - color: black; - align-items: cover; - cursor: pointer; - &:hover { - border-color: $color_2; - transition: color 100ms ease-out; - } -} - #search2 { + margin-top: 10px; height: 47px; background: #FFFFFF; border: 1px solid #C9C8C5; - margin-right: 15px; + margin-right: 13px; margin-bottom: 9px; + + .MuiInput-root { + width: 100%; + } >div { >div { >input { - margin-left: 13px; - margin-top: 6px; + padding-left: 16px; + padding-right: 40px; + padding-top: 14px; } >div { >#search-icon { margin-top: 7px; } + margin-right: 0; + &#search-input { + right: 5px; + } } } } } + #serviceIdTabs { margin-bottom: 10px; font-weight: bold; @@ -71,4 +43,63 @@ $color_2: #CC092F; color: $color_2; } +.MuiTab-wrapper { + width: 100%; + text-align: initial; + flex-direction: column; +} +span.MuiTab-wrapper { + display: inline-block; +} + +.MuiTabs-root { + margin-left: 39px; +} + +.MuiTabs-scroller.MuiTabs-scrollable > div > a { + background: var(--white-ffffff, #FFF); + max-width: 353px; + margin-bottom: 5px; + text-transform: none; + font-size: 16px; +} + +.MuiTabs-root.custom-tabs.MuiTabs-vertical { + margin-left: auto; +} + +svg.MuiSvgIcon-root.clear-text-search { + margin-left: 120px; + margin-bottom: 3px; +} + + +.tabs { + height: 100%; + display: flex; + align-items: center; + gap: 10px; + background: var(--white-ffffff, #FFF); + box-shadow: 2px 2px 4px 0px rgba(0, 0, 0, 0.25); +} + +.MuiTab-wrapped { + height: 60px; +} + +.custom-tabs .MuiTabs-indicator { + display: none; +} + +.custom-tabs .tabs:hover { + border: 4px solid $color_2; +} + +.custom-tabs .tabs.Mui-selected { + border: 4px solid $color_2; +} + +.MuiTabs-flexContainerVertical { + margin-bottom: 7px; +} diff --git a/api-catalog-ui/frontend/src/components/Swagger/Swagger.css b/api-catalog-ui/frontend/src/components/Swagger/Swagger.css index bac857abc1..5f2fdb5cf3 100644 --- a/api-catalog-ui/frontend/src/components/Swagger/Swagger.css +++ b/api-catalog-ui/frontend/src/components/Swagger/Swagger.css @@ -1753,3 +1753,7 @@ button.opblock-summary-control { svg.svg-assets { display: none; } + +#no-doc_message { + color: #de1b1b; +} diff --git a/api-catalog-ui/frontend/src/components/Swagger/SwaggerUI.jsx b/api-catalog-ui/frontend/src/components/Swagger/SwaggerUI.jsx index 6b72201705..72e0705e20 100644 --- a/api-catalog-ui/frontend/src/components/Swagger/SwaggerUI.jsx +++ b/api-catalog-ui/frontend/src/components/Swagger/SwaggerUI.jsx @@ -96,7 +96,7 @@ export default class SwaggerUI extends Component { setSwaggerState = () => { const { selectedService, selectedVersion } = this.props; let codeSnippets = null; - if (selectedService.apis.length !== 0) { + if (selectedService && 'apis' in selectedService && selectedService.apis && selectedService.apis.length !== 0) { if ( selectedService.apis[selectedVersion] !== null && selectedService.apis[selectedVersion] !== undefined && @@ -180,7 +180,7 @@ export default class SwaggerUI extends Component {
{error && (
-

+

API documentation could not be retrieved. There may be something wrong in your Swagger definition. Please review the values of 'schemes', 'host' and 'basePath'.

diff --git a/api-catalog-ui/frontend/src/components/Swagger/_swagger.scss b/api-catalog-ui/frontend/src/components/Swagger/_swagger.scss index f5756d57f4..09dd7ec05f 100644 --- a/api-catalog-ui/frontend/src/components/Swagger/_swagger.scss +++ b/api-catalog-ui/frontend/src/components/Swagger/_swagger.scss @@ -36,6 +36,7 @@ body { .opblock-summary-method { background: var( --success10 ); + width: 50px; } } &[aria-label*='get'], @@ -45,6 +46,7 @@ body { .opblock-summary-method { background: var( --info10 ); + width: 50px; } } &[aria-label*='delete'], @@ -54,6 +56,7 @@ body { .opblock-summary-method { background: var( --critical10 ); + width: 50px; } } &[aria-label*='put'], @@ -63,6 +66,7 @@ body { .opblock-summary-method { background: var( --warn10 ); + width: 50px; } } @@ -129,7 +133,7 @@ body { justify-content: space-between; > * { - + &:first-child { flex: 100%; } @@ -174,4 +178,9 @@ body { } } }//end opblock-body -} \ No newline at end of file +} + +#no-doc_message { + margin-left: -41px; + color: #de1b1b; +} diff --git a/api-catalog-ui/frontend/src/components/Tile/Tile.jsx b/api-catalog-ui/frontend/src/components/Tile/Tile.jsx index 1ea176843e..a4593ae075 100644 --- a/api-catalog-ui/frontend/src/components/Tile/Tile.jsx +++ b/api-catalog-ui/frontend/src/components/Tile/Tile.jsx @@ -56,6 +56,7 @@ export default class Tile extends Component { const tileRoute = `/service/${service.serviceId}`; storeCurrentTileId(tile.id); history.push(tileRoute); + localStorage.setItem('serviceId', service.serviceId); }; render() { diff --git a/api-catalog-ui/frontend/src/components/Tile/_tile.scss b/api-catalog-ui/frontend/src/components/Tile/_tile.scss index 5f933eb63a..fe25a40ffe 100644 --- a/api-catalog-ui/frontend/src/components/Tile/_tile.scss +++ b/api-catalog-ui/frontend/src/components/Tile/_tile.scss @@ -59,11 +59,6 @@ body { } } - #media-icons { - position: absolute; - right: 186px; - } - #videos { margin-right: 120px; width: 30px; @@ -74,6 +69,10 @@ body { height: 20px; } + .MuiCardContent-root.tile { + display: flex; + } + @media only screen and (max-width: 1500px) { #media-icons { position: relative; @@ -117,3 +116,30 @@ body { margin-right: 5px; } } + +/* CSS styles for the container holding all the tiles */ +.grid-container { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; +} + +/* CSS styles for each individual tile */ +.grid-item { + flex: 0 0 calc(25% - 16px); + margin: 8px; + position: relative; /* Add relative positioning to the tile */ +} + +/* CSS styles for media-icons group within each tile */ +#media-icons { + margin-right: -102px; + display: flex; + align-items: center; + justify-content: flex-end; + position: absolute; /* Use absolute positioning for the icons container */ + right: 0; /* Align the icons container to the right side of the tile */ + top: 50%; /* Vertically center the icons container */ + transform: translateY(-50%); /* Correct the vertical alignment */ +} + diff --git a/api-catalog-ui/frontend/src/components/Wizard/DialogDropdown.jsx b/api-catalog-ui/frontend/src/components/Wizard/DialogDropdown.jsx index 548aa7b150..3c315dce9f 100644 --- a/api-catalog-ui/frontend/src/components/Wizard/DialogDropdown.jsx +++ b/api-catalog-ui/frontend/src/components/Wizard/DialogDropdown.jsx @@ -11,7 +11,6 @@ import { Component } from 'react'; import { Button, Menu, MenuItem } from '@material-ui/core'; import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; -import './wizard.css'; import { isAPIPortal } from '../../utils/utilFunctions'; export default class DialogDropdown extends Component { diff --git a/api-catalog-ui/frontend/src/components/Wizard/WizardDialog.jsx b/api-catalog-ui/frontend/src/components/Wizard/WizardDialog.jsx index 2606fcfc34..7c934b8ecb 100644 --- a/api-catalog-ui/frontend/src/components/Wizard/WizardDialog.jsx +++ b/api-catalog-ui/frontend/src/components/Wizard/WizardDialog.jsx @@ -11,7 +11,6 @@ import yaml from 'js-yaml'; import * as YAML from 'yaml'; import { Component } from 'react'; import { Dialog, DialogContent, DialogContentText, DialogTitle, DialogActions, IconButton } from '@material-ui/core'; -import './wizard.css'; import WizardNavigationContainer from './WizardComponents/WizardNavigationContainer'; export default class WizardDialog extends Component { diff --git a/api-catalog-ui/frontend/src/components/Wizard/wizard.css b/api-catalog-ui/frontend/src/components/Wizard/wizard.css index 58871e192f..9b54fb426e 100644 --- a/api-catalog-ui/frontend/src/components/Wizard/wizard.css +++ b/api-catalog-ui/frontend/src/components/Wizard/wizard.css @@ -22,6 +22,7 @@ #wizard-navigation { min-height: 20em; + margin-left: auto; } #wizard-navigation > div { @@ -153,7 +154,7 @@ label.MuiFormLabel-root.MuiInputLabel-root.MuiInputLabel-formControl.MuiInputLab display: none; } -#yaml-upload { +#yaml-upload { display: flex; justify-content: center; align-items: center; diff --git a/api-catalog-ui/frontend/src/index.css b/api-catalog-ui/frontend/src/index.css index f54eb631f9..a6a38ebeed 100644 --- a/api-catalog-ui/frontend/src/index.css +++ b/api-catalog-ui/frontend/src/index.css @@ -10,8 +10,10 @@ @import "../src/components/DetailPage/ReactRouterTabs.css"; @import "../src/components/Swagger/Swagger.css"; @import "../src/components/ServiceTab/ServiceTab.css"; +@import "../src/components/ServiceVersionDiff/ServiceVersionDiff.css"; @import "../src/components/PageNotFound/PageNotFound.css"; @import "../src/components/ServicesNavigationBar/ServicesNavigationBar.css"; +@import "../src/components/Wizard/wizard.css"; body { margin: 0; padding: 0; diff --git a/api-catalog-ui/frontend/src/selectors/selectors.jsx b/api-catalog-ui/frontend/src/selectors/selectors.jsx index 63bccc2b12..41a166bb58 100644 --- a/api-catalog-ui/frontend/src/selectors/selectors.jsx +++ b/api-catalog-ui/frontend/src/selectors/selectors.jsx @@ -21,11 +21,7 @@ function filterService(searchCriteria, service) { if (!service.title) { return false; } - let matchDoc = false; - if (service.apiDoc) { - matchDoc = service.apiDoc.toLowerCase().includes(searchCriteria.toLowerCase()); - } - return service.title.toLowerCase().includes(searchCriteria.toLowerCase()) || matchDoc; + return service.title.toLowerCase().includes(searchCriteria.toLowerCase()); } function compareResult(searchCriteria, tile, filteredServices) { diff --git a/api-catalog-ui/frontend/src/utils/utilFunctions.js b/api-catalog-ui/frontend/src/utils/utilFunctions.js index dc51a77ee9..30066c981e 100644 --- a/api-catalog-ui/frontend/src/utils/utilFunctions.js +++ b/api-catalog-ui/frontend/src/utils/utilFunctions.js @@ -30,6 +30,17 @@ export default function countAdditionalContents(service) { return { useCasesCounter, tutorialsCounter, videosCounter }; } +function setButtonsColor(wizardButton, uiConfig, refreshButton) { + const color = + uiConfig.headerColor === 'white' || uiConfig.headerColor === '#FFFFFF' ? 'black' : uiConfig.headerColor; + if (wizardButton) { + wizardButton.style.setProperty('color', color); + } + if (refreshButton) { + refreshButton.style.setProperty('color', color); + } +} + function setMultipleElements(uiConfig) { if (uiConfig.headerColor) { const divider = document.getElementById('separator2'); @@ -37,6 +48,8 @@ function setMultipleElements(uiConfig) { const title1 = document.getElementById('title'); const swaggerLabel = document.getElementById('swagger-label'); const header = document.getElementsByClassName('header'); + const wizardButton = document.querySelector('#onboard-wizard-button > span.MuiButton-label'); + const refreshButton = document.querySelector('#refresh-api-button > span.MuiIconButton-label'); if (header && header.length > 0) { header[0].style.setProperty('background-color', uiConfig.headerColor); } @@ -52,6 +65,7 @@ function setMultipleElements(uiConfig) { if (logoutButton) { logoutButton.style.setProperty('color', uiConfig.headerColor); } + setButtonsColor(wizardButton, uiConfig, refreshButton); } } @@ -79,6 +93,33 @@ function fetchImagePath() { }); } +function handleWhiteHeader(uiConfig) { + const goBackButton = document.querySelector('#go-back-button'); + const swaggerLabel = document.getElementById('swagger-label'); + const title = document.getElementById('title'); + const productTitle = document.getElementById('product-title'); + if (uiConfig.headerColor === 'white' || uiConfig.headerColor === '#FFFFFF') { + if (uiConfig.docLink) { + const docText = document.querySelector('#internal-link'); + if (docText) { + docText.style.color = 'black'; + } + } + if (goBackButton) { + goBackButton.style.color = 'black'; + } + if (swaggerLabel) { + swaggerLabel.style.color = 'black'; + } + if (title) { + title.style.color = 'black'; + } + if (productTitle) { + productTitle.style.color = 'black'; + } + } +} + /** * Custom the UI look to match the setup from the service metadata * @param uiConfig the configuration to customize the UI @@ -96,6 +137,8 @@ export const customUIStyle = async (uiConfig) => { const img = await fetchImagePath(); link.href = img; logo.src = img; + logo.style.height = 'auto'; + logo.style.width = 'auto'; } if (uiConfig.backgroundColor) { @@ -131,6 +174,7 @@ export const customUIStyle = async (uiConfig) => { description.style.color = uiConfig.textColor; } } + handleWhiteHeader(uiConfig); }; export const isAPIPortal = () => process.env.REACT_APP_API_PORTAL === 'true'; diff --git a/api-catalog-ui/frontend/src/utils/utilFunctions.test.js b/api-catalog-ui/frontend/src/utils/utilFunctions.test.js index 11d7768e9f..74a57c8317 100644 --- a/api-catalog-ui/frontend/src/utils/utilFunctions.test.js +++ b/api-catalog-ui/frontend/src/utils/utilFunctions.test.js @@ -30,6 +30,15 @@ describe('>>> Util Functions tests', () => {
+ +
+
+ +
+
+ +
+

`; }); @@ -68,12 +77,16 @@ describe('>>> Util Functions tests', () => { const detailPage = document.getElementsByClassName('content')[0]; const description = document.getElementById('description'); const link = document.querySelector("link[rel~='icon']"); + const wizardButton = document.querySelector('#onboard-wizard-button > span.MuiButton-label'); + const refreshButton = document.querySelector('#refresh-api-button > span.MuiIconButton-label'); expect(logo.src).toContain('img-url'); expect(link.href).toContain('img-url'); expect(header.style.getPropertyValue('background-color')).toBe('red'); expect(divider.style.getPropertyValue('background-color')).toBe('red'); expect(title.style.getPropertyValue('color')).toBe('red'); expect(swaggerLabel.style.getPropertyValue('color')).toBe('red'); + expect(wizardButton.style.getPropertyValue('color')).toBe('red'); + expect(refreshButton.style.getPropertyValue('color')).toBe('red'); expect(logoutButton.style.getPropertyValue('color')).toBe('red'); expect(homepage.style.backgroundColor).toBe('blue'); expect(homepage.style.backgroundImage).toBe('none'); @@ -85,4 +98,50 @@ describe('>>> Util Functions tests', () => { jest.restoreAllMocks(); global.fetch.mockRestore(); }); + + it('should handle elements in case of white header', async () => { + const uiConfig = { + logo: '/path/img.png', + headerColor: 'white', + backgroundColor: 'blue', + fontFamily: 'Arial', + textColor: 'black', + docLink: 'doc|doc.com', + }; + + global.URL.createObjectURL = jest.fn().mockReturnValue('img-url'); + global.fetch = mockFetch(); + await customUIStyle(uiConfig); + const header = document.getElementsByClassName('header')[0]; + const title = document.getElementById('title'); + const productTitle = document.getElementById('product-title'); + const docLink = document.getElementById('internal-link'); + const swaggerLabel = document.getElementById('swagger-label'); + const wizardButton = document.querySelector('#onboard-wizard-button > span.MuiButton-label'); + const refreshButton = document.querySelector('#refresh-api-button > span.MuiIconButton-label'); + const link = document.querySelector("link[rel~='icon']"); + const tileLabel = document.querySelector('p#tileLabel'); + expect(link.href).toContain('img-url'); + expect(header.style.getPropertyValue('background-color')).toBe('white'); + expect(title.style.getPropertyValue('color')).toBe('black'); + expect(productTitle.style.getPropertyValue('color')).toBe('black'); + expect(docLink.style.getPropertyValue('color')).toBe('black'); + expect(swaggerLabel.style.getPropertyValue('color')).toBe('black'); + expect(wizardButton.style.getPropertyValue('color')).toBe('black'); + expect(refreshButton.style.getPropertyValue('color')).toBe('black'); + expect(tileLabel.style.getPropertyValue('font-family')).toBe('Arial'); + expect(document.documentElement.style.backgroundColor).toBe('blue'); + // Clean up the mocks + jest.restoreAllMocks(); + global.fetch.mockRestore(); + }); + + it('should return network error when fetching image', async () => { + const uiConfig = { + logo: '/wrong-path/img.png', + }; + + global.fetch = () => Promise.resolve({ ok: false, status: 404 }); + await expect(customUIStyle(uiConfig)).rejects.toThrow('Network response was not ok'); + }); }); diff --git a/api-catalog-ui/frontend/src/webflow.css b/api-catalog-ui/frontend/src/webflow.css index e4d6ce4d98..9c51b7dd11 100644 --- a/api-catalog-ui/frontend/src/webflow.css +++ b/api-catalog-ui/frontend/src/webflow.css @@ -227,7 +227,7 @@ td, th { } html { - height: 100% + height: 100%; } body { @@ -2997,7 +2997,7 @@ html.w-mod-js *[data-ix="doc-load-hidden"] { @media only screen and (max-width: 600px) { .header { - width: 600px; + width: max-content; } }