diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 4201c3b14..3e05b81aa 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -7,9 +7,9 @@ assignees: '' --- -- [] check screenshots -- [] test-click through app, reports -- [] check docs -- [] gather changelog -- [] merge to master and tag -- [] AFTER release increment pom and package.json versions +- [ ] check screenshots +- [ ] test-click through app, reports +- [ ] check docs +- [ ] gather changelog +- [ ] merge to master and tag +- [ ] AFTER release increment pom and package.json versions diff --git a/docs/gui.png b/docs/gui.png index fecb1621f..c6beb3af6 100644 Binary files a/docs/gui.png and b/docs/gui.png differ diff --git a/nivio-demo/readme.md b/nivio-demo/readme.md index 3e6be0637..0ea40b809 100644 --- a/nivio-demo/readme.md +++ b/nivio-demo/readme.md @@ -2,7 +2,15 @@ This folder contains sources to construct a demo landscape for [https://github.com/dedica-team/nivio] -## Usage +## Quick Start + +Just run + + DEMO=1 docker-compose up + +and then head to http://localhost:8080 + +## Custom Input ### Setup @@ -20,7 +28,7 @@ You can try one of these: php -S 127.0.0.1:3000 ruby -run -ehttpd . -p3000 -### Running the Demo manually +### Running the Demo manually with specific files * First create a landscape: diff --git a/pom.xml b/pom.xml index d06aa40b0..fb147b221 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ de.bonndan nivio - 0.4.2 + 0.4.3 org.springframework.boot diff --git a/src/main/app/package.json b/src/main/app/package.json index 32c892178..92eb38498 100644 --- a/src/main/app/package.json +++ b/src/main/app/package.json @@ -1,6 +1,6 @@ { "name": "nivio", - "version": "0.4.2", + "version": "0.4.3", "private": true, "homepage": "./", "dependencies": { @@ -11,8 +11,8 @@ "@stomp/stompjs": "^5.4.4", "@types/dateformat": "^3.0.1", "@types/jest": "^26.0.24", - "@types/node": "^14.17.18", - "@types/react": "^16.14.15", + "@types/node": "^14.17.29", + "@types/react": "^16.14.20", "@types/react-dom": "^16.9.14", "@types/react-html-parser": "^2.0.2", "@types/react-router-dom": "^5.3.0", diff --git a/src/main/app/src/App.tsx b/src/main/app/src/App.tsx index ecdf07773..ab5fc8f20 100644 --- a/src/main/app/src/App.tsx +++ b/src/main/app/src/App.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { HashRouter as Router, Route, Switch } from 'react-router-dom'; import LandscapeOverview from './Components/Landscape/Overview/Overview'; @@ -27,7 +27,6 @@ interface Index { } const App: React.FC = () => { - const [sidebarContent, setSidebarContent] = useState([]); const [pageTitle, setPageTitle] = useState(''); const [logo, setLogo] = useState(''); const [message, setMessage] = useState(''); @@ -94,19 +93,12 @@ const App: React.FC = () => { - + ( { ( - - )} + render={(props) => } /> ( - - )} + render={(props) => } /> diff --git a/src/main/app/src/Components/Landscape/Dashboard/KPIConfigLayout.tsx b/src/main/app/src/Components/Landscape/Dashboard/KPIConfigLayout.tsx index 4c7e123bb..8ab598134 100644 --- a/src/main/app/src/Components/Landscape/Dashboard/KPIConfigLayout.tsx +++ b/src/main/app/src/Components/Landscape/Dashboard/KPIConfigLayout.tsx @@ -65,20 +65,18 @@ const KPIConfigLayout: React.FC = ({ name, kpi }) => { } return ( - - + + {name} + } aria-controls={'panel_kpi' + name + 'bh-content'} id={'panel_kpi' + name + 'bh-header'} > - {name} + {kpi.description} - -
{kpi.description}
+
- {ranges.length ? ( {ranges} diff --git a/src/main/app/src/Components/Landscape/Dashboard/StatusBarLayout.tsx b/src/main/app/src/Components/Landscape/Dashboard/StatusBarLayout.tsx index 11c0eea28..ddf16a60a 100644 --- a/src/main/app/src/Components/Landscape/Dashboard/StatusBarLayout.tsx +++ b/src/main/app/src/Components/Landscape/Dashboard/StatusBarLayout.tsx @@ -1,26 +1,23 @@ -import React, { useContext, useState } from 'react'; +import React, { useContext } from 'react'; import { IGroup } from '../../../interfaces'; import StatusChip from '../../StatusChip/StatusChip'; import Button from '@material-ui/core/Button'; import { AppBar, - Card, - CardHeader, + Box, Tab, Table, TableBody, TableCell, TableRow, Tabs, + Typography, } from '@material-ui/core'; import { LandscapeContext } from '../../../Context/LandscapeContext'; -import componentStyles from '../../../Resources/styling/ComponentStyles'; -import IconButton from '@material-ui/core/IconButton'; -import { Close, Settings, Warning } from '@material-ui/icons'; +import { Settings, Warning } from '@material-ui/icons'; import ItemAvatar from '../Modals/Item/ItemAvatar'; import GroupAvatar from '../Modals/Group/GroupAvatar'; import { a11yProps, TabPanel } from '../Utils/TabUtils'; -import CardContent from '@material-ui/core/CardContent'; import KPIConfigLayout from './KPIConfigLayout'; interface Props { @@ -33,8 +30,6 @@ interface Props { */ const StatusBarLayout: React.FC = ({ onItemClick, onGroupClick }) => { const context = useContext(LandscapeContext); - const componentClasses = componentStyles(); - const [visible, setVisible] = useState(true); const [currentTab, setCurrentTab] = React.useState(0); const getItems = (group: IGroup) => { @@ -122,8 +117,6 @@ const StatusBarLayout: React.FC = ({ onItemClick, onGroupClick }) => { setCurrentTab(newValue); }; - if (!visible) return null; - const kpiConfig = context.landscape?.kpis; let kpis: JSX.Element[] = []; if (kpiConfig) { @@ -136,19 +129,10 @@ const StatusBarLayout: React.FC = ({ onItemClick, onGroupClick }) => { } return ( - - { - setVisible(false); - }} - > - - - } - /> + +
+ Status +
= ({ onItemClick, onGroupClick }) => { /> - - -
- - {context.landscape ? getGroups(context.landscape.groups) : null} - {context.landscape?.groups.map((group) => getItems(group))} - -
- - - {kpis} - - - + + + + {context.landscape ? getGroups(context.landscape.groups) : null} + {context.landscape?.groups.map((group) => getItems(group))} + +
+
+ + {kpis} + +
); }; diff --git a/src/main/app/src/Components/Landscape/Map/Map.tsx b/src/main/app/src/Components/Landscape/Map/Map.tsx index 8d1d77a06..fc56bac90 100644 --- a/src/main/app/src/Components/Landscape/Map/Map.tsx +++ b/src/main/app/src/Components/Landscape/Map/Map.tsx @@ -34,6 +34,7 @@ import { Value, } from 'react-svg-pan-zoom'; +const sidebarWidth = 280; const useStyles = makeStyles((theme: Theme) => createStyles({ menuIcon: { @@ -44,11 +45,22 @@ const useStyles = makeStyles((theme: Theme) => top: 20, backgroundColor: darken(theme.palette.primary.main, 0.2), }, + sideBar: { + position: 'absolute', + right: 0, + top: 5, + width: sidebarWidth, + overflow: 'auto', + maxHeight: 'calc(100vh - 50px)', + zIndex: 5000, + }, + content: { + position: 'relative', + }, }) ); interface Props { - setSidebarContent: Function; setPageTitle: Function; } @@ -63,20 +75,20 @@ interface SVGData { /** * Displays a chosen landscape as interactive SVG * - * @param setSidebarContent function to set sidebar/drawer content * @param setLocateFunction function to use to find an item. make sure to pass an anon func returning the actually used function * @param setPageTitle can be used to set the page title in parent state */ -const Map: React.FC = ({ setSidebarContent, setPageTitle }) => { +const Map: React.FC = ({ setPageTitle }) => { const classes = useStyles(); // It wants a value or null but if we defined it as null it throws an error that shouldn't use null // In their own documentation, they initialize it with {}, but that will invoke a typescript error // @ts-ignore const [value, setValue] = useState({}); const [data, setData] = useState(null); + const [sidebarContent, setSidebarContent] = useState([]); const [renderWithTransition, setRenderWithTransition] = useState(false); const [highlightElement, setHighlightElement] = useState(null); - const [visualFocus, setVisualFocus] = useState(null); + const [visualFocus, setVisualFocus] = useState(null); const { identifier } = useParams<{ identifier: string }>(); const [isFirstRender, setIsFirstRender] = useState(true); @@ -116,13 +128,14 @@ const Map: React.FC = ({ setSidebarContent, setPageTitle }) => { if (fqi && landscapeContext.landscape) { let item = getItem(landscapeContext.landscape, fqi); - if (item) - setSidebarContent( - ); + } } }; @@ -132,7 +145,10 @@ const Map: React.FC = ({ setSidebarContent, setPageTitle }) => { if (fqi && landscapeContext.landscape) { let group = getGroup(landscapeContext.landscape, fqi); - if (group) setSidebarContent(); + if (group) { + // @ts-ignore + setSidebarContent(); + } } }; @@ -182,8 +198,8 @@ const Map: React.FC = ({ setSidebarContent, setPageTitle }) => { if (source && target && dataTarget) { const relId = source.fullyQualifiedIdentifier + ';' + dataTarget; let relation = source.relations[relId]; - setSidebarContent( - = ({ setSidebarContent, setPageTitle }) => { } return ( -
- {isZoomed && ( - { - // @ts-ignore - setValue(fitToViewer(value, 'center', 'center')); - setIsZoomed(false); - }} - size={'small'} - > - - - )} - - - - - - } - render={(content: ReactElement[]) => ( - { - setIsZoomed(true); +
+
+ {isZoomed && ( + { + // @ts-ignore + setValue(fitToViewer(value, 'center', 'center')); + setIsZoomed(false); }} - tool={TOOL_AUTO} - onChangeValue={(newValue: Value) => setValue(newValue)} - onChangeTool={() => {}} - value={value} - className={`ReactSVGPanZoom ${renderWithTransition ? 'with-transition' : ''}`} + size={'small'} > - - {content} - - + + )} - /> + + + + + + } + render={(content: ReactElement[]) => ( + { + setIsZoomed(true); + }} + tool={TOOL_AUTO} + onChangeValue={(newValue: Value) => setValue(newValue)} + onChangeTool={() => {}} + value={value} + className={`ReactSVGPanZoom ${renderWithTransition ? 'with-transition' : ''}`} + > + + {content} + + + )} + /> +
+
{sidebarContent}
); } diff --git a/src/main/app/src/Components/Landscape/Modals/Group/Group.tsx b/src/main/app/src/Components/Landscape/Modals/Group/Group.tsx index b11f96044..4915b5718 100644 --- a/src/main/app/src/Components/Landscape/Modals/Group/Group.tsx +++ b/src/main/app/src/Components/Landscape/Modals/Group/Group.tsx @@ -46,7 +46,10 @@ const Group: React.FC = ({ group }) => { } }} > - +   {item.identifier} diff --git a/src/main/app/src/Components/Landscape/Modals/Group/GroupAvatar.tsx b/src/main/app/src/Components/Landscape/Modals/Group/GroupAvatar.tsx index fa82f8c43..bd4564403 100644 --- a/src/main/app/src/Components/Landscape/Modals/Group/GroupAvatar.tsx +++ b/src/main/app/src/Components/Landscape/Modals/Group/GroupAvatar.tsx @@ -1,4 +1,4 @@ -import { IGroup } from '../../../../interfaces'; +import {IGroup} from '../../../../interfaces'; import React from 'react'; import Avatar from '@material-ui/core/Avatar'; import componentStyles from '../../../../Resources/styling/ComponentStyles'; @@ -30,8 +30,9 @@ const GroupAvatar: React.FC = ({ group, statusColor }) => { title={'Click to highlight the group.'} style={{ backgroundColor: '#' + group.color, - paddingTop: 6, + paddingLeft: 1, }} + src={group.icon} > {group.identifier[0].toUpperCase()} diff --git a/src/main/app/src/Components/Landscape/Modals/Item/Item.test.tsx b/src/main/app/src/Components/Landscape/Modals/Item/Item.test.tsx index 7dd803616..c3fbfad37 100644 --- a/src/main/app/src/Components/Landscape/Modals/Item/Item.test.tsx +++ b/src/main/app/src/Components/Landscape/Modals/Item/Item.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, waitFor } from '@testing-library/react'; +import { fireEvent, getByTitle, queryByText, render, waitFor } from '@testing-library/react'; import * as APIClient from '../../../../utils/API/APIClient'; import Item from './Item'; import { IItem } from '../../../../interfaces'; @@ -25,13 +25,14 @@ describe('', () => { contact: 'foo', relations: Irelations, interfaces: [], - labels: { foo: 'foo' }, + labels: { 'framework.spring boot': '2.2', 'team': 'ops guys' }, type: 'foo', fullyQualifiedIdentifier: 'foo', tags: [], color: 'foo', icon: 'foo', _links: { homepage: { href: 'http://acme.com' } }, + networks: ['vpn'], }; it('should avoid displaying undefined and null value', () => { @@ -57,4 +58,22 @@ describe('', () => { await waitFor(() => expect(mock).toHaveBeenCalledTimes(1)); await waitFor(() => expect(getByText('homepage')).toBeInTheDocument()); }); + + it('should display networks, frameworks and other labels', async () => { + //given + const mock = jest.spyOn(APIClient, 'get'); + mock.mockReturnValue(Promise.resolve(useItem)); + + //when + const { container, queryByText } = render(); + fireEvent.click(getByTitle(container, 'API / Interfaces')); + + console.log(mock); + //then + await waitFor(() => expect(mock).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(queryByText('vpn')).toBeInTheDocument()); + await waitFor(() => expect(queryByText('Networks')).toBeInTheDocument()); + await waitFor(() => expect(queryByText('spring boot')).toBeInTheDocument()); + await waitFor(() => expect(queryByText('ops guys')).toBeInTheDocument()); + }); }); diff --git a/src/main/app/src/Components/Landscape/Modals/Item/Item.tsx b/src/main/app/src/Components/Landscape/Modals/Item/Item.tsx index 7ef77bc2a..16b054548 100644 --- a/src/main/app/src/Components/Landscape/Modals/Item/Item.tsx +++ b/src/main/app/src/Components/Landscape/Modals/Item/Item.tsx @@ -34,6 +34,7 @@ import componentStyles from '../../../../Resources/styling/ComponentStyles'; import ItemAvatar from './ItemAvatar'; import { LandscapeContext } from '../../../../Context/LandscapeContext'; import { a11yProps, TabPanel } from '../../Utils/TabUtils'; +import MappedString from '../../Utils/MappedString'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -54,6 +55,7 @@ const useStyles = makeStyles((theme: Theme) => interface Props { small?: boolean; + sticky?: boolean; fullyQualifiedItemIdentifier?: string; } @@ -62,7 +64,7 @@ interface Props { * * */ -const Item: React.FC = ({ fullyQualifiedItemIdentifier, small }) => { +const Item: React.FC = ({ fullyQualifiedItemIdentifier, small, sticky }) => { const [item, setItem] = useState(undefined); const [compact, setCompact] = useState(false); const [visible, setVisible] = useState(true); @@ -93,6 +95,7 @@ const Item: React.FC = ({ fullyQualifiedItemIdentifier, small }) => { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', + paddingLeft: 10, }} > {iface.name || iface.path} @@ -113,24 +116,74 @@ const Item: React.FC = ({ fullyQualifiedItemIdentifier, small }) => {
) : null} - Path: {iface.path || '-'} -
-
- Params: {iface.parameters || '-'} -
-
- Format: {iface.format || '-'} -
-
- Payload: {iface.payload || '-'} -
-
- Protection: {iface.protection || '-'} -
-
- Deprecated: {iface.deprecated ? 'Yes' : '-'} -
-
+ + + Path: {iface.path || '-'} + + + Params: {iface.parameters || '-'} + + + Format: {iface.format || '-'} + + + Payload: {iface.payload || '-'} + + + Protection: {iface.protection || '-'} + + + Deprecated: {iface.deprecated ? 'Yes' : '-'} + +
); @@ -201,7 +254,9 @@ const Item: React.FC = ({ fullyQualifiedItemIdentifier, small }) => { .map((assessment) => { return ( - {assessment.field} + + + = ({ fullyQualifiedItemIdentifier, small }) => { const frameworks: ReactElement | null = item ? getLabelsWithPrefix('framework', item) : null; const interfaces: ReactElement | null = item ? getInterfaces(item) : null; + let network: ReactElement[] = []; + item?.networks.forEach((networkValue) => + network.push( + + + + ) + ); + const networks = + item?.networks && item?.networks.length ? {network} : null; + const changeTab = (event: React.ChangeEvent<{}>, newValue: number) => { setValue(newValue); }; @@ -236,15 +302,17 @@ const Item: React.FC = ({ fullyQualifiedItemIdentifier, small }) => { ) : null} - { - setItem(undefined); - setVisible(false); - }} - > - - + {!sticky ? ( + { + setItem(undefined); + setVisible(false); + }} + > + + + ) : null} ); const assessmentSummary = item @@ -252,11 +320,10 @@ const Item: React.FC = ({ fullyQualifiedItemIdentifier, small }) => { : null; const tags = item?.tags && item?.tags.length - ? item.tags.map((value) => ( - + ? item.tags.map((tagValue) => ( + )) : null; - if (!visible) return null; return ( @@ -295,21 +362,21 @@ const Item: React.FC = ({ fullyQualifiedItemIdentifier, small }) => { } - label={'info'} + label={} style={{ minWidth: 50 }} title={'Info'} {...a11yProps(0, 'item')} /> } - label={'relations'} + label={} style={{ minWidth: 50 }} title={'Relations'} {...a11yProps(1, 'item')} /> } - label={'Details'} + label={} title={'API / Interfaces'} style={{ minWidth: 50 }} {...a11yProps(2, 'item')} @@ -322,37 +389,49 @@ const Item: React.FC = ({ fullyQualifiedItemIdentifier, small }) => { {item?.group ? ( - Group + + + {item?.group} ) : null} {item?.type ? ( - Type + + + {item?.type} ) : null} {item?.description ? ( - Info + + + {item?.description} ) : null} {item?.owner ? ( - Owner + + + {item?.owner} ) : null} {item?.contact ? ( - Contact + + + {item?.contact} ) : null} {item?.address ? ( - Address + + + {item?.address} ) : null} @@ -362,9 +441,18 @@ const Item: React.FC = ({ fullyQualifiedItemIdentifier, small }) => { if (data[0] === 'self') return null; return ( - {data[0]} - + + + + {data[1].href} @@ -378,7 +466,9 @@ const Item: React.FC = ({ fullyQualifiedItemIdentifier, small }) => { {assessmentStatus.length > 0 ? ( <>
- Status + + + {assessmentStatus}
@@ -389,7 +479,9 @@ const Item: React.FC = ({ fullyQualifiedItemIdentifier, small }) => { {inboundRelations && inboundRelations.length ? (
- Inbound + + + {inboundRelations}
) : ( @@ -397,7 +489,9 @@ const Item: React.FC = ({ fullyQualifiedItemIdentifier, small }) => { )} {outboundRelations && outboundRelations.length ? (
- Outbound + + + {outboundRelations}
) : ( @@ -408,21 +502,36 @@ const Item: React.FC = ({ fullyQualifiedItemIdentifier, small }) => { {frameworks ? (
- Frameworks + + + {frameworks}
) : null} + {networks ? ( +
+ + + + {networks} +
+ ) : null} + {labels ? ( <> - Labels + + + {labels} ) : null} {interfaces != null ? (
- Interfaces + + + {interfaces}
) : null} diff --git a/src/main/app/src/Components/Landscape/Overview/Overview.tsx b/src/main/app/src/Components/Landscape/Overview/Overview.tsx index 1e4e5d729..b47965c3d 100644 --- a/src/main/app/src/Components/Landscape/Overview/Overview.tsx +++ b/src/main/app/src/Components/Landscape/Overview/Overview.tsx @@ -1,25 +1,14 @@ -import React, {useCallback, useEffect, useState} from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; -import {ILandscape, ILandscapeLinks} from '../../../interfaces'; +import { ILandscape, ILandscapeLinks } from '../../../interfaces'; import OverviewLayout from './OverviewLayout'; -import {get} from '../../../utils/API/APIClient'; -import Events from '../../Events/Events'; -import {createStyles, darken, Theme} from '@material-ui/core'; -import {Redirect} from 'react-router-dom'; +import { get } from '../../../utils/API/APIClient'; +import { createStyles, darken, Theme } from '@material-ui/core'; +import { Redirect } from 'react-router-dom'; import makeStyles from '@material-ui/core/styles/makeStyles'; -import {withBasePath} from '../../../utils/API/BasePath'; +import { withBasePath } from '../../../utils/API/BasePath'; import Avatar from '@material-ui/core/Avatar'; -/** - * Logic Component to display all available landscapes - */ - -interface Props { - setSidebarContent: Function; - setPageTitle: Function; - welcomeMessage: string; -} - const useStyles = makeStyles((theme: Theme) => createStyles({ loading: { @@ -46,7 +35,15 @@ const useStyles = makeStyles((theme: Theme) => }) ); -const Overview: React.FC = ({ setSidebarContent, setPageTitle, welcomeMessage }) => { +interface Props { + setPageTitle: Function; + welcomeMessage: string; +} + +/** + * Logic Component to display all available landscapes + */ +const Overview: React.FC = ({ setPageTitle, welcomeMessage }) => { const [landscapes, setLandscapes] = useState([]); const [landscapeLinks, setLandscapeLinks] = useState(); const [loadLandscapes, setLoadLandscapes] = useState(true); @@ -73,9 +70,8 @@ const Overview: React.FC = ({ setSidebarContent, setPageTitle, welcomeMes useEffect(() => { getLandscapes(); - setSidebarContent(); setPageTitle(welcomeMessage); - }, [getLandscapes, setSidebarContent, setPageTitle, welcomeMessage]); + }, [getLandscapes, setPageTitle, welcomeMessage]); const loading = (
@@ -92,7 +88,7 @@ const Overview: React.FC = ({ setSidebarContent, setPageTitle, welcomeMes return landscapes.length > 0 ? ( landscapesCount > 1 ? ( - + ) : ( ) diff --git a/src/main/app/src/Components/Landscape/Overview/OverviewLayout.tsx b/src/main/app/src/Components/Landscape/Overview/OverviewLayout.tsx index acdfa5b06..91978b554 100644 --- a/src/main/app/src/Components/Landscape/Overview/OverviewLayout.tsx +++ b/src/main/app/src/Components/Landscape/Overview/OverviewLayout.tsx @@ -7,8 +7,7 @@ import { ILandscape } from '../../../interfaces'; import dateFormat from 'dateformat'; import { withBasePath } from '../../../utils/API/BasePath'; import IconButton from '@material-ui/core/IconButton'; -import { Assignment, FormatListBulleted, MapOutlined } from '@material-ui/icons'; -import Log from '../Modals/Log/Log'; +import { Assignment, MapOutlined } from '@material-ui/icons'; import CardContent from '@material-ui/core/CardContent'; import { createStyles, makeStyles } from '@material-ui/core/styles'; import componentStyles from '../../../Resources/styling/ComponentStyles'; @@ -16,9 +15,11 @@ import componentStyles from '../../../Resources/styling/ComponentStyles'; const useStyles = makeStyles((theme: Theme) => createStyles({ overview: { - marginLeft: 10, - width: 'calc(100% - 300px)', - paddingTop: 0, + margin: '0.5em', + marginRight: '0.5em', + marginLeft: '0em', + paddingRight: '0.5em', + width: '100vw', }, card: { marginBottom: 5, @@ -41,14 +42,13 @@ const useStyles = makeStyles((theme: Theme) => interface Props { landscapes: ILandscape[] | null | undefined; - setSidebarContent: Function; } /** * Displays all available landscapes and provides all needed navigation */ -const OverviewLayout: React.FC = ({ landscapes, setSidebarContent }) => { +const OverviewLayout: React.FC = ({ landscapes }) => { const classes = useStyles(); const componentClasses = componentStyles(); let content: ReactElement[] = [Loading landscapes...]; @@ -75,13 +75,6 @@ const OverviewLayout: React.FC = ({ landscapes, setSidebarContent }) => { classes={{ subheader: componentClasses.cardSubheader }} action={ - setSidebarContent()} - > - - ', () => { { label: 'green', value: 12, + color: '#161618', }, { label: 'red', value: 7, + color: '#161618', }, ], }, @@ -26,10 +28,12 @@ describe('', () => { { label: 'foo', value: 5, + color: '', }, { label: 'bar', value: 7, + color: '#161618', }, ], }, @@ -74,7 +78,7 @@ describe('', () => { ); fireEvent.click(getByTitle(container, 'Export current search as report')); - expect(getByText('Report')).toBeInTheDocument(); + expect(getByText('report')).toBeInTheDocument(); fireEvent.click(getByTitle(container, 'Export as report')); expect(saveSearch.mock.calls.length).toBe(1); @@ -84,8 +88,15 @@ describe('', () => { const { getByText } = render( ); - fireEvent.click(getByText('foo')); expect(addFacet.mock.calls.length).toBe(1); }); + + it('should call addFacet', () => { + const { getByText } = render( + + ); + fireEvent.click(getByText('foo')); + expect(addFacet).toBeCalled(); + }); }); diff --git a/src/main/app/src/Components/Landscape/Search/Facets.tsx b/src/main/app/src/Components/Landscape/Search/Facets.tsx index c55c0d830..15a683690 100644 --- a/src/main/app/src/Components/Landscape/Search/Facets.tsx +++ b/src/main/app/src/Components/Landscape/Search/Facets.tsx @@ -16,6 +16,7 @@ import Button from '@material-ui/core/Button'; import { SaveSearchConfig } from './SaveSearchConfig'; import { IFacet } from '../../../interfaces'; import { a11yProps, TabPanel } from '../Utils/TabUtils'; +import MappedString from '../Utils/MappedString'; interface FacetsProps { addFacet: (dim: string, label: string) => string; @@ -38,20 +39,22 @@ const Facets: React.FC = ({ facets, addFacet, saveSearch }) => { .forEach((facet: IFacet) => facetsHtml.push( - {facet.dim} + + + - {facet.labelValues.map((lv) => ( + {facet.labelValues.map((cv) => ( { - addFacet(facet.dim, lv.label); + addFacet(facet.dim, cv.label); }} variant={'default'} color={'primary'} - style={{ margin: 1 }} + style={{ margin: 1, backgroundColor: cv.color }} size={'small'} - key={facet.dim + '' + lv.label} - label={lv.label} - avatar={{lv.value}} + key={facet.dim + '' + cv.label} + label={} + avatar={{cv.value}} /> ))} @@ -74,17 +77,17 @@ const Facets: React.FC = ({ facets, addFacet, saveSearch }) => { {getLabel(facet)} - {facet.labelValues.map((lv) => ( + {facet.labelValues.map((cv) => ( { - addFacet(facet.dim, lv.label); + addFacet(facet.dim, cv.label); }} variant={'default'} size={'small'} - key={facet.dim + '' + lv.label} - label={lv.label} - style={{ backgroundColor: lv.label, color: 'black', margin: 1 }} - avatar={{lv.value}} + key={facet.dim + '' + cv.label} + label={cv.label} + style={{ backgroundColor: cv.label, color: 'black', margin: 1 }} + avatar={{cv.value}} /> ))} @@ -101,27 +104,25 @@ const Facets: React.FC = ({ facets, addFacet, saveSearch }) => { return ( <> -
-
} - label={'fields'} + label={} style={{ minWidth: 50 }} title={'Fields'} {...a11yProps(0, 'search')} /> } - label={'kpis'} + label={} style={{ minWidth: 50 }} title={'KPIs'} {...a11yProps(1, 'search')} /> } - label={'Report'} + label={} title={'Export current search as report'} style={{ minWidth: 50 }} {...a11yProps(2, 'search')} diff --git a/src/main/app/src/Components/Landscape/Search/Search.test.tsx b/src/main/app/src/Components/Landscape/Search/Search.test.tsx index 9feb0addc..9861d1249 100644 --- a/src/main/app/src/Components/Landscape/Search/Search.test.tsx +++ b/src/main/app/src/Components/Landscape/Search/Search.test.tsx @@ -1,16 +1,84 @@ -import { render } from '@testing-library/react'; +import { fireEvent, getByTitle, render, waitFor } from '@testing-library/react'; import React from 'react'; import Search from './Search'; import { LandscapeContext } from '../../../Context/LandscapeContext'; import landscapeContextValue from '../../../utils/testing/LandscapeContextValue'; +import { createTheme, ThemeOptions, ThemeProvider } from '@material-ui/core/styles'; +import defaultThemeVariables from '../../../Resources/styling/theme'; +import { IFacet } from '../../../interfaces'; +import * as APIClient from '../../../utils/API/APIClient'; +function MockTheme({ children }: any) { + const tv: ThemeOptions = defaultThemeVariables; + + // @ts-ignore + tv.palette.background.default = '#161618'; + // @ts-ignore + tv.palette.primary.main = '#22F2C2'; + // @ts-ignore + tv.palette.secondary.main = '#eeeeee'; + const theme = createTheme(tv); + return {children}; +} + +const facets: IFacet[] = [ + { + dim: 'kpi_security', + value: 19, + labelValues: [ + { + label: 'green', + value: 12, + color: '#161618', + }, + { + label: 'red', + value: 7, + color: '#161618', + }, + ], + }, + { + dim: 'owner', + value: 12, + labelValues: [ + { + label: 'foo', + value: 5, + color: '', + }, + { + label: 'bar', + value: 7, + color: '#161618', + }, + ], + }, +]; describe('', () => { it('should render', () => { const { getByText } = render( - {}} setSidebarContent={() => {}} /> + {}} /> ); expect(getByText('Search')).toBeInTheDocument(); }); + it('should add facet to search term', async () => { + const mock = jest.spyOn(APIClient, 'get'); + mock.mockReturnValue(Promise.resolve(facets)); + const { container, getByText, getByDisplayValue } = render( + + + {}} /> + + + ); + + fireEvent.click(getByTitle(container, 'Fields')); + + await waitFor(() => getByText('foo')); + const fooFacetChip = getByText('foo'); + expect(fooFacetChip).toBeInTheDocument(); + }); }); diff --git a/src/main/app/src/Components/Landscape/Search/Search.tsx b/src/main/app/src/Components/Landscape/Search/Search.tsx index f5d8c460b..eda88b86e 100644 --- a/src/main/app/src/Components/Landscape/Search/Search.tsx +++ b/src/main/app/src/Components/Landscape/Search/Search.tsx @@ -1,12 +1,11 @@ import React, { useContext, useEffect, useState } from 'react'; -import { Box, Input, InputAdornment, Theme } from '@material-ui/core'; +import { Theme } from '@material-ui/core'; import { get } from '../../../utils/API/APIClient'; import { IFacet, IItem } from '../../../interfaces'; import Item from '../Modals/Item/Item'; -import { Backspace, Close, SearchOutlined } from '@material-ui/icons'; import IconButton from '@material-ui/core/IconButton'; -import { createStyles, makeStyles } from '@material-ui/core/styles'; +import { createStyles, makeStyles, useTheme } from '@material-ui/core/styles'; import componentStyles from '../../../Resources/styling/ComponentStyles'; import HelpTooltip from '../../Help/HelpTooltip'; import Facets from './Facets'; @@ -18,8 +17,14 @@ import { LandscapeContext } from '../../../Context/LandscapeContext'; const useStyles = makeStyles((theme: Theme) => createStyles({ + searchContainer: { + display: 'flex', + flexDirection: 'column', + height: '100%', + }, search: { margin: 0, + marginBottom: '1em', padding: 0, borderRadius: 50, height: '2.5em', @@ -33,31 +38,40 @@ const useStyles = makeStyles((theme: Theme) => paddingRight: 5, width: '100%', }, + searchResults: { + marginTop: '1em', + flexGrow: 1, + flexShrink: 1, + overflowY: 'auto', + }, }) ); -interface PropsInterface { - setSidebarContent: Function; - showSearch: Function; +interface SearchProps { + searchTerm: string; + setSearchTerm: Function; } -const Search: React.FC = ({ setSidebarContent, showSearch }) => { +const Search: React.FC = ({ setSearchTerm, searchTerm }) => { const [currentLandscape, setCurrentLandscape] = useState(''); const [results, setResults] = useState([]); + const [renderedResults, setRenderedResults] = useState([]); const [facets, setFacets] = useState([]); - const [searchTerm, setSearchTerm] = useState(''); const [searchSupport, setSearchSupport] = useState(null); const [render, setRender] = useState(false); const classes = useStyles(); const componentClasses = componentStyles(); - const searchInput = React.useRef(null); const landscapeContext = useContext(LandscapeContext); + const theme = useTheme(); /** * Search on search term change, set results. */ useEffect(() => { - if (searchTerm.length < 2) return; + if (searchTerm.length < 2) { + setResults([]); + return; + } get( '/api/landscape/' + @@ -73,8 +87,45 @@ const Search: React.FC = ({ setSidebarContent, showSearch }) => .catch((reason) => { console.warn(reason); }); - }, [searchTerm, landscapeContext.identifier]); + }, [searchTerm, landscapeContext.identifier, render]); + + useEffect(() => { + if (results && results.length > 0) { + setRenderedResults( + results.map((value1: IItem) => ( + + )) + ); + return; + } + + let msg = '...'; + if (searchTerm && searchTerm.length) { + msg = 'No results found.'; + } + setRenderedResults(<>{msg}); + }, [results, searchTerm]); + useEffect(() => { + facets.forEach((facet) => { + facet.labelValues.forEach((chipValue) => { + let label = chipValue.label; + if (label.indexOf(' ') !== -1) { + label = `"${label}"`; //to handle whitespace + } + if (searchTerm.indexOf(facet.dim + ':' + label) === -1) { + chipValue.color = theme.palette.primary.main; + } else { + chipValue.color = theme.palette.secondary.main; + } + }); + }); + }, [searchTerm, facets, theme]); /** * loading of facets * @@ -82,18 +133,18 @@ const Search: React.FC = ({ setSidebarContent, showSearch }) => */ useEffect(() => { const addFacet = (dim: string, label: string): string => { - let current = searchInput.current; - if (current && dim.length && label.length) { - if (searchTerm.indexOf(dim + ':' + label) === -1) { - if (label.indexOf(' ') !== -1) { - label = `"${label}"`; //to handle whitespace - } - setSearchTerm(`${searchTerm} ${dim}:${label}`); - setRender(true); - } - current.focus(); + if (label.indexOf(' ') !== -1) { + label = `"${label}"`; //to handle whitespace + } + if (searchTerm.length === 0) { + setSearchTerm(`${dim}:${label}`); + setRender(true); + } else if (searchTerm.indexOf(dim + ':' + label) === -1) { + setSearchTerm(`${searchTerm} ${dim}:${label}`); + setRender(true); + } else { + setSearchTerm(searchTerm.replace(`${dim}:${label}`, '').trim()); } - return searchTerm; }; @@ -112,21 +163,14 @@ const Search: React.FC = ({ setSidebarContent, showSearch }) => }; setSearchSupport(); - }, [setSearchSupport, searchTerm, facets, componentClasses.card, currentLandscape]); - - /** - * Update rendered search results - */ - useEffect(() => { - const searchResult = results.map((value1: IItem) => ( - - )); - setSidebarContent(<>{searchResult}); - }, [results, setSidebarContent, render]); + }, [ + setSearchSupport, + searchTerm, + setSearchTerm, + facets, + componentClasses.card, + currentLandscape, + ]); async function loadFacets(identifier: string | undefined) { if (identifier == null) { @@ -137,13 +181,8 @@ const Search: React.FC = ({ setSidebarContent, showSearch }) => ); if (!result) return; - setFacets(result); - } - function clear() { - setSearchTerm(''); - setResults([]); - setSidebarContent(null); + setFacets(result); } useEffect(() => { @@ -156,47 +195,26 @@ const Search: React.FC = ({ setSidebarContent, showSearch }) => if (currentLandscape == null || currentLandscape !== landscapeContext.identifier) { setFacets([]); - setSearchTerm(''); setCurrentLandscape(landscapeContext.identifier); } return ( -
-
- - } /> - - showSearch(false)} title={'Close search'}> - - +
+
+ + Search + + } /> + + +
+ + {searchSupport} +
+ Results + {searchTerm} + {renderedResults}
- Search - - setSearchTerm(event.target.value)} - startAdornment={ - setRender(!render)} title={'Show results'}> - - - } - endAdornment={ - searchTerm.length ? ( - - clear()} title={'Clear'}> - - - - ) : null - } - /> - {searchSupport} -
); }; diff --git a/src/main/app/src/Components/Landscape/Search/SearchField.test.tsx b/src/main/app/src/Components/Landscape/Search/SearchField.test.tsx new file mode 100644 index 000000000..81e896939 --- /dev/null +++ b/src/main/app/src/Components/Landscape/Search/SearchField.test.tsx @@ -0,0 +1,10 @@ +import { getByDisplayValue, render } from '@testing-library/react'; +import React from 'react'; +import SearchField from './SearchField'; + +describe('', () => { + it('should render', () => { + const { getByDisplayValue } = render( {}} />); + expect(getByDisplayValue('')).toBeInTheDocument(); + }); +}); diff --git a/src/main/app/src/Components/Landscape/Search/SearchField.tsx b/src/main/app/src/Components/Landscape/Search/SearchField.tsx new file mode 100644 index 000000000..d5aa9a7ef --- /dev/null +++ b/src/main/app/src/Components/Landscape/Search/SearchField.tsx @@ -0,0 +1,87 @@ +import React, { useEffect, useState } from 'react'; + +import { Input, InputAdornment, Theme } from '@material-ui/core'; +import { Backspace, SearchOutlined } from '@material-ui/icons'; +import IconButton from '@material-ui/core/IconButton'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; +import Search from './Search'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + search: { + margin: 0, + marginLeft: 10, + padding: 0, + paddingLeft: 5, + borderRadius: 50, + height: '2.5em', + border: '1px solid ', + backgroundColor: theme.palette.background.default, + borderColor: theme.palette.primary.main, + }, + searchField: { + marginTop: 0, + paddingLeft: 5, + paddingRight: 5, + width: '100%', + }, + }) +); + +interface Props { + setSidebarContent: Function; +} + +/** + * Search input field displayed in the top nav + * + * @param setSidebarContent function the set content to the drawer sidebar + */ +const SearchField: React.FC = ({ setSidebarContent }) => { + const classes = useStyles(); + const [searchTerm, setSearchTerm] = useState(''); + const searchInput = React.useRef(null); + + const update = () => { + setSidebarContent(); + }; + + useEffect(() => { + setSidebarContent(); + }, [searchTerm, setSidebarContent]); + + function clear() { + setSearchTerm(''); + } + + return ( +
+ setSearchTerm(event.target.value)} + onFocus={() => update()} + endAdornment={ + + {searchTerm.length ? ( + clear()} title={'Clear'}> + + + ) : ( + <> + )} + update()} title={'Show results'}> + + + + } + /> +
+ ); +}; + +export default SearchField; diff --git a/src/main/app/src/Components/Landscape/Utils/utils.tsx b/src/main/app/src/Components/Landscape/Utils/utils.tsx index 39f18915e..909c8a683 100644 --- a/src/main/app/src/Components/Landscape/Utils/utils.tsx +++ b/src/main/app/src/Components/Landscape/Utils/utils.tsx @@ -1,6 +1,6 @@ -import React, { ReactElement } from 'react'; -import { IGroup, IItem, ILandscape, IRelation } from '../../../interfaces'; -import { Button, Link, List, ListItem, ListItemText } from '@material-ui/core'; +import React, {ReactElement} from 'react'; +import {IGroup, IItem, ILandscape, IRelation} from '../../../interfaces'; +import {Button, Link, List, ListItem, ListItemText} from '@material-ui/core'; import MappedString from './MappedString'; /** @@ -30,7 +30,10 @@ export const getItem = (landscape: ILandscape, fullyQualifiedIdentifier: string) * @param landscape object * @param fullyQualifiedIdentifier string to identify the group */ -export const getGroup = (landscape: ILandscape, fullyQualifiedIdentifier: string): IGroup | null => { +export const getGroup = ( + landscape: ILandscape, + fullyQualifiedIdentifier: string +): IGroup | null => { let group: IGroup | null = null; for (let i = 0; i < landscape.groups.length; i++) { let value = landscape.groups[i]; @@ -157,9 +160,10 @@ export const getLabelsWithPrefix = (prefix: string, element: IGroup | IItem) => const value = element.labels?.[key] || null; if (!value) return; const primary = key.replace(prefix + '.', ''); + const secondary = value.substr(0, 150); labels.push( - + ); }); diff --git a/src/main/app/src/Components/Layout/Layout.test.tsx b/src/main/app/src/Components/Layout/Layout.test.tsx new file mode 100644 index 000000000..b3d947da6 --- /dev/null +++ b/src/main/app/src/Components/Layout/Layout.test.tsx @@ -0,0 +1,22 @@ +import { getByAltText, render } from '@testing-library/react'; +import React from 'react'; +import Layout from './Layout'; +import { MemoryRouter } from 'react-router-dom'; + +describe('', () => { + it('should render navigation with title, logo and version', () => { + const { getByText, getByAltText } = render( + +
} + version={'123'} + /> + + ); + expect(getByAltText('logo')).toHaveAttribute('src', 'https://acme.com/logo.png'); + expect(getByText('foo')).toBeInTheDocument(); + expect(getByText('nivio 123')).toBeInTheDocument(); + }); +}); diff --git a/src/main/app/src/Components/Layout/Layout.tsx b/src/main/app/src/Components/Layout/Layout.tsx index 2b23b8f44..77888ba8b 100644 --- a/src/main/app/src/Components/Layout/Layout.tsx +++ b/src/main/app/src/Components/Layout/Layout.tsx @@ -1,106 +1,94 @@ -import React, { ReactElement } from 'react'; +import React, { ReactElement, useContext, useEffect, useState } from 'react'; import Navigation from '../Navigation/Navigation'; import { Drawer, Theme } from '@material-ui/core'; import { createStyles, makeStyles } from '@material-ui/core/styles'; -import Search from '../Landscape/Search/Search'; +import IconButton from '@material-ui/core/IconButton'; +import { CloseSharp } from '@material-ui/icons'; +import { LandscapeContext } from '../../Context/LandscapeContext'; interface Props { children: string | ReactElement | ReactElement[]; - sidebarContent: string | ReactElement | ReactElement[]; - setSidebarContent: Function; pageTitle?: string; logo?: string; version?: string; } -const searchSupportWidth = 360; -const sidebarWidth = 280; +const drawerWidth = 360; const useStyles = makeStyles((theme: Theme) => createStyles({ root: { display: 'flex', }, - sideBar: { - position: 'absolute', - right: 0, - top: 5, - width: sidebarWidth, - overflow: 'auto', - maxHeight: 'calc(100vh - 50px)', - zIndex: 5000, - }, - - outer: { - display: 'flex', - flexDirection: 'row', - }, - content: { - position: 'relative', - }, + outer: {}, flexItem: { flexShrink: 1, flexGrow: 1, }, main: { + display: 'flex', + flexDirection: 'row', + }, + children: { flexShrink: 1, flexGrow: 2, width: '1000px', }, - searchSupport: { + sidebar: { backgroundColor: theme.palette.primary.dark, - width: searchSupportWidth, + width: drawerWidth, padding: 5, + top: 0, + position: 'absolute', }, }) ); /** * Contains our site layout, Navigation on top, content below - * @param param0 */ -const Layout: React.FC = ({ - children, - sidebarContent, - setSidebarContent, - pageTitle, - logo, - version, -}) => { +const Layout: React.FC = ({ children, pageTitle, logo, version }) => { const classes = useStyles(); - const [searchSupport, setSearchSupport] = React.useState(false); + const [sidebarContent, setSidebarContent] = useState(null); + const [sidebarOpen, setSidebarOpen] = useState(false); + const landscapeContext = useContext(LandscapeContext); + + useEffect(() => { + const isOpen = sidebarContent != null && landscapeContext.identifier != null; + setSidebarOpen(isOpen); + }, [sidebarContent, landscapeContext]); return (
+
- -
-
{sidebarContent}
- {children} -
+
{children}
+ +
+ setSidebarOpen(false)} size={'small'}> + + +
+ {sidebarContent} +
- { - setSearchSupport(false); - }} - > - -
); }; diff --git a/src/main/app/src/Components/Manual/Man.css b/src/main/app/src/Components/Manual/Man.css index 1b117fbb2..7bb83c6b2 100644 --- a/src/main/app/src/Components/Manual/Man.css +++ b/src/main/app/src/Components/Manual/Man.css @@ -5,3 +5,7 @@ a.reference.external { .highlight { background-color: #333333; } + +.footer { + display: none; +} diff --git a/src/main/app/src/Components/Manual/Man.tsx b/src/main/app/src/Components/Manual/Man.tsx index cdd83c8aa..a18c31911 100644 --- a/src/main/app/src/Components/Manual/Man.tsx +++ b/src/main/app/src/Components/Manual/Man.tsx @@ -14,8 +14,14 @@ const useStyles = makeStyles((theme: Theme) => createStyles({ manualContainer: { overflowY: 'scroll', - height: '90vh', - marginRight: 340, + height: '99vh', + padding: 10, + display: 'flex', + }, + sidebar: { + width: 330, + flexShrink: 1, + flexGrow: 1, }, link: { color: theme.palette.primary.contrastText, @@ -27,14 +33,13 @@ const useStyles = makeStyles((theme: Theme) => ); interface Props { - setSidebarContent: Function; setPageTitle: Function; } /** * Renders nivio manual, depending on which url param is given */ -const Man: React.FC = ({ setSidebarContent, setPageTitle }) => { +const Man: React.FC = ({ setPageTitle }) => { const classes = useStyles(); const [html, setHtml] = useState( @@ -45,6 +50,7 @@ const Man: React.FC = ({ setSidebarContent, setPageTitle }) => { if (usage == null || typeof usage == 'undefined') usage = 'index'; const [topic, setTopic] = useState(usage + ''); const [side, setSide] = useState(null); + const [sidebarContent, setSidebarContent] = useState([]); const [emptyManual, setemptyManual] = useState(false); const handleSphinxSidebar = useCallback( @@ -191,6 +197,7 @@ const Man: React.FC = ({ setSidebarContent, setPageTitle }) => { return ( {html} +
{sidebarContent}
); }; diff --git a/src/main/app/src/Components/Navigation/Navigation.test.tsx b/src/main/app/src/Components/Navigation/Navigation.test.tsx index 9e3c35aac..4b04e53e4 100644 --- a/src/main/app/src/Components/Navigation/Navigation.test.tsx +++ b/src/main/app/src/Components/Navigation/Navigation.test.tsx @@ -8,21 +8,23 @@ global.TextEncoder = TextEncoder; // @ts-ignore global.TextDecoder = TextDecoder; -it('should render Home link', () => { - const { getByText } = render( - - {}} /> - - ); - expect(getByText('Home')).toBeInTheDocument(); -}); +describe('', () => { + it('should render Home link', () => { + const { getByText } = render( + + {}} /> + + ); + expect(getByText('Home')).toBeInTheDocument(); + }); -it('should link to manual on button click', () => { - const { getByText } = render( - - {}} /> - - ); + it('should link to manual on button click', () => { + const { getByText } = render( + + {}} /> + + ); - expect(getByText('Help').closest('a')).toHaveAttribute('href', '/man/install.html'); + expect(getByText('Help').closest('a')).toHaveAttribute('href', '/man/install.html'); + }); }); diff --git a/src/main/app/src/Components/Navigation/Navigation.tsx b/src/main/app/src/Components/Navigation/Navigation.tsx index f1867c77e..fce9afed1 100644 --- a/src/main/app/src/Components/Navigation/Navigation.tsx +++ b/src/main/app/src/Components/Navigation/Navigation.tsx @@ -18,27 +18,24 @@ import IconButton from '@material-ui/core/IconButton'; import Avatar from '@material-ui/core/Avatar'; import { withBasePath } from '../../utils/API/BasePath'; import Notification from '../Notification/Notification'; -import { SearchOutlined } from '@material-ui/icons'; import componentStyles from '../../Resources/styling/ComponentStyles'; import LandscapeWatcher from '../Landscape/Dashboard/LandscapeWatcher'; import { LandscapeContext } from '../../Context/LandscapeContext'; +import SearchField from '../Landscape/Search/SearchField'; const useStyles = makeStyles((theme: Theme) => createStyles({ - grow: { - flexGrow: 1, - }, pageTitle: { padding: 11, paddingLeft: 16, paddingRight: 16, + flexGrow: 1, }, logo: { height: '1.5em', width: '1.5em', }, appBar: { - zIndex: theme.zIndex.drawer + 1, position: 'relative', backgroundColor: theme.palette.primary.main, }, @@ -47,8 +44,6 @@ const useStyles = makeStyles((theme: Theme) => interface Props { setSidebarContent: Function; - setSearchSupport: Function; - searchSupport: boolean; pageTitle?: string; logo?: string; version?: string; @@ -57,14 +52,7 @@ interface Props { /** * Header Component */ -const Navigation: React.FC = ({ - setSidebarContent, - setSearchSupport, - searchSupport, - pageTitle, - logo, - version, -}) => { +const Navigation: React.FC = ({ setSidebarContent, pageTitle, logo, version }) => { const classes = useStyles(); const componentClasses = componentStyles(); const landscapeContext = useContext(LandscapeContext); @@ -110,12 +98,14 @@ const Navigation: React.FC = ({ className={classes.logo} imgProps={{ style: { objectFit: 'contain' } }} src={logo} + alt={'logo'} /> ) : ( )} @@ -133,17 +123,11 @@ const Navigation: React.FC = ({ {pageTitle} + {landscapeContext.identifier ? : null} + {landscapeContext.identifier ? ( - setSearchSupport(!searchSupport)} - title={'Toggle search'} - > - - + ) : null}{' '} - - ); }; diff --git a/src/main/app/src/Components/Notification/Changes.tsx b/src/main/app/src/Components/Notification/Changes.tsx index a4d50898e..b44d67cc3 100644 --- a/src/main/app/src/Components/Notification/Changes.tsx +++ b/src/main/app/src/Components/Notification/Changes.tsx @@ -1,15 +1,15 @@ -import React, { ReactElement, useContext, useEffect, useState } from 'react'; -import { IChange } from '../../interfaces'; -import { Card, CardHeader, Table, TableBody, TableCell, TableRow } from '@material-ui/core'; -import { get } from '../../utils/API/APIClient'; +import React, {ReactElement, useContext, useEffect, useState} from 'react'; +import {IChange} from '../../interfaces'; +import {Box, Table, TableBody, TableCell, TableRow, Typography} from '@material-ui/core'; +import {get} from '../../utils/API/APIClient'; import IconButton from '@material-ui/core/IconButton'; import ItemAvatar from '../Landscape/Modals/Item/ItemAvatar'; import componentStyles from '../../Resources/styling/ComponentStyles'; -import { LocateFunctionContext } from '../../Context/LocateFunctionContext'; +import {LocateFunctionContext} from '../../Context/LocateFunctionContext'; import GroupAvatar from '../Landscape/Modals/Group/GroupAvatar'; -import { Close, LinkOutlined } from '@material-ui/icons'; +import {LinkOutlined} from '@material-ui/icons'; import Button from '@material-ui/core/Button'; -import { LandscapeContext } from '../../Context/LandscapeContext'; +import {LandscapeContext} from '../../Context/LandscapeContext'; /** * Displays the changes of an ProcessingFinishedEvent @@ -21,23 +21,18 @@ const Changes: React.FC = () => { const [renderedChanges, setRenderedChanges] = useState([]); const locateFunctionContext = useContext(LocateFunctionContext); const landscapeContext = useContext(LandscapeContext); - const [visible, setVisible] = useState(true); - - const close = ( - { - setVisible(false); - }} - > - - - ); /** * render changes, calling api for component info */ useEffect(() => { - if (landscapeContext.changes == null) return; + if ( + landscapeContext.changes == null || + landscapeContext.changes.landscape !== landscapeContext.identifier + ) { + setRenderedChanges([]); + return; + } const getItemChange = (key: string, change: IChange): Promise => { if (change.changeType === 'DELETED') { @@ -133,8 +128,8 @@ const Changes: React.FC = () => { }; let promises: Promise[] = []; - for (let key of Object.keys(landscapeContext.changes)) { - let change = landscapeContext.changes[key]; + for (let key of Object.keys(landscapeContext.changes.changelog.changes)) { + let change = landscapeContext.changes.changelog.changes[key]; switch (change.componentType) { case 'Item': @@ -151,17 +146,22 @@ const Changes: React.FC = () => { Promise.all(promises).then((rows) => { setRenderedChanges(rows); }); - }, [ componentClasses.card, locateFunctionContext, landscapeContext]); - - if (!visible) return null; + }, [componentClasses.card, locateFunctionContext, landscapeContext]); return ( - - - - {landscapeContext.changes != null ? renderedChanges : null} -
-
+ +
+ Latest changes + {landscapeContext.landscape?.name} +
+ {landscapeContext.changes != null ? ( + + {renderedChanges} +
+ ) : ( + No changes recorded yet. + )} +
); }; diff --git a/src/main/app/src/Components/Notification/Notification.tsx b/src/main/app/src/Components/Notification/Notification.tsx index 5203c4d7b..a3a03b65d 100644 --- a/src/main/app/src/Components/Notification/Notification.tsx +++ b/src/main/app/src/Components/Notification/Notification.tsx @@ -1,10 +1,10 @@ -import React, {ReactElement, useContext, useEffect, useState} from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import IconButton from '@material-ui/core/IconButton'; -import {Badge} from '@material-ui/core'; -import {Notifications} from '@material-ui/icons'; +import { Badge } from '@material-ui/core'; +import { Notifications } from '@material-ui/icons'; import Changes from './Changes'; import componentStyles from '../../Resources/styling/ComponentStyles'; -import {LandscapeContext} from '../../Context/LandscapeContext'; +import { LandscapeContext } from '../../Context/LandscapeContext'; interface Props { setSidebarContent: Function; @@ -18,7 +18,6 @@ interface Props { const Notification: React.FC = ({ setSidebarContent }) => { const classes = componentStyles(); const [newChanges, setNewChanges] = useState(false); - const [renderedChanges, setRenderedChanges] = useState(null); const landscapeContext = useContext(LandscapeContext); /** @@ -26,7 +25,6 @@ const Notification: React.FC = ({ setSidebarContent }) => { */ useEffect(() => { if (landscapeContext.changes == null) return; - setRenderedChanges(); setNewChanges(true); }, [landscapeContext.changes]); @@ -43,7 +41,7 @@ const Notification: React.FC = ({ setSidebarContent }) => { className={classes.navigationButton} onClick={() => { setNewChanges(false); - return setSidebarContent(renderedChanges); + return setSidebarContent(); }} > diff --git a/src/main/app/src/Context/LandscapeContext.tsx b/src/main/app/src/Context/LandscapeContext.tsx index 31213a8d4..12c95f283 100644 --- a/src/main/app/src/Context/LandscapeContext.tsx +++ b/src/main/app/src/Context/LandscapeContext.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { IAssessment, IAssessmentProps, IChanges, ILandscape, INotificationMessage } from "../interfaces"; +import { IAssessment, IAssessmentProps, ILandscape, INotificationMessage } from '../interfaces'; import { get } from '../utils/API/APIClient'; import { withBasePath } from '../utils/API/BasePath'; import { Client, StompSubscription } from '@stomp/stompjs'; @@ -9,7 +9,7 @@ export interface LandscapeContextType { readonly assessment: IAssessment | null; readonly identifier: string | null; readonly notification: INotificationMessage | null; - readonly changes: IChanges | null; + readonly changes: INotificationMessage | null; next: (identifier: string | null) => void; getAssessmentSummary: (fqi: string) => IAssessmentProps | null; } @@ -39,7 +39,7 @@ const LandscapeContextProvider: React.FC = (props) => { const [landscape, setLandscape] = useState(null); const [assessment, setAssessment] = useState(null); const [notification, setNotification] = useState(null); - const [changes, setChanges] = useState(null); + const [changes, setChanges] = useState(null); const [identifier, setIdentifier] = useState(null); const backendUrl = withBasePath('/subscribe'); @@ -62,8 +62,9 @@ const LandscapeContextProvider: React.FC = (props) => { if (notificationMessage.type === 'LayoutChangedEvent') { setNotification(notificationMessage); } + if (notificationMessage.type === 'ProcessingFinishedEvent') { - setChanges(notificationMessage.changelog.changes); + setChanges(notificationMessage); } }); @@ -102,6 +103,7 @@ const LandscapeContextProvider: React.FC = (props) => { setAssessment(response); console.debug(`Loaded assessment data after identifier change: ${identifier}`); }); + setChanges(null); }, [identifier]); /** @@ -113,10 +115,12 @@ const LandscapeContextProvider: React.FC = (props) => { return; } - get(`/assessment/${identifier}`).then((response) => { - console.debug(`Loaded assessment data for ${identifier}`, response); - setAssessment(response); - }); + if (identifier === notification?.landscape) { + get(`/assessment/${identifier}`).then((response) => { + console.debug(`Loaded assessment data for ${identifier}`, response); + setAssessment(response); + }); + } }, [identifier, notification]); return ( diff --git a/src/main/app/src/interfaces.ts b/src/main/app/src/interfaces.ts index 104e33625..63e8419ba 100644 --- a/src/main/app/src/interfaces.ts +++ b/src/main/app/src/interfaces.ts @@ -90,6 +90,7 @@ export interface IGroup { labels?: ILabels; _links?: ILinks; items: IItem[]; + icon: string; } export interface IItem { @@ -109,6 +110,7 @@ export interface IItem { color?: string; icon: string; _links?: ILinks; + networks: Array; } export interface IInterfaces { @@ -217,10 +219,11 @@ export interface IFacet { /** * different label counts */ - labelValues: ILabelValue[]; + labelValues: IChipValue[]; } -export interface ILabelValue { +export interface IChipValue { label: string; value: number; + color: string; } diff --git a/src/main/app/src/labels.json b/src/main/app/src/labels.json index 4edfa1454..886f13896 100644 --- a/src/main/app/src/labels.json +++ b/src/main/app/src/labels.json @@ -1,22 +1 @@ -{ - "capability": "The capability the service provides for the business or, in case of infrastructure, the technical capability like enabling service discovery, configuration, secrets, or persistence.", - "color": "A hex color code (items inherit group colors as default)", - "costs": "Running costs of the item.", - "fill": "Background image (for displaying purposes).", - "frameworks": "A comma-separated list of frameworks as key-value pairs (key is name, value is version).", - "health": "Description of the item's health status.", - "icon": "Icon/image (for displaying purposes).", - "label": "A custom label (like a note, but very short).", - "layer": "A technical layer.", - "lifecycle": "A lifecycle phase (``PLANNED|plan``, ``INTEGRATION|int``, ``PRODUCTION|prod``, ``END_OF_LIFE|eol|end``).", - "note": "A custom note.", - "scale": "Number of instances.", - "security": "Description of the item's security status.", - "shortname": "Abbreviated name.", - "software": "Software/OS name.", - "stability": "Description of the item's stability.", - "team": "Name of the responsible team (e.g. technical owner).", - "version": "The version (e.g. software version or protocol version).", - "visibility": "Visibility to other items.", - "weight": "Importance or relations. Used as factor for drawn width if numbers between 0 and 5 are given." -} +{"capability":"The capability the service provides for the business or, in case of infrastructure, the technical capability like enabling service discovery, configuration, secrets, or persistence.","color":"A hex color code (items inherit group colors as default)","costs":"Running costs of the item.","fill":"Background image (for displaying purposes).","frameworks":"A comma-separated list of frameworks as key-value pairs (key is name, value is version).","health":"Description of the item's health status.","icon":"Icon/image (for displaying purposes).","label":"A custom label (like a note, but very short).","layer":"A technical layer.","lifecycle":"A lifecycle phase (``PLANNED|plan``, ``INTEGRATION|int``, ``PRODUCTION|prod``, ``END_OF_LIFE|eol|end``).","note":"A custom note.","scale":"Number of instances.","security":"Description of the item's security status.","shortname":"Abbreviated name.","software":"Software/OS name.","stability":"Description of the item's stability.","team":"Name of the responsible team (e.g. technical owner).","version":"The version (e.g. software version or protocol version).","visibility":"Visibility to other items.","weight":"Importance or relations. Used as factor for drawn width if numbers between 0 and 5 are given."} \ No newline at end of file diff --git a/src/main/app/src/utils/testing/LandscapeContextValue.ts b/src/main/app/src/utils/testing/LandscapeContextValue.ts index 180593997..fae362ab4 100644 --- a/src/main/app/src/utils/testing/LandscapeContextValue.ts +++ b/src/main/app/src/utils/testing/LandscapeContextValue.ts @@ -1,4 +1,11 @@ -import { IAssessment, IChanges, IGroup, IItem, ILandscape } from "../../interfaces"; +import { + IAssessment, + IChanges, + IGroup, + IItem, + ILandscape, + INotificationMessage, +} from '../../interfaces'; import { LandscapeContextType } from '../../Context/LandscapeContext'; const items: IItem[] = [ @@ -15,6 +22,7 @@ const items: IItem[] = [ tags: [], type: 'service', icon: '', + networks: [], }, ]; @@ -24,6 +32,7 @@ const groups: IGroup[] = [ name: 'A Group', items: items, identifier: 'groupA', + icon: '', }, ]; @@ -69,16 +78,28 @@ const assessments: IAssessment = { ], }, }; - const changes: IChanges = { - -} + test: { + changeType: '', + componentType: '', + message: '', + }, +}; +const notification: INotificationMessage = { + timestamp: 'test', + landscape: 'test', + message: 'test', + level: 'success', + type: 'test', + date: new Date(), + changelog: { changes }, +}; const landscapeContextValue: LandscapeContextType = { identifier: 'test', landscape: landscape, assessment: assessments, - changes: changes, + changes: notification, next: typeof jest != 'undefined' ? jest.fn() : () => {}, getAssessmentSummary: (fqi) => { return assessments.results[fqi].find((assessmentResult) => assessmentResult.summary) || null; diff --git a/src/main/app/yarn.lock b/src/main/app/yarn.lock index 7e7b083a3..b24cbd62f 100644 --- a/src/main/app/yarn.lock +++ b/src/main/app/yarn.lock @@ -2193,10 +2193,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.24.tgz#b0f86f58564fa02a28b68f8b55d4cdec42e3b9d6" integrity sha512-btt/oNOiDWcSuI721MdL8VQGnjsKjlTMdrKyTcLCKeQp/n4AAMFJ961wMbp+09y8WuGPClDEv07RIItdXKIXAA== -"@types/node@^14.17.18": - version "14.17.27" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.27.tgz#5054610d37bb5f6e21342d0e6d24c494231f3b85" - integrity sha512-94+Ahf9IcaDuJTle/2b+wzvjmutxXAEXU6O81JHblYXUg2BDG+dnBy7VxIPHKAyEEDHzCMQydTJuWvrE+Aanzw== +"@types/node@^14.17.29": + version "14.17.33" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.33.tgz#011ee28e38dc7aee1be032ceadf6332a0ab15b12" + integrity sha512-noEeJ06zbn3lOh4gqe2v7NMGS33jrulfNqYFDjjEbhpDEHR5VTxgYNQSBqBlJIsBJW3uEYDgD6kvMnrrhGzq8g== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -2292,10 +2292,10 @@ "@types/prop-types" "*" csstype "^3.0.2" -"@types/react@^16.14.15": - version "16.14.17" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.17.tgz#c57fcfb05efa6423f5b65fcd4a75f63f05b162bf" - integrity sha512-pMLc/7+7SEdQa9A+hN9ujI8blkjFqYAZVqh3iNXqdZ0cQ8TIR502HMkNJniaOGv9SAgc47jxVKoiBJ7c0AakvQ== +"@types/react@^16.14.20": + version "16.14.20" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.20.tgz#ff6e932ad71d92c27590e4a8667c7a53a7d0baad" + integrity sha512-SV7TaVc8e9E/5Xuv6TIyJ5VhQpZoVFJqX6IZgj5HZoFCtIDCArE3qXkcHlc6O/Ud4UwcMoX+tlvDA95YrKdLgA== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -3893,9 +3893,9 @@ color-name@^1.0.0, color-name@~1.1.4: integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== color-string@^1.5.2: - version "1.5.3" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" - integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== + version "1.6.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312" + integrity sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA== dependencies: color-name "^1.0.0" simple-swizzle "^0.2.2" diff --git a/src/main/java/de/bonndan/nivio/input/ProcessingFinishedEvent.java b/src/main/java/de/bonndan/nivio/input/ProcessingFinishedEvent.java index 1e3b78be3..75d429e23 100644 --- a/src/main/java/de/bonndan/nivio/input/ProcessingFinishedEvent.java +++ b/src/main/java/de/bonndan/nivio/input/ProcessingFinishedEvent.java @@ -1,6 +1,7 @@ package de.bonndan.nivio.input; import de.bonndan.nivio.input.dto.LandscapeDescription; +import de.bonndan.nivio.model.FullyQualifiedIdentifier; import de.bonndan.nivio.model.Landscape; import org.springframework.lang.NonNull; @@ -65,4 +66,9 @@ public String getMessage() { public ProcessingChangelog getChangelog() { return changelog; } + + @Override + public FullyQualifiedIdentifier getSource() { + return landscape.getFullyQualifiedIdentifier(); + } } diff --git a/src/main/java/de/bonndan/nivio/model/Item.java b/src/main/java/de/bonndan/nivio/model/Item.java index a87f69491..34690f27b 100644 --- a/src/main/java/de/bonndan/nivio/model/Item.java +++ b/src/main/java/de/bonndan/nivio/model/Item.java @@ -320,6 +320,4 @@ public List getChanges(final Item newer) { return changes; } - - } \ No newline at end of file diff --git a/src/main/java/de/bonndan/nivio/model/ItemFactory.java b/src/main/java/de/bonndan/nivio/model/ItemFactory.java index 7700a20ed..a9fc93418 100644 --- a/src/main/java/de/bonndan/nivio/model/ItemFactory.java +++ b/src/main/java/de/bonndan/nivio/model/ItemFactory.java @@ -25,7 +25,7 @@ public static Item getTestItem(String group, String identifier) { } public static Item getTestItem(String group, String identifier, Landscape landscape) { - return new Item(identifier, landscape, group, null,null,null, + return new Item(identifier, landscape, group, null, null, null, null, null, null, null, null); } diff --git a/src/main/java/de/bonndan/nivio/output/docs/HtmlGenerator.java b/src/main/java/de/bonndan/nivio/output/docs/HtmlGenerator.java index 49e5dbdc3..f8a5c9ca6 100644 --- a/src/main/java/de/bonndan/nivio/output/docs/HtmlGenerator.java +++ b/src/main/java/de/bonndan/nivio/output/docs/HtmlGenerator.java @@ -123,9 +123,9 @@ protected ContainerTag writeItem(Item item, Assessment assessment, Collection join( dt(FormatUtils.nice( - statusItem.getField().endsWith("." + item.getIdentifier()) - ? statusItem.getField().replace("." + item.getIdentifier(), "") - : statusItem.getField() + statusItem.getField().endsWith("." + item.getIdentifier()) + ? statusItem.getField().replace("." + item.getIdentifier(), "") + : statusItem.getField() ) + " " ).with( span(" " + statusItem.getStatus() + " ") @@ -168,11 +168,11 @@ protected ContainerTag writeItem(Item item, Assessment assessment, Collection li( + item.getInterfaces().stream().filter(Objects::nonNull).map(interfaceItem -> li( span(interfaceItem.getDescription()), iff(StringUtils.hasLength(interfaceItem.getFormat()), span(", format: " + interfaceItem.getFormat())), iff(interfaceItem.getUrl() != null && StringUtils.hasLength(interfaceItem.getUrl().toString()), - span(", ").with(a(interfaceItem.getUrl().toString()).attr("href", interfaceItem.getUrl().toString())) + span(", ").with(a(String.valueOf(interfaceItem.getUrl())).attr("href", String.valueOf(interfaceItem.getUrl()))) ) )) )) diff --git a/src/main/java/de/bonndan/nivio/output/dto/ItemApiModel.java b/src/main/java/de/bonndan/nivio/output/dto/ItemApiModel.java index f429e9216..76fb3b8a0 100644 --- a/src/main/java/de/bonndan/nivio/output/dto/ItemApiModel.java +++ b/src/main/java/de/bonndan/nivio/output/dto/ItemApiModel.java @@ -110,4 +110,8 @@ public String toString() { return getFullyQualifiedIdentifier().toString(); } + public String[] getNetworks() { + return item.getLabels(Label.network).values().toArray(new String[0]); + } + } diff --git a/src/main/java/de/bonndan/nivio/output/icons/IconCannotBeLoadedException.java b/src/main/java/de/bonndan/nivio/output/icons/IconCannotBeLoadedException.java new file mode 100644 index 000000000..4474060b6 --- /dev/null +++ b/src/main/java/de/bonndan/nivio/output/icons/IconCannotBeLoadedException.java @@ -0,0 +1,8 @@ +package de.bonndan.nivio.output.icons; + +public class IconCannotBeLoadedException extends RuntimeException { + + public IconCannotBeLoadedException(String errorMessage) { + super(errorMessage); + } +} \ No newline at end of file diff --git a/src/main/java/de/bonndan/nivio/output/icons/IconMapping.java b/src/main/java/de/bonndan/nivio/output/icons/IconMapping.java index 722d7f43e..eec1d46e9 100644 --- a/src/main/java/de/bonndan/nivio/output/icons/IconMapping.java +++ b/src/main/java/de/bonndan/nivio/output/icons/IconMapping.java @@ -19,6 +19,7 @@ public class IconMapping { private static final Logger LOGGER = LoggerFactory.getLogger(IconMapping.class); public static final String DEFAULT_ICON = "cog"; + public static final String DEFAULT_GROUP_ICON = "hexagon-multiple-outline"; public static final String BACKEND = "backend"; public static final String CACHE = "cache"; diff --git a/src/main/java/de/bonndan/nivio/output/icons/IconService.java b/src/main/java/de/bonndan/nivio/output/icons/IconService.java index 19da35e68..172437eec 100644 --- a/src/main/java/de/bonndan/nivio/output/icons/IconService.java +++ b/src/main/java/de/bonndan/nivio/output/icons/IconService.java @@ -12,8 +12,6 @@ /** * Provides the builtin icons (shipped with nivio) and vendor icons (loaded form remote locations) as embeddable data. - * - * */ @Service public class IconService { @@ -53,7 +51,7 @@ public String getIconUrl(Item item) { } Optional iconUrl = localIcons.getIconUrl(icon); - if(iconUrl.isPresent()) { + if (iconUrl.isPresent()) { if (iconUrl.get().startsWith("http")) { try { return externalIcons.getUrl(new URL(iconUrl.get())).orElse(iconUrl.get()); diff --git a/src/main/java/de/bonndan/nivio/output/icons/LocalIcons.java b/src/main/java/de/bonndan/nivio/output/icons/LocalIcons.java index 5da505ad5..356a8daec 100644 --- a/src/main/java/de/bonndan/nivio/output/icons/LocalIcons.java +++ b/src/main/java/de/bonndan/nivio/output/icons/LocalIcons.java @@ -13,6 +13,7 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import static de.bonndan.nivio.output.icons.IconMapping.DEFAULT_GROUP_ICON; import static de.bonndan.nivio.output.icons.IconMapping.DEFAULT_ICON; /** @@ -21,7 +22,7 @@ public class LocalIcons { private static final Logger LOGGER = LoggerFactory.getLogger(LocalIcons.class); - private static final String initErrorMsg = "Default icon could not be loaded from icon set folder %s." + + private static final String INIT_ERROR_MSG = "Default icon could not be loaded from icon set folder %s." + " Make sure all npm dependencies are installed (or run mvn package)."; public static final String DEFAULT_ICONS_FOLDER = "/static/icons/svg/"; @@ -30,6 +31,8 @@ public class LocalIcons { */ private final String defaultIcon; + private final String defaultGroupIcon; + private final String iconFolder; /** @@ -49,8 +52,13 @@ public LocalIcons(@NonNull final String iconFolder) { this.iconFolder = DEFAULT_ICONS_FOLDER; } defaultIcon = getIconUrl(DEFAULT_ICON).orElseThrow(() -> { - throw new RuntimeException(String.format(initErrorMsg, this.iconFolder)); + throw new IconCannotBeLoadedException(String.format(INIT_ERROR_MSG, this.iconFolder)); + }); + + defaultGroupIcon = getIconUrl(DEFAULT_GROUP_ICON).orElseThrow(() -> { + throw new IconCannotBeLoadedException(String.format(INIT_ERROR_MSG, this.iconFolder)); }); + } public LocalIcons() { @@ -86,6 +94,10 @@ public String getDefaultIcon() { return defaultIcon; } + public String getDefaultGroupIcon() { + return defaultGroupIcon; + } + /** * Creates a SVG data url from the given resource path. Is cached. * diff --git a/src/main/java/de/bonndan/nivio/output/layout/AppearanceProcessor.java b/src/main/java/de/bonndan/nivio/output/layout/AppearanceProcessor.java index 56fd96d30..764ad4905 100644 --- a/src/main/java/de/bonndan/nivio/output/layout/AppearanceProcessor.java +++ b/src/main/java/de/bonndan/nivio/output/layout/AppearanceProcessor.java @@ -1,10 +1,8 @@ package de.bonndan.nivio.output.layout; -import de.bonndan.nivio.model.Item; -import de.bonndan.nivio.model.Label; -import de.bonndan.nivio.model.Labeled; -import de.bonndan.nivio.model.Landscape; +import de.bonndan.nivio.model.*; import de.bonndan.nivio.output.icons.IconService; +import de.bonndan.nivio.output.icons.LocalIcons; import de.bonndan.nivio.util.URLHelper; import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; @@ -26,13 +24,13 @@ public AppearanceProcessor(IconService iconService) { public void process(@NonNull final Landscape landscape) { Objects.requireNonNull(landscape).getGroupItems().forEach(group -> { - setIconFillAppearance(group); - landscape.getItems().retrieve(group.getItems()).forEach(this::setIconFillAppearance); + setIconAndFillAppearance(group); + landscape.getItems().retrieve(group.getItems()).forEach(this::setIconAndFillAppearance); }); - setIconFillAppearance(landscape); + setIconAndFillAppearance(landscape); } - private void setIconFillAppearance(Labeled labeled) { + private void setIconAndFillAppearance(Labeled labeled) { if (labeled instanceof Item) { labeled.setLabel(Label._icondata, iconService.getIconUrl((Item) labeled)); @@ -43,6 +41,10 @@ private void setIconFillAppearance(Labeled labeled) { .flatMap(iconService::getExternalUrl) .ifPresent(s -> labeled.setLabel(Label._icondata, s)); } + if (!StringUtils.hasLength(icon) && labeled instanceof Group) { + LocalIcons localIcons = new LocalIcons(); + labeled.setLabel(Label._icondata, localIcons.getDefaultGroupIcon()); + } } String fill = labeled.getLabel(Label.fill); diff --git a/src/main/java/de/bonndan/nivio/util/IconCannotBeLoadedException.java b/src/main/java/de/bonndan/nivio/util/IconCannotBeLoadedException.java new file mode 100644 index 000000000..5f36ae919 --- /dev/null +++ b/src/main/java/de/bonndan/nivio/util/IconCannotBeLoadedException.java @@ -0,0 +1,8 @@ +package de.bonndan.nivio.util; + +public class IconCannotBeLoadedException extends RuntimeException { + + public IconCannotBeLoadedException(String errorMessage) { + super(errorMessage); + } +} \ No newline at end of file diff --git a/src/main/resources/frontendMapping.yml b/src/main/resources/frontendMapping.yml index 661e2d9be..91d991322 100644 --- a/src/main/resources/frontendMapping.yml +++ b/src/main/resources/frontendMapping.yml @@ -2,5 +2,8 @@ frontendmapping: keys: shortname: short name END_OF_LIFE: end of life + PRODUCTION: production + INTEGRATION: integration + PLANNED: planned descriptions: END_OF_LIFE: An end-of-life product is a product at the end of the product lifecycle which prevents users from receiving updates, indicating that the product is at the end of its useful life. \ No newline at end of file diff --git a/src/test/java/de/bonndan/nivio/model/ItemTest.java b/src/test/java/de/bonndan/nivio/model/ItemTest.java index e83146a46..59fdd3bc5 100644 --- a/src/test/java/de/bonndan/nivio/model/ItemTest.java +++ b/src/test/java/de/bonndan/nivio/model/ItemTest.java @@ -1,6 +1,5 @@ package de.bonndan.nivio.model; -import de.bonndan.nivio.input.kubernetes.InputFormatHandlerKubernetes; import de.bonndan.nivio.assessment.Assessable; import org.junit.jupiter.api.Test; @@ -11,7 +10,8 @@ import static de.bonndan.nivio.model.ItemFactory.getTestItem; import static de.bonndan.nivio.model.ItemFactory.getTestItemBuilder; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; class ItemTest { @@ -54,7 +54,6 @@ void equalsWithLandscape() { } - @Test void getChangesInLabels() { Landscape landscape = LandscapeFactory.createForTesting("l1", "l1Landscape").build(); diff --git a/src/test/java/de/bonndan/nivio/output/dto/ItemApiModelTest.java b/src/test/java/de/bonndan/nivio/output/dto/ItemApiModelTest.java index 9e73dda44..0cd8ffd4e 100644 --- a/src/test/java/de/bonndan/nivio/output/dto/ItemApiModelTest.java +++ b/src/test/java/de/bonndan/nivio/output/dto/ItemApiModelTest.java @@ -97,7 +97,7 @@ void getName() { Item s1 = itemTemplate.build(); ItemApiModel itemApiModel = new ItemApiModel(s1, group); assertThat(itemApiModel.getColor()).isEqualTo(group.getColor()); - assertThat(itemApiModel.getName()).isEqualTo(""); + assertThat(itemApiModel.getName()).isEmpty(); } @Test @@ -189,4 +189,11 @@ void testToString() { assertThat(itemApiModel.getColor()).isEqualTo(group.getColor()); assertThat(itemApiModel.toString()).hasToString("l1/g1/a"); } + + @Test + void getNetworks() { + Item s1 = itemTemplate.build(); + ItemApiModel itemApiModel = new ItemApiModel(s1, group); + assertThat(itemApiModel.getNetworks()).isEqualTo(new String[0]); + } } \ No newline at end of file diff --git a/src/test/java/de/bonndan/nivio/output/icons/LocalIconsTest.java b/src/test/java/de/bonndan/nivio/output/icons/LocalIconsTest.java index fa13e0339..e686d645d 100644 --- a/src/test/java/de/bonndan/nivio/output/icons/LocalIconsTest.java +++ b/src/test/java/de/bonndan/nivio/output/icons/LocalIconsTest.java @@ -1,13 +1,17 @@ package de.bonndan.nivio.output.icons; +import org.junit.Assert; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Base64; import java.util.Optional; +import static de.bonndan.nivio.output.icons.IconMapping.DEFAULT_GROUP_ICON; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; class LocalIconsTest { @@ -47,4 +51,15 @@ void returnsDefault() { void returnsTypeIgnoreCase() { assertThat(localIcons.getIconUrl("AccOunT")).isNotEmpty(); } + + @Test + void returnsGroupDefault() { + String icon = localIcons.getDefaultGroupIcon(); + assertThat(icon).isNotEmpty(); + + String payload = icon.replace(DataUrlHelper.DATA_IMAGE_SVG_XML_BASE_64, ""); + String decoded = new String(Base64.getDecoder().decode(payload)); + assertThat(decoded).contains("xml"); + } + } \ No newline at end of file diff --git a/src/test/java/de/bonndan/nivio/output/layout/AppearanceProcessorTest.java b/src/test/java/de/bonndan/nivio/output/layout/AppearanceProcessorTest.java index 4eb98985c..889c165ab 100644 --- a/src/test/java/de/bonndan/nivio/output/layout/AppearanceProcessorTest.java +++ b/src/test/java/de/bonndan/nivio/output/layout/AppearanceProcessorTest.java @@ -3,6 +3,7 @@ import de.bonndan.nivio.model.*; import de.bonndan.nivio.output.icons.DataUrlHelper; import de.bonndan.nivio.output.icons.IconService; +import de.bonndan.nivio.output.icons.LocalIcons; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -10,12 +11,10 @@ import java.net.URL; import java.util.ArrayList; import java.util.HashSet; -import java.util.Set; import static de.bonndan.nivio.model.ItemFactory.getTestItem; import static de.bonndan.nivio.model.ItemFactory.getTestItemBuilder; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -24,6 +23,7 @@ class AppearanceProcessorTest { private AppearanceProcessor resolver; private Landscape landscape; private IconService iconService; + private LocalIcons localIcons; private Group g1; private ArrayList items; @@ -31,6 +31,7 @@ class AppearanceProcessorTest { public void setup() { iconService = mock(IconService.class); + localIcons = mock(LocalIcons.class); landscape = LandscapeFactory.createForTesting("l1", "l1Landscape").build(); resolver = new AppearanceProcessor(iconService); @@ -54,7 +55,7 @@ public void setup() { } @Test - void setItemIcons_LabelIcon() { + void item_icon_setIconAndFillAppearance() { Item s1 = landscape.getItems().pick("s1", "g1"); s1.setLabel(Label.icon, "https://dedica.team/images/logo_orange_weiss.png"); when(iconService.getIconUrl(s1)).thenReturn(DataUrlHelper.DATA_IMAGE_SVG_XML_BASE_64 + "foo"); @@ -67,7 +68,7 @@ void setItemIcons_LabelIcon() { } @Test - void setItemIcons_LabelFill() throws MalformedURLException { + void item_fill_setIconAndFillAppearance() throws MalformedURLException { Item s1 = landscape.getItems().pick("s1", "g1"); s1.setLabel(Label.fill, "http://dedica.team/images/portrait.jpeg"); when(iconService.getExternalUrl(new URL(s1.getLabel(Label.fill)))).thenReturn(java.util.Optional.of(DataUrlHelper.DATA_IMAGE_SVG_XML_BASE_64 + "foo")); @@ -80,7 +81,7 @@ void setItemIcons_LabelFill() throws MalformedURLException { } @Test - void setLandscapeIcons_LabelIcon() throws MalformedURLException { + void landscape_icon_setIconAndFillAppearance() throws MalformedURLException { // given landscape.setLabel(Label.icon, "https://dedica.team/images/logo_orange_weiss.png"); @@ -94,7 +95,7 @@ void setLandscapeIcons_LabelIcon() throws MalformedURLException { } @Test - void setLandscapeIcons_LabelFill() throws MalformedURLException { + void landscape_fill_setIconAndFillAppearance() throws MalformedURLException { // given landscape.setLabel(Label.fill, "http://dedica.team/images/portrait.jpeg"); @@ -108,7 +109,7 @@ void setLandscapeIcons_LabelFill() throws MalformedURLException { } @Test - void setGroupIcons_LabelIcon() throws MalformedURLException { + void group_icon_setIconAndFillAppearance() throws MalformedURLException { // given g1.setLabel(Label.icon, "https://dedica.team/images/logo_orange_weiss.png"); @@ -122,7 +123,7 @@ void setGroupIcons_LabelIcon() throws MalformedURLException { } @Test - void setGroupIcons_LabelFill() throws MalformedURLException { + void group_fill_setIconAndFillAppearance() throws MalformedURLException { // given g1.setLabel(Label.fill, "http://dedica.team/images/portrait.jpeg"); @@ -135,4 +136,18 @@ void setGroupIcons_LabelFill() throws MalformedURLException { assertThat(g1.getLabel(Label._filldata)).isEqualTo(DataUrlHelper.DATA_IMAGE_SVG_XML_BASE_64 + "foo"); } + @Test + void group_setDefaultIcon_setIconAndFillAppearance() { + + // given + // default group icon + when(localIcons.getDefaultGroupIcon()).thenReturn(DataUrlHelper.DATA_IMAGE_SVG_XML_BASE_64 + "PD94bWwg"); + + // when + resolver.process(landscape); + + // then + assertThat(g1.getIcon()).startsWith(DataUrlHelper.DATA_IMAGE_SVG_XML_BASE_64 + "PD94bWwg"); + } + } \ No newline at end of file diff --git a/src/test/java/de/bonndan/nivio/util/FrontendMappingTest.java b/src/test/java/de/bonndan/nivio/util/FrontendMappingTest.java index 14d421912..f6bb2d07c 100644 --- a/src/test/java/de/bonndan/nivio/util/FrontendMappingTest.java +++ b/src/test/java/de/bonndan/nivio/util/FrontendMappingTest.java @@ -24,8 +24,8 @@ class FrontendMappingTest { @Test @Order(1) void getKeys() { - var testMap = Map.of("shortname", "short name", "END_OF_LIFE", "end of life"); - assertThat(frontendMapping.getKeys()).isEqualTo(testMap); + assertThat(frontendMapping.getKeys()).containsEntry("END_OF_LIFE", "end of life"); + assertThat(frontendMapping.getKeys()).containsEntry("shortname", "short name"); } @Test diff --git a/src/test/resources/example/dedica.dot b/src/test/resources/example/dedica.dot index a272ddf67..b3b2c2aab 100644 --- a/src/test/resources/example/dedica.dot +++ b/src/test/resources/example/dedica.dot @@ -47,14 +47,14 @@ digraph G { nivio_name = "Daniel Brünker", nivio_contact= "daniel.bruenker@dedica.team", nivio_group= dedica - nivio_fill= "http://dedica.team/images/danielbruenker.jpeg" + nivio_fill= "https://dedica.team/images/danielbruenker.jpeg" ] jennifer [ nivio_name = "Jennifer Arps", nivio_contact= "jennifer@dedica.team", nivio_group= dedica - nivio_fill= "http://dedica.team/images/jenniferarps.jpeg" + nivio_fill= "https://dedica.team/images/jenniferarps.jpg" ] dedica -> matthias [nivio_type = provider] @@ -62,5 +62,7 @@ digraph G { dedica -> jan [nivio_type = provider] dedica -> michelle [nivio_type = provider] dedica -> marvin [nivio_type = provider] + dedica -> daniel2 [nivio_type = provider] + dedica -> jennifer [nivio_type = provider] } \ No newline at end of file diff --git a/src/test/resources/example/dedica.yml b/src/test/resources/example/dedica.yml index c3d135298..77a4cfe8c 100644 --- a/src/test/resources/example/dedica.yml +++ b/src/test/resources/example/dedica.yml @@ -57,20 +57,20 @@ items: - identifier: marvin name: Marvin Schöning group: dedica - fill: http://dedica.team/images/marvinschoening.jpeg + fill: https://dedica.team/images/marvinschoening.jpeg providedBy: - dedica - identifier: daniel2 name: Daniel Brünker group: dedica - fill: http://dedica.team/images/danielbruenker.jpeg + fill: https://dedica.team/images/danielbruenker.jpeg providedBy: - dedica - identifier: jennifer name: Jennifer Arps group: dedica - fill: http://dedica.team/images/jenniferarps.jpeg + fill: https://dedica.team/images/jenniferarps.jpg providedBy: - dedica \ No newline at end of file