This component is designed to show list of all entities in static inventory and allow them to navigate to detail of each entity. In such detail user will see some basic static data alongside custom application details.
This component is hot loaded via chrome, so any changes made to it will be automatically pulled by any application that uses it.
When using system detail do not use Route
set to exact
. It is designed as partial component and app details of inventory is loaded in same view so it will break if not used in non-exact mode.
You will need to register two routes (one for inventory table the other one for inventory detail) in this way
<Route exact path={'some/path/:itemId'} component={ItemPage} />
<Route path={'some/path/:itemId/:inventoryId'} component={InventoryPage} />
Where ItemPage
contains InventoryTable
and InventoryPage
has inventory detail. If back button is not working correctly you might want to consider adding root
to Inventory detail so it picks correct props and maps them to URL.
These examples count on using insight's registry, if you are using different make sure that you pass along correct one and don't use registryDecorator
To load such inventory via chrome just call window.insights.loadInventory
with dependencies and wait for it to load all data.
Expected dependencies is object with shape:
{
react: React, //Whole react
reactRouterDom: reactRouterDom //React router dom { withRouter, Switch, Route, Redirect, Link } are required
reactIcons: reactIcons //PF icons { TimesIcon, SyncIcon, hieldAltIcon, DollarSignIcon, WrenchIcon, CertificateIcon } are required, but they might be changed and more will be needed in future
reactCore: reactCore //PF react core items, best is to import * and pass whole reactCore
pfreact: pfreact // PF 3 react components - PaginationRow is currently used
}
To load inventory table use like this:
import React from 'react';
import * as reactRouterDom from 'react-router-dom';
import * as reactCore from '@patternfly/react-core';
import * as reactIcons from '@patternfly/react-icons';
import { PaginationRow } from 'patternfly-react';
import { registry as registryDecorator } from '@red-hat-insights/insights-frontend-components';
@registryDecorator()
class SomeCmp extends React.Component {
constructor(props) {
super(props);
this.state = {
InventoryCmp: () => <div>Loading...</div>
}
this.fetchInventory();
}
async fetchInventory() {
const { inventoryConnector, mergeWithEntities } = await insights.loadInventory({
react: React,
reactRouterDom,
reactCore,
reactIcons,
pfReact: { PaginationRow }
});
this.getRegistry().register({
...mergeWithEntities()
});
this.setState({
InventoryCmp: inventoryConnector().InventoryTable
})
}
render() {
const { InventoryCmp } = this.state;
return (
<InventoryCmp />
)
}
}
To show inventory detail use it like this:
import React from 'react';
import * as reactRouterDom from 'react-router-dom';
import * as reactCore from '@patternfly/react-core';
import * as reactIcons from '@patternfly/react-icons';
import { PaginationRow } from 'patternfly-react';
import { registry as registryDecorator } from '@red-hat-insights/insights-frontend-components';
@registryDecorator()
class SomeCmp extends React.Component {
constructor(props) {
super(props);
this.state = {
InventoryCmp: () => <div>Loading...</div>
}
this.fetchInventory();
}
async fetchInventory() {
const { inventoryConnector, mergeWithDetail } = await insights.loadInventory({
react: React,
reactRouterDom,
reactCore,
reactIcons,
pfReact: { PaginationRow }
});
this.getRegistry().register({
...mergeWithDetail()
});
this.setState({
InventoryCmp: inventoryConnector().InventoryDetail
})
}
render() {
const { InventoryCmp } = this.state;
return (
<InventoryCmp root={'some/url/:someId'}/>
)
}
}
You'll have to also register inventory reducers so the data are fetched correctly that is represent by calling
this.getRegistry().register
with mergeWithEntities
and mergeWithDetail
.
With inventory component loaded same as in previous step we have couple of variants of how to use this component
- Passing array of prefetched items from different data source - if you want to fetch inventory information from another source and help inventory to fetch facts for only those items you can pass either array of (string) IDs or objects with ID and additional props.
import React from 'react';
import * as reactRouterDom from 'react-router-dom';
import * as reactCore from '@patternfly/react-core';
import * as reactIcons from '@patternfly/react-icons';
import { PaginationRow } from 'patternfly-react';
import { registry as registryDecorator } from '@red-hat-insights/insights-frontend-components';
import { hostData } from './api';
@registryDecorator()
class SomeCmp extends React.Component {
constructor(props) {
super(props);
this.state = {
InventoryCmp: () => (<div>Loading...</div>)
}
this.fetchInventory();
}
async fetchInventory() {
// This can be data from server, redux data or just plain object.
const hostEntities = await hostData(); // from server
// const hostEntities = this.props.hostEntities // from redux
// const hostEntities = [{ id: '12-56-r-g', some: 'another', myData: 'something specific' }] // objects with ID
// cons hostEntities = [ '12-56-r-g' ] // array with IDs
const { inventoryConnector, mergeWithEntities } = await insights.loadInventory({
react: React,
reactRouterDom,
reactCore,
reactIcons,
pfReact: { PaginationRow }
});
this.getRegistry().register({
...mergeWithEntities()
});
this.setState({
InventoryCmp: inventoryConnector().InventoryTable,
hostEntities: hostEntities
})
}
render() {
const { InventoryCmp, hostEntities } = this.state;
return (
<InventoryCmp items={ hostEntities } />
)
}
}
- Calling some action when all entities are loaded - if you want to do something with fetched data, callback function will receive argument with shape
{data: data, stopBubble: () => boolean}
, where data is Promise with fetched data and stopBubble prevents from bubling to store.
import React from 'react';
import * as reactRouterDom from 'react-router-dom';
import * as reactCore from '@patternfly/react-core';
import * as reactIcons from '@patternfly/react-icons';
import { PaginationRow } from 'patternfly-react';
import { registry as registryDecorator } from '@red-hat-insights/insights-frontend-components';
@registryDecorator()
class SomeCmp extends React.Component {
//...
async fetchInventory() {
const { inventoryConnector, mergeWithEntities} = await insights.loadInventory({
react: React,
reactRouterDom,
reactCore,
reactIcons,
pfReact: { PaginationRow }
});
this.getRegistry().register({
...mergeWithEntities()
});
this.entitiesListener = addNewListener({
actionType: INVENTORY_ACTION_TYPES.LOAD_ENTITIES,
callback: this.callSomeFunction
});
this.setState({
InventoryCmp: inventoryConnector().InventoryTable
})
}
callSomeFunction({ data }) {
//Do something with data Promise
}
//...
}
- Calling some action when entity detail is loaded - if you want to get the ID of entity callback function will receive argument with promise with shape
{data: data, stopBubble: () => boolean}
, where data is Promise with ID of selected item and fetched data, stopBubble prevents from bubling to store.
import React from 'react';
import * as reactRouterDom from 'react-router-dom';
import * as reactCore from '@patternfly/react-core';
import * as reactIcons from '@patternfly/react-icons';
import { PaginationRow } from 'patternfly-react';
import { registry as registryDecorator } from '@red-hat-insights/insights-frontend-components';
@registryDecorator()
class SomeCmp extends React.Component {
//...
async fetchInventory() {
const { inventoryConnector, mergeWithDetail, INVENTORY_ACTION_TYPES } = await insights.loadInventory({
react: React,
reactRouterDom,
reactCore,
reactIcons,
pfReact: { PaginationRow }
});
this.getRegistry().register({
...mergeWithDetail()
});
this.entityListener = addNewListener({
actionType: INVENTORY_ACTION_TYPES.LOAD_ENTITY,
callback: this.callSomeOtherFunction
});
this.setState({
InventoryCmp: inventoryConnector().InventoryDetail
});
}
callSomeOtherFunction({ data }) {
//Do something with data Promise
}
//...
}
Since inventory table is regular table you can pass additional data to it to be rendered as tree table with collapsible rows and some specific data in such row.
To access store for dispatching events down to inventory you need to connect your component to redux using connect
function and set store into context props.
import React from 'react';
import * as reactRouterDom from 'react-router-dom';
import * as reactCore from '@patternfly/react-core';
import * as reactIcons from '@patternfly/react-icons';
import { PaginationRow } from 'patternfly-react';
import { registry as registryDecorator } from '@red-hat-insights/insights-frontend-components';
@registryDecorator()
class SomeCmp extends React.Component {
constructor(props, ctx) {
super(props, ctx);
this.state = {
InventoryCmp: () => <div>Loading...</div>,
items: [{
id: 'some-id',
children: [1], // please select some kind of more specific ID
active: false // to indicate that parent is not expanded
}, {
account: true, // since inventory table is checking if account is set
isOpen: false,
title: <div>Blaaa</div>, // What to show in expanded row
}]
}
this.onExpandClick = this.onExpandClick.bind(this);
this.fetchInventory();
}
async fetchInventory() {
const { inventoryConnector, mergeWithEntities } = await insights.loadInventory({
react: React,
reactRouterDom,
reactCore,
reactIcons,
pfReact: { PaginationRow }
});
this.getRegistry().register({
...mergeWithEntities()
});
const { InventoryTable, updateEntities } = inventoryConnector();
this.updateEntities = updateEntities;
this.setState({
InventoryCmp: InventoryTable
})
}
onExpandClick(_cell, row, key) {
const { items } = this.state;
items.find(item => item.id === key).active = !row.active;
// Not ideal, but for the sake of example it's fine
row.children.forEach(child => {
items.find(item => item.id === child.id).isOpen = !row.active;
});
this.setState({
items
});
this.context.store.dispatch(this.updateEntities(items));
}
render() {
const { InventoryCmp, items } = this.state;
return (
<InventoryCmp items={items} expandable onExpandClick={this.onExpandClick} />
)
}
}
SomeCmp.contextTypes = {
store: propTypes.object
};
export default connect(() => ({}))(SomeCmp);
If you want to include inventory table in smaller area you can pass attribute variant
it is the same as in PF4 table
import React from 'react';
import * as reactRouterDom from 'react-router-dom';
import * as reactCore from '@patternfly/react-core';
import * as reactIcons from '@patternfly/react-icons';
import { registry as registryDecorator } from '@red-hat-insights/insights-frontend-components';
@registryDecorator()
class SomeCmp extends React.Component {
//...
render() {
const { InventoryCmp, items } = this.state;
return (
<InventoryCmp variant={reactCore.TableVariant.compact} />
)
}
}
When user wants to update table, filter data (both trough filter select and textual filter) or you want to update visible items you can either update data in redux or use inventory ref and onRefreshData
function.
import React from 'react';
import * as reactRouterDom from 'react-router-dom';
import * as reactCore from '@patternfly/react-core';
import * as reactIcons from '@patternfly/react-icons';
import { PaginationRow } from 'patternfly-react';
import { registry as registryDecorator } from '@red-hat-insights/insights-frontend-components';
@registryDecorator()
class SomeCmp extends React.Component {
constructor(props, ctx) {
super(props, ctx);
this.inventory = React.createRef();
this.state = {
InventoryCmp: () => <div>Loading...</div>,
items: [] // some data
}
this.fetchInventory();
}
async fetchInventory() {
// ..
}
onRefresh(options) {
// Do something with this.state items and refresh data trough onRefreshData function
this.inventory.current && this.inventory.current.onRefreshData();
}
render() {
const { InventoryCmp, items } = this.state;
return (
<InventoryCmp items={items} ref={this.inventory} onRefresh={this.onRefresh} />
)
}
}
import React from 'react';
import * as reactRouterDom from 'react-router-dom';
import * as reactCore from '@patternfly/react-core';
import * as reactIcons from '@patternfly/react-icons';
import { PaginationRow } from 'patternfly-react';
import { registry as registryDecorator } from '@red-hat-insights/insights-frontend-components';
@registryDecorator()
class SomeCmp extends React.Component {
constructor(props, ctx) {
// ..
// initial data can be static or from server
this.state = {
// ..
InventoryCmp: () => <div>Loading...</div>,
page: 1,
perPage: 25
}
}
async fetchInventory() {
// ..
}
onRefresh = (options) => {
// This will be called when user clicks on pagination
// Do something with these information
fetch(`/some/endpoint?page=${options.page}&count=${options.perPage}`).then(data => {
data.json().then(({ items, meta }) => {
this.setState({
items,
total: meta.total,
page: meta.page,
perPage: meta.count
});
});
});
}
render() {
const { InventoryCmp, items, page, perPage, total } = this.state;
return (
<InventoryCmp items={items} onRefresh={this.onRefresh} page={page} perPage={perPage} total={total} />
)
}
}
Inventory has some basic filters over name, system type and OS version. However if you want to add your own filters you can do that by passing filters. Also if you want to show some extra content in header just pass children and inventory will show them next to filters and refresh.
You will be notified in onRefresh
function about filter changes.
import React from 'react';
import * as reactRouterDom from 'react-router-dom';
import * as reactCore from '@patternfly/react-core';
import * as reactIcons from '@patternfly/react-icons';
import { PaginationRow } from 'patternfly-react';
import { registry as registryDecorator } from '@red-hat-insights/insights-frontend-components';
@registryDecorator()
class SomeCmp extends React.Component {
constructor(props, ctx) {
super(props, ctx);
this.inventory = React.createRef();
this.state = {
InventoryCmp: () => <div>Loading...</div>
}
this.fetchInventory();
}
async fetchInventory() {
// ..
}
// options: { page, per_page, filters }
onRefresh(options) {
// do something with options
}
render() {
const { InventoryCmp } = this.state;
return (
<InventoryCmp ref={this.inventory} filters={[
{ title: 'Some filter', value: 'some-filter', items: [{ title: 'First', value: 'first' }] }
]} onRefresh={this.onRefresh}/>
)
}
}
If you want to change list of entities you should change them in redux store so the changes are reflected in entity table automatically.
Function mergeWithEntities
accepts redux reducer (function wwhich in simplest way can look like (state) => state
), just to be clear reducer function accepts state
and payload
as parameter and return either unchanged state or state which is changed
based on payload.
You'll want to split the reducers from your app logic in our example we have all reducers stored under src/store/reducers
, but your application can have them anywhere you want.
If you are going to change rows or entities in store, please use mergeArraysByKey
function which helps you with merging both current state and new payload together so you don't loose any of it.
src/store/reducers.js
- let's useapplyReducerHash
to demonstrate how to use such function
import { applyReducerHash } from '@red-hat-insights/insights-frontend-components/Utilities/ReducerRegistry';
import { ACTION_TYPES } from '../constants';
export const listReducer = applyReducerHash({
[ACTION_TYPES.GET_ENTITIES_FULFILLED]: (state, payload) => {
//do some logic with state
return {...state};
}
})
src/SomeComponent.js
- this is our example component
import React from 'react';
import * as reactRouterDom from 'react-router-dom';
import * as reactCore from '@patternfly/react-core';
import * as reactIcons from '@patternfly/react-icons';
import { PaginationRow } from 'patternfly-react';
import { listReducer } from './store/reducers';
import { registry as registryDecorator } from '@red-hat-insights/insights-frontend-components';
@registryDecorator()
class SomeCmp extends React.Component {
//...
async fetchInventory() {
const { inventoryConnector, mergeWithEntities, INVENTORY_ACTION_TYPES } = await insights.loadInventory({
react: React,
reactRouterDom,
reactCore,
reactIcons,
pfReact: { PaginationRow }
});
this.getRegistry().register({
...mergeWithEntities(listReducer)
});
this.entityListener = addNewListener({
actionType: INVENTORY_ACTION_TYPES.LOAD_ENTITY,
callback: this.props.fetchEntities
}
});
this.setState({
InventoryCmp: inventoryConnector().InventoryTable
})
}
//...
}
If you want to change detail of selected entity you should change it in redux store so the changes are reflected in entity detail automatically.
Function mergeWithDetail
accepts redux reducer (function wwhich in simplest way can look like (state) => state
), just to be clear reducer function accepts state
and action
as parameter and return either unchanged state or state which is changed
based on action's payload.
You'll want to split the reducers from your app logic in our example we have all reducers stored under src/store/reducers
, but your application can have them anywhere you want.
src/store/reducers.js
- let's switch and also inventory action to demonstrate such usage
import { ACTION_TYPES } from '../constants';
export function entityDetailReducer(INVENTORY_ACTIONS) {
return function(state, action) {
switch(action.type) {
case INVENTORY_ACTIONS.LOAD_ENTITY_FULFILLED: {
// do some thing with entity
return {...state}
}
case ACTION_TYPES.GET_ENTITY_FULFILLED: {
// do some thing with entity
return {...state}
}
}
}
}
src/SomeComponent.js
- this is our example component
import React from 'react';
import * as reactRouterDom from 'react-router-dom';
import * as reactCore from '@patternfly/react-core';
import * as reactIcons from '@patternfly/react-icons';
import { PaginationRow } from 'patternfly-react';
import { entityDetailReducer } from './store/reducers';
import { registry as registryDecorator } from '@red-hat-insights/insights-frontend-components';
@registryDecorator()
class SomeCmp extends React.Component {
//...
async fetchInventory() {
const {
inventoryConnector,
mergeWithDetail,
INVENTORY_ACTION_TYPES
} = await insights.loadInventory({
react: React,
reactRouterDom,
reactCore,
reactIcons,
pfReact: { PaginationRow }
});
this.getRegistry().register({
...mergeWithDetail(entityDetailReducer(INVENTORY_ACTION_TYPES))
});
this.entityListener = addNewListener({
actionType: INVENTORY_ACTION_TYPES.LOAD_ENTITY,
callback: this.props.fetchEntities
}
});
this.setState({
InventoryCmp: inventoryConnector().InventoryDetail
})
}
//...
}
If you want to display some information in entity detail you have option to do so, by adding details to store based on your application.
We'll use our reducers file and add some application to it
import { Overview } from '@red-hat-insights/insights-frontend-components';
function enableApplications(state) {
return {
...state,
loaded: true,
activeApps: [
{ title: 'Overview', name: 'overview', component: Overview },
{ title: 'Vulnerabilities', name: 'vulnerabilities' },
]
}
}
export function entitesDetailReducer(INVENTORY_ACTION_TYPES) {
return applyReducerHash(
{
[INVENTORY_ACTION_TYPES.LOAD_ENTITY_FULFILLED]: enableApplications,
},
defaultState
);
}
The most iportant part over here is the part of state activeApps
which requires array of objects with title
this will be displayed as tab, name
this is to correctly navigate in router and component
is optional with component which will be displayed as tab content.
Sometimes you might want to change size of each column to style the table properly. You can do this by adding props
to columns
import { ACTION_TYPES } from '../constants';
export function entityDetailReducer(INVENTORY_ACTIONS) {
return function(state, action) {
switch(action.type) {
case INVENTORY_ACTIONS.LOAD_ENTITY_FULFILLED: {
state.columns = [
{
key: 'some.compliacated.key',
title: 'Some title',
props: {
width: 40
}
}, {
key: 'simple',
title: 'Another',
props: {
width: 10
}
}
]
return {...state}
}
}
}
}
Please write these application specific details in some place where others can benefit from your implementation.
These actions are fired from inventory component. If action is marked with *
it means it's async actions and so it has _FULFILLED
, _PENDING
and _REJECTED
variants.
LOAD_ENTITIES
- to trigger fetching entities from specific endpointLOAD_ENTITY
- when detail data are being received from serverSELECT_ENTITY
- if user clicks on checkbox in entity listCHANGE_SORT
- when user changes sortFILTER_ENTITIES
- when user wants to filter entitiesAPPLICATION_SELECTED
- fired after user clicks on application detail
As mentioned before the async loader will load two functions mergeWithEntities
and mergeWithDetail
both have access to your store and they will create specific keys in store. Please do not change the data directly, since that can break the inventory component.
Let's assume that the store looks like
{
someKey: {}
}
Given store will look like
{
someKey: {},
entities: {
columns: Array({key: String, title: String, composed: Array(String)})
loaded: Boolean
rows: Array({}),
entities: Array({})
}
}
- columns - each entry has
key
,title
andcomposed
. Composed is array of paths for multiple values,key
is path to display value. - loaded - if data are loaded to indicate loading.
- rows - contains actual data. Based on columns the data will be queried by
key
orcomposed
and shown in table. If no data are foundunknown
will be shown for such column. - entities - is just copy of rows which are filtered and sorted.
{
someKey: {},
entityDetails: {
activeApp: {appName: String},
activeApps: Array({title: String, name: String, component: React.Component}),
entity: {},
tags: {key: Array(String)}
}
}
- activeApp - name of active app.
- activeApps - array with visible apps,
title
will be tab title,name
will be used for react router andcomponent
as tab content. - entity - actual entity data.
- Hostname:
display_name
- UUID:
facts.host_system_id
- System:
facts.release
- Last Check-in: TODO
- Registered: TODO
- Hostname:
- tags - tags data
To add your own data to inventory you can do that by passing some specific data to inventory endpoint, please do this only on test server.
window.insights.chrome.auth.getUser().then(
data => fetch('/r/insights/platform/inventory/api/hosts', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"display_name": "Test computer",
"account": data.account_number,
"insights_id": "abc-123",
facts: [
{
facts: {
hostname: `server01.redhat.com`,
machine_id: `c1497de-0ec7-43bb-a8a6-35cabd59e0bf`,
release: 'Red Hat Enterprise Linux Server release 7.5 (Maipo)',
},
namespace: 'inventory'
}
]
})
}));