Migrate to patternfly 4

Change-Id: Ieb31bc739740c382d46a074f15ce7e9e94c1b480
This commit is contained in:
Guillaume Vincent 2018-12-18 15:21:36 +01:00
parent 3aa04800ef
commit d42b04f049
19 changed files with 1164 additions and 1803 deletions

2184
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,17 +2,18 @@
"name": "ara-web",
"version": "1.0.0",
"private": true,
"homepage": "./",
"dependencies": {
"@patternfly/patternfly-next": "1.0.105",
"axios": "^0.18.0",
"patternfly": "^3.54.2",
"patternfly-react": "^2.17.3",
"react": "^16.5.1",
"react-dom": "^16.5.1",
"react-redux": "^5.0.7",
"react-router-dom": "^4.3.1",
"react-scripts": "^1.1.5",
"redux": "^4.0.0",
"redux-thunk": "^2.3.0"
"redux-thunk": "^2.3.0",
"styled-components": "^4.1.3"
},
"scripts": {
"start": "react-scripts start",

View File

@ -1,3 +0,0 @@
body {
background-color: #f5f5f5;
}

View File

@ -2,57 +2,42 @@ import React, { Component } from "react";
import { BrowserRouter, Route, Switch, Redirect } from "react-router-dom";
import { Provider } from "react-redux";
import "./App.css";
import "@patternfly/patternfly-next/patternfly.css";
import store from "./store";
import { setConfig } from "./config/configActions";
import * as Containers from "./containers";
class App extends Component {
constructor(props) {
super(props);
this.state = {
loading: true
};
}
state = {
isLoading: true
};
componentDidMount() {
store.dispatch(
setConfig({
apiURL: "http://localhost:8000",
ara_version: "1.0.0",
ansible_version: "2.6",
python_version: "3.6"
apiURL: "http://localhost:8000"
})
);
this.setState({ loading: false });
this.setState({ isLoading: false });
}
render() {
const { isLoading } = this.state;
if (isLoading) return null;
return (
<div className="App">
{this.state.loading ? (
<Containers.LoadingContainer />
) : (
<Provider store={store}>
<BrowserRouter>
<Switch>
<Redirect from="/" exact to="/playbooks" />
<Route
path="/playbooks"
exact
component={Containers.PlaybooksContainer}
/>
<Route
path="/about"
exact
component={Containers.AboutContainer}
/>
<Route component={Containers.Container404} />
</Switch>
</BrowserRouter>
</Provider>
)}
</div>
<Provider store={store}>
<BrowserRouter>
<Switch>
<Redirect from="/" exact to="/playbooks" />
<Route
path="/playbooks"
exact
component={Containers.PlaybooksContainer}
/>
<Route component={Containers.Container404} />
</Switch>
</BrowserRouter>
</Provider>
);
}
}

View File

@ -1,12 +0,0 @@
import React, { Component } from "react";
import { MainContainer } from "../containers";
export default class AboutContainer extends Component {
render() {
return (
<MainContainer>
<p>AboutContainer</p>
</MainContainer>
);
}
}

View File

@ -1,5 +1,3 @@
export { default as Container404 } from "./layout/Container404";
export { default as LoadingContainer } from "./layout/LoadingContainer";
export { default as MainContainer } from "./layout/MainContainer";
export { default as PlaybooksContainer } from "./playbooks/PlaybooksContainer";
export { default as AboutContainer } from "./about/AboutContainer";

View File

@ -1,5 +0,0 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}

View File

@ -1,8 +1,5 @@
import React from "react";
import ReactDOM from "react-dom";
import "patternfly/dist/css/patternfly.min.css";
import "patternfly/dist/css/patternfly-additions.min.css";
import "./index.css";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));

View File

