UI: Add components page
This adds a new page /components that lists all components which are retieved from the /components API endpoint. For the look and feel this page has a similar design like the builds and buildsets pages. Change-Id: I38b3a9b456b71e49f02712b63417a905a0aa1397
This commit is contained in:
parent
495418c350
commit
7c6af60a5d
|
@ -54,6 +54,7 @@ import {
|
|||
BellIcon,
|
||||
BookIcon,
|
||||
CodeIcon,
|
||||
ServiceIcon,
|
||||
UsersIcon,
|
||||
} from '@patternfly/react-icons'
|
||||
|
||||
|
@ -198,6 +199,11 @@ class App extends React.Component {
|
|||
})
|
||||
}
|
||||
|
||||
handleComponentsLink = () => {
|
||||
const { history } = this.props
|
||||
history.push('/components')
|
||||
}
|
||||
|
||||
handleApiLink = () => {
|
||||
const { history } = this.props
|
||||
history.push('/openapi')
|
||||
|
@ -306,6 +312,12 @@ class App extends React.Component {
|
|||
const nav = this.renderMenu()
|
||||
|
||||
const kebabDropdownItems = [
|
||||
<DropdownItem
|
||||
key="components"
|
||||
onClick={event => this.handleComponentsLink(event)}
|
||||
>
|
||||
<ServiceIcon /> Components
|
||||
</DropdownItem>,
|
||||
<DropdownItem key="api" onClick={event => this.handleApiLink(event)}>
|
||||
<CodeIcon /> API
|
||||
</DropdownItem>,
|
||||
|
@ -335,6 +347,13 @@ class App extends React.Component {
|
|||
<PageHeaderToolsGroup
|
||||
visibility={{ default: 'hidden', lg: 'visible' }}
|
||||
>
|
||||
<PageHeaderToolsItem>
|
||||
<Link to='/components'>
|
||||
<Button variant={ButtonVariant.plain}>
|
||||
<ServiceIcon /> Components
|
||||
</Button>
|
||||
</Link>
|
||||
</PageHeaderToolsItem>
|
||||
<PageHeaderToolsItem>
|
||||
<Link to='/openapi'>
|
||||
<Button variant={ButtonVariant.plain}>
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
// Copyright 2021 BMW Group
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { capitalize } from '@patternfly/react-core'
|
||||
import {
|
||||
expandable,
|
||||
Table,
|
||||
TableBody,
|
||||
TableHeader,
|
||||
TableVariant,
|
||||
} from '@patternfly/react-table'
|
||||
import {
|
||||
CodeIcon,
|
||||
OnRunningIcon,
|
||||
OutlinedHddIcon,
|
||||
PauseCircleIcon,
|
||||
RunningIcon,
|
||||
QuestionIcon,
|
||||
StopCircleIcon,
|
||||
} from '@patternfly/react-icons'
|
||||
|
||||
import { IconProperty } from '../build/Misc'
|
||||
|
||||
const STATE_ICON_CONFIGS = {
|
||||
RUNNING: {
|
||||
icon: RunningIcon,
|
||||
color: 'var(--pf-global--success-color--100)',
|
||||
},
|
||||
PAUSED: {
|
||||
icon: PauseCircleIcon,
|
||||
color: 'var(--pf-global--warning-color--100)',
|
||||
},
|
||||
STOPPED: {
|
||||
icon: StopCircleIcon,
|
||||
color: 'var(--pf-global--danger-color--100)',
|
||||
},
|
||||
}
|
||||
|
||||
const DEFAULT_STATE_ICON_CONFIG = {
|
||||
icon: QuestionIcon,
|
||||
color: 'var(--pf-global--info-color--100)',
|
||||
}
|
||||
|
||||
function ComponentStateIcon({ state }) {
|
||||
const iconConfig =
|
||||
STATE_ICON_CONFIGS[state.toUpperCase()] || DEFAULT_STATE_ICON_CONFIG
|
||||
const Icon = iconConfig.icon
|
||||
|
||||
return (
|
||||
<span style={{ color: iconConfig.color }}>
|
||||
<Icon
|
||||
size="sm"
|
||||
style={{
|
||||
marginRight: 'var(--pf-global--spacer--sm)',
|
||||
verticalAlign: '-0.2em',
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
ComponentStateIcon.propTypes = {
|
||||
state: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
function ComponentState({ state }) {
|
||||
const iconConfig =
|
||||
STATE_ICON_CONFIGS[state.toUpperCase()] || DEFAULT_STATE_ICON_CONFIG
|
||||
|
||||
return <span style={{ color: iconConfig.color }}>{state.toUpperCase()}</span>
|
||||
}
|
||||
|
||||
ComponentState.propTypes = {
|
||||
state: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
function ComponentTable({ components }) {
|
||||
// We have to keep the rows in state to be complient to how the PF4
|
||||
// expandable/collapsible table works (see the handleCollapse function).
|
||||
const [rows, setRows] = useState([])
|
||||
|
||||
const sortComponents = (a, b) => {
|
||||
if (a.hostname < b.hostname) {
|
||||
return -1
|
||||
}
|
||||
if (a.hostname > b.hostname) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const createTableRows = () => {
|
||||
const allRows = []
|
||||
let i = 0
|
||||
let sectionIndex = 0
|
||||
for (const [kind, _components] of Object.entries(components)) {
|
||||
sectionIndex = i
|
||||
i++
|
||||
const sectionRows = []
|
||||
for (const component of [..._components].sort(sortComponents)) {
|
||||
sectionRows.push(createComponentRow(kind, component, sectionIndex))
|
||||
i++
|
||||
}
|
||||
allRows.push(createSectionRow(kind, sectionRows.length))
|
||||
allRows.push(...sectionRows)
|
||||
}
|
||||
|
||||
return allRows
|
||||
}
|
||||
|
||||
setRows(createTableRows())
|
||||
// Ensure that the effect is only called once and not after each
|
||||
// render (which would happen if no dependency array is provided).
|
||||
// But as we are changing the state during the effect, this would
|
||||
// result in an infinite render loop as every state change
|
||||
// re-renders the component.
|
||||
// Side note: We could also pass an empty depdency array here, but
|
||||
// eslint is complaining about that. So we provide the components
|
||||
// variable which is provided via props and thus doesn't change
|
||||
// during the lifetime of this react component.
|
||||
}, [components])
|
||||
|
||||
// TODO (felix): We could change this to an expandable table and show some
|
||||
// details about the component in the expandable row. E.g. similar to what
|
||||
// OpenShift shows in for deployments and pods (metrics, performance,
|
||||
// additional attributes).
|
||||
const columns = [
|
||||
{
|
||||
title: <IconProperty icon={<OutlinedHddIcon />} value="Hostname" />,
|
||||
dataLabel: 'Hostname',
|
||||
cellFormaters: [expandable],
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<OnRunningIcon />} value="State" />,
|
||||
dataLabel: 'State',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<CodeIcon />} value="Version" />,
|
||||
dataLabel: 'Version',
|
||||
},
|
||||
]
|
||||
|
||||
function createSectionRow(kind, childrenCount) {
|
||||
return {
|
||||
// Keep all sections open on initial page load. The handleCollapse()
|
||||
// function will deal with open/closing sections.
|
||||
isOpen: true,
|
||||
cells: [`${capitalize(kind)} (${childrenCount})`],
|
||||
}
|
||||
}
|
||||
|
||||
function createComponentRow(kind, component, parent_id) {
|
||||
return {
|
||||
parent: parent_id,
|
||||
cells: [
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
<ComponentStateIcon state={component.state} />{' '}
|
||||
{component.hostname}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <ComponentState state={component.state} />,
|
||||
},
|
||||
component.version,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function handleCollapse(event, rowKey, isOpen) {
|
||||
const _rows = [...rows]
|
||||
/* Note from PF4:
|
||||
* Please do not use rowKey as row index for more complex tables.
|
||||
* Rather use some kind of identifier like ID passed with each row.
|
||||
*/
|
||||
rows[rowKey].isOpen = isOpen
|
||||
setRows(_rows)
|
||||
}
|
||||
|
||||
return (
|
||||
/* NOTE (felix): The mobile version of this expandable table looks kind of
|
||||
* broken, but the same applies to the example in the PF4 documentation:
|
||||
* https://www.patternfly.org/2020.04/documentation/react/components/table#compact-expandable
|
||||
*
|
||||
* I don't think this is something we have to attract now, but we should
|
||||
* keep this note as reference.
|
||||
*/
|
||||
<>
|
||||
<Table
|
||||
aria-label="Components Table"
|
||||
variant={TableVariant.compact}
|
||||
onCollapse={handleCollapse}
|
||||
cells={columns}
|
||||
rows={rows}
|
||||
className="zuul-build-table"
|
||||
>
|
||||
<TableHeader />
|
||||
<TableBody />
|
||||
</Table>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ComponentTable.propTypes = {
|
||||
components: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
export default ComponentTable
|
|
@ -61,6 +61,17 @@ a.refresh {
|
|||
font-size: var(--pf-global--FontSize--md);
|
||||
}
|
||||
|
||||
/* Align padding of compact expendable (child) rows. Without this there is
|
||||
nearly no padding. */
|
||||
.zuul-build-table .pf-c-table__expandable-row.pf-m-expanded {
|
||||
--pf-c-table--cell--PaddingTop: var(
|
||||
--pf-c-table--m-compact--cell--PaddingTop
|
||||
);
|
||||
--pf-c-table--cell--PaddingBottom: var(
|
||||
--pf-c-table--m-compact--cell--PaddingBottom
|
||||
);
|
||||
}
|
||||
|
||||
/* Use the same hover effect on table rows like for the selectable data list */
|
||||
.zuul-build-table tbody tr:hover {
|
||||
box-shadow: var(--pf-global--BoxShadow--sm-top),
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright 2021 BMW Group
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React, { useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateVariant,
|
||||
EmptyStateIcon,
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
Text,
|
||||
TextContent,
|
||||
Title,
|
||||
} from '@patternfly/react-core'
|
||||
import { ServiceIcon } from '@patternfly/react-icons'
|
||||
|
||||
import { fetchComponents } from '../actions/component'
|
||||
import { Fetching } from '../containers/Fetching'
|
||||
import ComponentTable from '../containers/component/ComponentTable'
|
||||
|
||||
function ComponentsPage({ components, isFetching, fetchComponents }) {
|
||||
useEffect(() => {
|
||||
document.title = 'Zuul Components'
|
||||
fetchComponents()
|
||||
}, [fetchComponents])
|
||||
|
||||
// TODO (felix): Let the table handle the empty state and the fetching,
|
||||
// similar to the builds table.
|
||||
const content =
|
||||
components === undefined || isFetching ? (
|
||||
<Fetching />
|
||||
) : Object.keys(components).length === 0 ? (
|
||||
<EmptyState variant={EmptyStateVariant.small}>
|
||||
<EmptyStateIcon icon={ServiceIcon} />
|
||||
<Title headingLevel="h4" size="lg">
|
||||
It looks like no components are connected to ZooKeeper
|
||||
</Title>
|
||||
</EmptyState>
|
||||
) : (
|
||||
<ComponentTable components={components} />
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<TextContent>
|
||||
<Text component="h1">Components</Text>
|
||||
<Text component="p">
|
||||
This page shows all Zuul components and their current state.
|
||||
</Text>
|
||||
</TextContent>
|
||||
{content}
|
||||
</PageSection>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ComponentsPage.propTypes = {
|
||||
components: PropTypes.object.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
fetchComponents: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
components: state.component.components,
|
||||
isFetching: state.component.isFetching,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = { fetchComponents }
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ComponentsPage)
|
|
@ -12,6 +12,7 @@
|
|||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import ComponentsPage from './pages/Components'
|
||||
import StatusPage from './pages/Status'
|
||||
import ChangeStatusPage from './pages/ChangeStatus'
|
||||
import ProjectPage from './pages/Project'
|
||||
|
@ -127,7 +128,12 @@ const routes = () => [
|
|||
to: '/openapi',
|
||||
component: OpenApiPage,
|
||||
noTenantPrefix: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
to: '/components',
|
||||
component: ComponentsPage,
|
||||
noTenantPrefix: true,
|
||||
},
|
||||
]
|
||||
|
||||
export { routes }
|
||||
|
|
Loading…
Reference in New Issue