@ -5,7 +5,15 @@ export default class Container404 extends Component {
render() {
return (
<MainContainer>
<p>404</p>
<div className="pf-l-bullseye">
<div className="pf-l-bullseye__item">
<div className="pf-c-card">
<div className="pf-c-card__body">
<p>We are looking for your page...but we can't find it</p>
</div>
</div>
</div>
</div>
</MainContainer>
);
}

View File

@ -1,7 +0,0 @@
import React, { Component } from "react";
export default class LoadingContainer extends Component {
render() {
return <div>loading...</div>;
}
}

View File

@ -1,18 +1,18 @@
import React, { Component } from "react";
import Navbar from "./navigation/Navbar";
import { Grid, Row, Col } from "patternfly-react";
import Header from "./navigation/Header";
export default class MainContainer extends Component {
render() {
const { children } = this.props;
return (
<div className="MainContent">
<Navbar />
<Grid fluid>
<Row>
<Col xs={12}>{children}</Col>
</Row>
</Grid>
<div>
<div className="pf-c-background-image" />
<div className="pf-c-page" id="page-layout-horizontal-nav">
<Header />
<main role="main" className="pf-c-page__main">
<section className="pf-c-page__main-section">{children}</section>
</main>
</div>
</div>
);
}

View File

@ -0,0 +1,72 @@
import React, { Component } from "react";
import styled from "styled-components";
import { Link } from "react-router-dom";
import { withRouter } from "react-router";
import logo from "./logo.svg";
const AraImg = styled.img`
height: 45px;
`;
export class NavLink extends Component {
render() {
const { to, className, location, children, ...rest } = this.props;
return (
<Link
to={to}
className={`${className} ${
location.pathname === to ? "pf-m-current" : ""
}`}
{...rest}
>
{children}
</Link>
);
}
}
export class Header extends Component {
render() {
const { location } = this.props;
return (
<header role="banner" className="pf-c-page__header">
<div className="pf-c-page__header-brand">
<Link to="/playbooks" className="pf-c-page__header-brand-link">
<AraImg className="pf-c-brand" src={logo} alt="Ara Logo" />
</Link>
</div>
<div className="pf-c-page__header-nav">
<nav
className="pf-c-nav"
id="page-layout-horizontal-nav-horizontal-nav"
aria-label="Horizontal Nav Example"
>
<ul className="pf-c-nav__horizontal-list">
<li className="pf-c-nav__item">
<NavLink
to="/playbooks"
className="pf-c-nav__link"
location={location}
>
Playbooks
</NavLink>
</li>
<li className="pf-c-nav__item">
<a
href="https://ara.readthedocs.io/en/latest/"
target="_blank"
rel="noopener noreferrer"
className="pf-c-nav__link"
>
Documentation
</a>
</li>
</ul>
</nav>
</div>
</header>
);
}
}
export default withRouter(Header);

View File

@ -1,15 +0,0 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
export default class NavLink extends Component {
render() {
const { id, to, location, children, ...rest } = this.props;
return (
<li className={location.pathname === to ? "active" : ""}>
<Link to={to} id={`navbar-navbar-primary__${id}-link`} {...rest}>
{children}
</Link>
</li>
);
}
}

View File

@ -1,105 +0,0 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { withRouter } from "react-router";
import logo from "./logo.svg";
import NavLink from "./NavLink";
export class Navbar extends Component {
render() {
const { location, config } = this.props;
const { ara_version, ansible_version, python_version } = config;
return (
<nav className="navbar navbar-default navbar-pf">
<div className="navbar-header">
<button
type="button"
className="navbar-toggle"
data-toggle="collapse"
data-target=".navbar-collapse-1"
>
<span className="sr-only">Toggle navigation</span>
<span className="icon-bar" />
<span className="icon-bar" />
<span className="icon-bar" />
</button>
<Link
to="/playbooks"
id="navbar-navbar-header__playbooks-link"
className="navbar-brand"
>
<img
src={logo}
alt="ARA: Ansible Run Analysis"
width="81"
height="32"
/>
</Link>
</div>
<div className="collapse navbar-collapse navbar-collapse-1">
<ul className="nav navbar-nav navbar-utility">
<li>
<a
href="https://ara.readthedocs.io/en/latest/"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</li>
{ara_version ? (
<li>
<a
href="https://github.com/openstack/ara"
target="_blank"
rel="noopener noreferrer"
>
<strong>ARA</strong> {ara_version}
</a>
</li>
) : null}
{ansible_version ? (
<li>
<a
href="https://www.ansible.com/"
target="_blank"
rel="noopener noreferrer"
>
<strong>Ansible</strong> {ansible_version}
</a>
</li>
) : null}
{python_version ? (
<li>
<a
href="https://www.python.org/"
target="_blank"
rel="noopener noreferrer"
>
<strong>Python</strong> {python_version}
</a>
</li>
) : null}
</ul>
<ul className="nav navbar-nav navbar-primary">
<NavLink id="playbooks" to="/playbooks" location={location}>
Playbooks reports
</NavLink>
<NavLink id="about" to="/about" location={location}>
About
</NavLink>
</ul>
</div>
</nav>
);
}
}
function mapStateToProps(state) {
return {
config: state.config
};
}
export default withRouter(connect(mapStateToProps)(Navbar));

View File

@ -1,9 +1,58 @@
import React, { Component } from "react";
import { Row, Col, ListView, Icon } from "patternfly-react";
import { Link } from "react-router-dom";
import PlaybookArgs from "./PlaybookArgs";
import PlaybookHosts from "./PlaybookHosts";
import PlaybookFiles from "./PlaybookFiles";
import styled from "styled-components";
function _getIconInfo(status) {
switch (status) {
case "running":
return {
title: "Playbook is in progress.",
icon: "fa-pause",
color: "blue"
};
case "completed":
return {
title: "Playbook has completed successfully.",
icon: "fa-check",
color: "green"
};
case "failed":
return {
title: "Playbook has failed with one or more errors.",
icon: "fa-warning",
color: "red"
};
default:
return {
title: "Playbook's status is unknown.",
icon: "fa-warning",
color: "red"
};
}
}
const IconWrapper = styled.i`
color: ${props => props.color};
`;
class StatusIcon extends Component {
render() {
const { status } = this.props;
const iconInfo = _getIconInfo(status);
return (
<IconWrapper
color={iconInfo.color}
className={`fas ${iconInfo.icon}`}
title={iconInfo.title}
/>
);
}
}
const DataListCell = styled.div`
cursor: pointer;
`;
export default class Playbook extends Component {
constructor(props) {
@ -17,120 +66,74 @@ export default class Playbook extends Component {
_toggleExpanded = selection => {
this.setState(prevState => {
if (selection === prevState.selection) {
return { expanded: !prevState.expanded };
return { expanded: !prevState.expanded, selection: null };
} else {
return { expanded: true, selection };
}
});
};
_getStatusMetadata(status) {
switch(status) {
case "running":
return {
"name": "running",
"title": "Playbook is in progress.",
"className": "pficon pficon-info list-view-pf-icon-md list-view-pf-icon-info"
}
case "completed":
return {
"name": "completed",
"title": "Playbook has completed successfully.",
"className": "pficon pficon-ok list-view-pf-icon-md list-view-pf-icon-success"
}
case "failed":
return {
"name": "failed",
"title": "Playbook has failed with one or more errors.",
"className": "pficon pficon-error-circle-o list-view-pf-icon-md list-view-pf-icon-danger"
}
case "unknown":
return {
"name": "unknown",
"title": "Playbook's status is unknown.",
"className": "pficon pficon-warning-triangle-o list-view-pf-icon-md list-view-pf-icon-warning"
}
default:
return {
"name": "unknown",
"title": "Playbook's status is unknown.",
"className": "pficon pficon-warning-triangle-o list-view-pf-icon-md list-view-pf-icon-warning"
}
}
}
render() {
const { playbook } = this.props;
const { expanded, selection } = this.state;
const status_metadata = this._getStatusMetadata(playbook.status);
const LeftIcon =
<ListView.Icon
name={status_metadata["name"]}
size="md"
title={status_metadata["title"]}
className={status_metadata["className"]}
/>
return (
<ListView.Item
checkboxInput={
<Link to={`/playbooks/${playbook.id}`} className="navbar-brand">
<Icon name="link" size="lg" />
</Link>
}
leftContent={LeftIcon}
additionalInfo={[
<ListView.InfoItem key="args">
<ListView.Expand
expanded={expanded && selection === "args"}
toggleExpanded={() => this._toggleExpanded("args")}
>
<Icon name="cogs" />
Arguments
</ListView.Expand>
</ListView.InfoItem>,
<ListView.InfoItem key="hosts">
<ListView.Expand
expanded={expanded && selection === "hosts"}
toggleExpanded={() => this._toggleExpanded("hosts")}
>
<Icon name="server" />
<b>{playbook.hosts.length}</b> Hosts
</ListView.Expand>
</ListView.InfoItem>,
<ListView.InfoItem key="files">
<ListView.Expand
expanded={expanded && selection === "files"}
toggleExpanded={() => this._toggleExpanded("files")}
>
<Icon name="folder-open" />
<b>{playbook.files.length}</b> Files
</ListView.Expand>
</ListView.InfoItem>
]}
actions={
<span>
<Icon name="clock-o" size="lg" /> {Math.round(playbook.duration)} sec
</span>
}
heading={
playbook.name
? playbook.name
: playbook.file.path.split("/").slice(-1)[0]
}
stacked={false}
compoundExpand
compoundExpanded={expanded}
onCloseCompoundExpand={() => this.setState({ expanded: false })}
>
<Row>
<Col xs={12} sm={8} smOffset={2} md={6} mdOffset={3}>
<ul className="pf-c-data-list pf-u-box-shadow-md">
<li
className={`pf-c-data-list__item ${expanded ? "pf-m-expanded" : ""}`}
>
<div className="pf-c-data-list__check">
<StatusIcon status={playbook.status} />
</div>
<DataListCell className="pf-c-data-list__cell pf-m-flex-5">
{playbook.file.path.split("/").slice(-1)[0]}
</DataListCell>
<DataListCell
className="pf-c-data-list__cell pf-m-flex-1"
onClick={() => this._toggleExpanded("args")}
>
<i
className={`fas fa-angle-${
selection === "args" ? "down" : "right"
} pf-u-mr-xs`}
/>
<b>{Object.keys(playbook.arguments).length}</b> arguments
</DataListCell>
<DataListCell
className="pf-c-data-list__cell pf-m-flex-1"
onClick={() => this._toggleExpanded("hosts")}
>
<i
className={`fas fa-angle-${
selection === "hosts" ? "down" : "right"
} pf-u-mr-xs`}
/>
<b>{playbook.hosts.length}</b> Hosts
</DataListCell>
<DataListCell
className="pf-c-data-list__cell pf-m-flex-1"
onClick={() => this._toggleExpanded("files")}
>
<i
className={`fas fa-angle-${
selection === "files" ? "down" : "right"
} pf-u-mr-xs`}
/>
<b>{playbook.files.length}</b> Files
</DataListCell>
<DataListCell className="pf-c-data-list__cell pf-u-text-align-right">
<i className={`fas fa-clock pf-u-mr-xs`} />
{Math.round(playbook.duration)} sec
</DataListCell>
<section
className="pf-c-data-list__expandable-content"
aria-label="Primary Content Details"
>
{selection === "args" && <PlaybookArgs playbook={playbook} />}
{selection === "hosts" && <PlaybookHosts playbook={playbook} />}
{selection === "files" && <PlaybookFiles playbook={playbook} />}
</Col>
</Row>
</ListView.Item>
</section>
</li>
</ul>
);
}
}

View File

@ -1,35 +1,4 @@
import React, { Component } from "react";
import { OverlayTrigger, Tooltip, Icon } from "patternfly-react";
export class ParamatersHelpIcon extends Component {
render() {
return (
<span style={{ float: "right" }}>
<OverlayTrigger
overlay={
<Tooltip id="tooltip">
<div>
<h3>Tips: Arguments</h3>
<hr />
<p>
This panel contains all the arguments and options passed to
the ansible-playbook command.
</p>
<p>
You can control which arguments ARA should ignore with the{" "}
<code>ignored_arguments</code> configuration.
</p>
</div>
</Tooltip>
}
placement="bottom"
>
<Icon name="question-circle" />
</OverlayTrigger>
</span>
);
}
}
export default class PlaybookArgs extends Component {
constructor(props) {
@ -55,35 +24,28 @@ export default class PlaybookArgs extends Component {
arg => arg.toLowerCase().indexOf(search.toLowerCase()) !== -1
);
return (
<div className="table-response">
<div className="dataTables_header">
<div className="dataTables_filter">
<div>
<div className="pf-l-grid pf-m-gutter pf-u-display-flex pf-u-align-items-center">
<div className="pf-l-grid__item">
<input
className="form-control"
className="pf-c-form-control"
placeholder="Search an argument"
type="search"
value={search}
onChange={e => this.setState({ search: e.target.value })}
/>
</div>
<div className="dataTables_info">
Showing <b>{filteredArgs.length}</b> of{" "}
<b>{args.length}</b> args
<ParamatersHelpIcon />
<div className="pf-l-grid__item">
{`Showing ${filteredArgs.length} of ${args.length} args`}
</div>
</div>
<table className="table table-striped table-bordered table-hover">
<thead>
<tr>
<th>Argument</th>
<th>Value</th>
</tr>
</thead>
<table className="pf-c-table pf-m-compact pf-m-grid-md" role="grid">
<tbody>
{filteredArgs.map((arg, i) => (
<tr key={i}>
<td>{arg}</td>
<td>{this._renderArg(playbook.arguments[arg])}</td>
<td className="pf-m-width-30">{arg}</td>
<td className="pf-m-width-70">{this._renderArg(playbook.arguments[arg])}</td>
</tr>
))}
</tbody>

View File

@ -1,5 +1,9 @@
import React, { Component } from "react";
import { Button, Icon, Modal } from "patternfly-react";
import styled from "styled-components";
const ModalBox = styled.div`
position: absolute;
`;
export default class PlaybookFiles extends Component {
state = {
@ -16,48 +20,52 @@ export default class PlaybookFiles extends Component {
const { playbook } = this.props;
const { showModal, filePath, fileContent } = this.state;
return (
<div className="table-response">
<Modal show={showModal} onHide={this.close} bsSize="large">
<Modal.Header>
<button
className="close"
onClick={this.close}
aria-hidden="true"
aria-label="Close"
<div>
{showModal && (
<ModalBox>
<div
className="pf-c-modal-box pf-m-lg"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<Icon type="pf" name="close" />
</button>
<Modal.Title>{filePath}</Modal.Title>
</Modal.Header>
<Modal.Body>
<pre>
<code>{fileContent}</code>
</pre>
</Modal.Body>
<Modal.Footer>
<Button
bsStyle="default"
className="btn-cancel"
onClick={this.close}
>
close
</Button>
</Modal.Footer>
</Modal>
<table className="table table-striped table-bordered table-hover">
<thead>
<tr>
<th>Name</th>
<th>Actions</th>
</tr>
</thead>
<div className="pf-c-modal-box__close">
<button
className="pf-c-button pf-m-plain"
aria-label="Close"
onClick={this.close}
>
<i className="fas fa-times" aria-hidden="true" />
</button>
</div>
<header className="pf-c-modal-box__header">
<h1 className="pf-c-modal-box__header-title" id="modal-title">
{filePath}
</h1>
</header>
<div className="pf-c-modal-box__body" id="modal-description">
<pre>
<code>{fileContent}</code>
</pre>
</div>
<footer className="pf-c-modal-box__footer">
<button type="button" onClick={this.close}>
close
</button>
</footer>
</div>
</ModalBox>
)}
<table className="pf-c-table pf-m-compact pf-m-grid-md" role="grid">
<tbody>
{playbook.files.map(file => (
<tr key={file.id}>
<td>{file.path}</td>
<td className="text-center">
<Button
bsStyle="primary"
<td>
<button
type="button"
className="pf-c-button pf-m-secondary"
onClick={() =>
this.setState({
showModal: true,
@ -66,8 +74,8 @@ export default class PlaybookFiles extends Component {
})
}
>
<Icon name="eye" />
</Button>
See content
</button>
</td>
</tr>
))}

View File

@ -1,30 +1,4 @@
import React, { Component } from "react";
import { OverlayTrigger, Tooltip, Icon } from "patternfly-react";
export class HostsHelpIcon extends Component {
render() {
return (
<span style={{ float: "right" }}>
<OverlayTrigger
overlay={
<Tooltip id="tooltip">
<div>
<h3>Tips: Hosts</h3>
<hr />
<p>
This panel contains all the hosts involved in the playbook.
</p>
</div>
</Tooltip>
}
placement="bottom"
>
<Icon name="question-circle" />
</OverlayTrigger>
</span>
);
}
}
export default class PlaybookHosts extends Component {
constructor(props) {
@ -41,35 +15,30 @@ export default class PlaybookHosts extends Component {
host => host.name.toLowerCase().indexOf(search.toLowerCase()) !== -1
);
return (
<div className="table-response">
<div className="dataTables_header">
<div className="dataTables_filter">
<div>
<div className="pf-l-grid pf-m-gutter pf-u-display-flex pf-u-align-items-center">
<div className="pf-l-grid__item">
<input
className="form-control"
className="pf-c-form-control"
placeholder="Search a host"
type="search"
value={search}
onChange={e => this.setState({ search: e.target.value })}
/>
</div>
<div className="dataTables_info">
Showing <b>{filteredHosts.length}</b> of{" "}
<b>{playbook.hosts.length}</b> hosts
<HostsHelpIcon />
<div className="pf-l-grid__item">
{`Showing ${filteredHosts.length} of ${
playbook.hosts.length
} hosts`}
</div>
</div>
<table className="table table-striped table-bordered table-hover">
<thead>
<tr>
<th>Name</th>
<th>Alias</th>
</tr>
</thead>
<table className="pf-c-table pf-m-compact pf-m-grid-md" role="grid">
<tbody>
{filteredHosts.map(host => (
<tr key={host.id}>
<td>{host.name}</td>
<td>{host.alias}</td>
<td className="pf-m-width-30">{host.name}</td>
<td className="pf-m-width-70">{host.alias}</td>
</tr>
))}
</tbody>

View File

@ -1,24 +1,67 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { ListView } from "patternfly-react";
import { isEmpty } from "lodash";
import { MainContainer } from "../containers";
import { getPlaybooks } from "./playbooksActions";
import Playbook from "./Playbook";
export class PlaybooksContainer extends Component {
state = {
isLoading: true
};
componentDidMount() {
this.props.getPlaybooks();
this.props
.getPlaybooks()
.catch(error => console.log(error))
.then(() => this.setState({ isLoading: false }));
}
render() {
const { playbooks } = this.props;
const { isLoading } = this.state;
return (
<MainContainer>
<ListView>
{playbooks.map(playbook => (
<Playbook key={playbook.id} playbook={playbook} />
))}
</ListView>
{isLoading && (
<div className="pf-l-bullseye">
<div className="pf-l-bullseye__item">
<div className="pf-c-card">
<div className="pf-c-card__body">loading</div>
</div>
</div>
</div>
)}
{!isLoading && isEmpty(playbooks) && (
<div className="pf-l-bullseye">
<div className="pf-l-bullseye__item">
<div className="pf-c-card">
<div className="pf-c-card__body">
<div className="pf-c-empty-state">
<i
className="fas fa-cubes pf-c-empty-state__icon"
aria-hidden="true"
/>
<h1 className="pf-c-title pf-m-lg">No playbooks</h1>
<p className="pf-c-empty-state__body">
There is no playbook available on this instance of Ara
</p>
<div className="pf-c-empty-state__action">
<a
href="https://ara.readthedocs.io/en/latest/"
target="_blank"
rel="noopener noreferrer"
>
See documentation
</a>
</div>
</div>
</div>
</div>
</div>
</div>
)}
{playbooks.map(playbook => (
<Playbook key={playbook.id} playbook={playbook} />
))}
</MainContainer>
);
}