Project

General

Profile

« Previous | Next » 

Revision 5e7343ec

Added by Maria Agaphontzev 12 months ago

Fixes #36439 - remove storybook and update ui docs

View differences:

.dockerignore
package-lock.json
npm-debug.log
.vscode
.yo-rc.json
out*
.byebug_history
.gitignore
.eslintignore
webpack/simple_named_modules.js
*.test.js
*fixture*
*.stories.js
*const*
.github/workflows/storybook_deploy_main.yml
name: Deploy Main Storybook
on:
push:
branches:
- develop
paths:
- 'webpack/**'
permissions:
contents: read
jobs:
deploy_main_storybook:
runs-on: ubuntu-latest
if: github.repository == 'theforeman/foreman'
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
- name: Run npm install
run: npm install
- name: Build Storybook
run: npm run build-storybook -- --output-dir=storybooks/main
- name: Deploy Storybook to surge.sh
env:
SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }}
run: |
npm install --save-dev surge
npx surge --project storybooks/main --domain ${{ secrets.SURGE_DOMAIN }}
id: surge_deploy
.gitignore
package-lock.json
npm-debug.log
.vscode
.yo-rc.json
.vendor/
.solargraph.yml
.nvmrc
README.md
[![Code Climate](https://codeclimate.com/github/theforeman/foreman/badges/gpa.svg)](https://codeclimate.com/github/theforeman/foreman)
[![Coverage Status](https://coveralls.io/repos/github/theforeman/foreman/badge.svg?branch=develop)](https://coveralls.io/github/theforeman/foreman?branch=develop)
[![Support IRC channel](https://kiwiirc.com/buttons/irc.libera.chat/theforeman.png)](https://kiwiirc.com/client/irc.libera.chat/?#theforeman)
[![Storybook](https://raw.githubusercontent.com/storybooks/brand/master/badge/badge-storybook.svg)](https://foreman-storybook.surge.sh)
[Foreman](https://theforeman.org) is a free open source project that gives you the power to easily **automate repetitive tasks**, quickly **deploy applications**, and proactively **manage your servers life cycle**, on-premises or in the cloud.
developer_docs/adding-dependencies.asciidoc
[[adding-js-dependencies]]
= NPM dependencies
:toc: right
:toclevels: 5
## Using/Adding/updating NPM dependencies
Foreman manage npm dependencies with a seperate project called `@theforeman/vendor` which responsible to deliver 3rd-party modules to foreman and its plugins.
Foreman and its plugins consumes `@theforeman/vendor` project from `npm` in development and from `rpm` in production.
`@theforeman/vendor` lives inside a monorepo together with other foreman javascript tools in a project called https://github.com/theforeman/foreman-js[foreman-js]
https://github.com/theforeman/foreman-js/tree/master/packages/vendor[Read more about @theforeman/vendor]
## Consuming `foreman-js` projects from source (locally)
Clone, install, build and link the `foreman-js` project to foreman:
```sh
git clone git@github.com:theforeman/foreman-js.git
cd foreman-js
npm install
npm run build
npm run link -- --location ../foreman
```
To read to full documentation, please go to `foreman-js` contribution guide:
https://github.com/theforeman/foreman-js/blob/master/CONTRIBUTING.md
**NOTICE: You must remove `./node_modules/@theforeman` before running `npm install`.
If not, it will destroy your `foreman-js` copy.**
developer_docs/adding-new-components.asciidoc
[[adding-new-react-components]]
# Adding new React components
## Component Storage
Components are stored in the webpack/assets/javascripts/react_app/components/ directory. Each component should be placed in its own subfolder that follows the structure outlined below:
```
─ components/<COMPONENT_NAME>/
├─ <COMPONENT_NAME>.js ┈ react component
├─ <COMPONENT_NAME>.scss ┈ styles if needed
├─ <COMPONENT_NAME>.fixtures.js ┈ constants for testing, initial state, etc.
├─ <COMPONENT_NAME>.test.js ┈ test file for the component
├─ components/ ┈ folder for nested components if needed
```
React coponent files are limited to 100 lines of code. If you need to write more code, consider splitting the component into multiple components and/or wrapping the react component with an index file that will preform any general logic, api calls.
If you are creating a component that uses legacy Redux actions and reducers, the structure should be as follows:
```
─ components/<COMPONENT_NAME>/
├─ <COMPONENT_NAME>.js ┈ pure react component
├─ <COMPONENT_NAME>.scss ┈ styles if needed
├─ <COMPONENT_NAME>Actions.js ┈ redux actions
├─ <COMPONENT_NAME>Reducer.js ┈ redux reducer
├─ <COMPONENT_NAME>Selectors.js ┈ reselect selectors
├─ <COMPONENT_NAME>Constants.js ┈ constants such as action types
├─ <COMPONENT_NAME>.fixtures.js ┈ constants for testing, initial state, etc.
├─ components/ ┈ folder for nested components if needed
├─ __tests__/ ┈ folder for tests
╰─ index.js ┈ redux connected file
```
## Testing
### Testing components
Please use React-Testing-Library for tests. It's a library that helps you test your components in a way that resembles how they are used by the end user. It's a good idea to read the https://testing-library.com/docs/guiding-principles[guiding principles] of the library.
### Running the tests
All tests can be executed with `npm test`.
If you want to run only a single test run `npm test \-- <path to test file>`. For example `npm test \-- BreadcrumbBar.test.js`.
Linter (code style checking) can be executed with `npm run lint`. You can run it with parameter `--fix` to let it automatically fix the discovered issues. You need to pass the parameter to eslint, so run the command like this `npm run lint \-- --fix`.
## Making it available from ERB
If you want your component to be available for mounting into ERB templates, it must be added to [the component registry](https://github.com/theforeman/foreman/blob/develop/webpack/assets/javascripts/react_app/components/componentRegistry.js#L60-L71).
Then, you can mount it with the `react_component` helper:
```ruby
react_component(component_name, props)
```
**Example:**
```erb
<div id="my-cool-power-status">
<%= react_component('PowerStatus', id: host.id, url: power_host_path(host.id), errorText: 'N/A') %>
</div>
```
will render the following HTML:
```html
<div id="my-cool-power-status">
<foreman-react-component
name="PowerStatus"
data-props="<%= {
id: host.id,
url: power_host_path(host.id),
errorText: 'N/A',
}.to_json %>"
></foreman-react-component>
</div>
```
(Note that the React component is rendered as a [web component](https://developer.mozilla.org/en-US/docs/Web/Web_Components).)
### Changing the props from legacy JS
We allow changing the root component props from the legacy JS.
Be aware, that this will re-render the component.
This feature should only be used for limited use cases:
1. A legacy JS library/component needs to talk to a React component, AND
1. The component is simple enough that it wouldn't otherwise make sense to store its data in the Redux store.
We will use a method `reactProps` that our web component exposes and get the current props.
Then we will change this props and use `reactProps=` setter that will trigger the rerender.
```js
var myCoolPowerElement = document.getElementById("my-cool-power-status").getElementsByTagName('foreman-react-component')[0];
var newProps = myCoolPowerElement.reactProps;
newProps.errorText = 'MyNewErrorText';
myCoolPowerElement.reactProps = newProps;
```
*Note* that you can also directly set the data: `element.dataset.props = JSON.stringify({new: 'prop'});`
or even the attribute: `element.setAttribute('data-props', JSON.stringify({new: 'prop'}));`
Both of these need a JSON string as a new value to work.
## Before you start writing a new component
It's worth checking [PatternFly](https://www.patternfly.org) to make sure such component doesn't exist yet. Also consider if your component is universal enough to be used in other projects. In such case it might make sense to add it to PatternFly instead.
developer_docs/api-middleware-usage.asciidoc
[[api-middleware-intro]]
= API Middleware Usage
:toc: right
:toclevels: 5
# API Middleware
Instead of each component handling API calls in the same way we have the API Middleware that will handle it.
## How to use API in a Component using useAPI hook
The API middleware is abstracted by the `useAPI` custom hook.
```js
import { useAPI } from '../common/hooks/API';
import KEY_FOR_API from './consts';
import { successCallback, errorCallback } from './helper';
const MyComponent = () => {
const options = {
handleSuccess: successCallback,
handleError: error => (error.response.status === 401 ? logoutUser() : null),
successToast: response => 'This text will be shown as a toast after a success call',
errorToast: response => 'This text will be shown as a toast when error occurs',
};
const {
response: { results },
status, // The current status of the API call
key, // Generated key for storing in redux's store
setAPIOptions, // Function to update the options and make a new api call
} = useAPI('get', '/api/hosts', options);
return (
<ul>
<button onClick={() => setAPIOptions({ ...options, params: { search: 'os=fedora' } })}>
Fedora
</button>
<button onClick={() => setAPIOptions({ ...options, params: { search: 'os=debian' } })}>
Debian
</button>
{results.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
};
```
## How to use the API middleware
The api middleware is a redux middleware that handles API calls in the application.
It is recommended to use the `useAPI` hook instead of using the middleware directly.
```js
/** MyComponent.js*/
import React from 'react';
import { STATUS } from '../../constants';
const MyComponent = ({ status, error, items }) => {
if (status === STATUS.PENDING) {
return <div>Loading...</div>;
}
if (status === STATUS.ERROR) {
return <div>Error: {error.message}</div>;
}
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.title} {item.action}
</li>
))}
</ul>
);
};
```
```js
/** index.js*/
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { useSelector, useDispatch } from 'react-redux';
import {
selectItems,
selectStatus,
selectError,
} from './MyComponentsSelectors.js';
import { getData } from './MyComponentActions';
import MyComponent from './MyComponent';
const ConnectedMyComponent = ({ path }) => {
const items = useSelector(selectItems);
const status = useSelector(selectStatus);
const error = useSelector(selectError);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getData(path));
}, [path]);
return <MyComponent items={items} status={status} error={error} />;
};
ConnectedMyComponent.propTypes = {
path: PropTypes.string.isRequired,
};
export default ConnectedMyComponent;
```
### Access the API store
We provided you the `selectAPIByKey` in '/APISelectors.js' which will return the key substate,
there are also `selectAPIStatus`, `selectAPIPayload`, `selectAPIResponse`, `selectAPIError` and `selectAPIErrorMessage`.
```js
/** MyComponentSelectors.js*/
import { MY_SPECIAL_KEY } from './MyComponentConstants';
import {
selectAPIStatus,
selectAPIError,
selectAPIResponse,
} from '../../redux/API/APISelectors';
// use the same key that you've used in the API action.
export const selectItems = state =>
selectAPIResponse(state, MY_SPECIAL_KEY).items || [];
export const selectStatus = state => selectAPIStatus(state, MY_SPECIAL_KEY);
export const selectError = state => selectAPIError(state, MY_SPECIAL_KEY);
```
Then there will be called 2 actions: **MY_SPECIAL_KEY_REQUEST** and **MY_SPECIAL_KEY_SUCCESS/ MY_SPECIAL_KEY_FAILURE**:
**MY_SPECIAL_KEY_REQUEST** will have the payload only
**MY_SPECIAL_KEY_SUCCESS** will have the payload and the return data from the API call.
**MY_SPECIAL_KEY_FAILURE** will have the payload and the return error from the API call.
In the **payload** field you should send any headers and params for the GET request, and any other data you want for the action.
The actions types can be changed with the optional **actionTypes** parameter:
```js
/** MyComponentActions.js*/
export const getData = url => ({
type: API_OPERATIONS.GET,
payload: {
key: MY_SPECIAL_KEY, // you will need to re-use this key in order to access the right API reducer later.
url,
payload: {
page: 2,
per_page: 10,
},
actionTypes: {
REQUEST: 'CUSTOM_REQUEST',
}
},
});
```
The example API returns a JSON object like this:
```json
{
"items": [
{ "id": 319, "title": "setting", "action": "update" },
{ "id": 150, "title": "bookmark", "action": "create" }
]
}
```
Once the action is triggered, the API middleware will manage the request
and update the store with the request status:
the store on API pending:
```js
{
...
API: {
MY_SPECIAL_KEY: { // The key that was provided in the API action.
response: null,
status: "PENDING",
payload: {},
}
}
}
```
the store on API success:
```js
{
...
API: {
MY_SPECIAL_KEY: {
response: {
items: [
{id: 319, title: "setting", action: "update"},
{id: 150, title: "bookmark", action: "create"}
],
},
status: "RESOLVED",
payload: {},
}
}
}
```
the store on API failure:
```js
{
...
API: {
MY_SPECIAL_KEY: {
response: "Network Error",
status: "ERROR",
paylod: {},
}
}
}
```
developer_docs/client-routing.asciidoc
[[client-routing]]
# Client Routing
:toc: right
:toclevels: 5
Foreman uses `react-router` for rendering react pages without full page reload.
## Core
In order to add a new route in foreman core, please follow these steps:
1. Create a folder under `/react_app/routes` directory
2. Create an index file and import the wanted component:
+
[source,js]
----
import React from 'react';
import Page1 from './page1';
import { PAGE1_URL } from './constants';
export default {
path: PAGE1_URL,
render: props => <Page1 {...props} />,
};
----
+
3. import that index in `routes.js` file:
+
[source,js]
----
// Other routes...
import Page1 from './Page1';
import Page2 from './Page2'
export const routes = [Page1, Page2];
----
+
4. Add a route that point to this page in `routes.rb` :
```ruby
match 'page1' => 'react#index', :via => :get
```
## Plugins
You can use `react-router` in your plugin as well with no boilerplate.
### Register global routes file
In the plugin registeration please add:
``` ruby
Foreman::Plugin.register :"<plugin-name>" do
register_global_js_file 'routes'
# some code
end
```
This tells foreman core to load a `routes_index.js` file.
### Register routes
Create `routes_index.js` file under `webpack` directory:
```js
import { registerRoutes } from 'foremanReact/routes/RoutingService';
import Routes from './Routes';
registerRoutes('<plugin-name>', Routes);
```
### Creating the routes
Create a `routes.js`:
```js
import React from 'react';
import IndexPage from './IndexPage';
import ShowPage from './ShowPage';
const ForemanPluginRoutes = [
{
path: '/<plugin>/index',
exact: true,
render: props => <IndexPage {...props} />,
},
{
path: '/<plugin>/:id',
render: props => <ShowPage {...props} />,
},
];
export default ForemanPluginRoutes;
```
### Adding the routes in routes.rb
The plugin needs to identify that a page should be loaded via react router.
Doing so by updating the plugin's `routes.rb`:
```ruby
match '/plugin_page' => '/react#index', :via => [:get]
```
This will use the react template in foreman core.
### Adding Styles
CSS importing should work out of the box.
Please use `local` scope when overriding core's styles.
```css
/* styles.css */
:local(.overrideClass) {
margin-top: 50px;
color: red;
}
```
```js
// index.js
import React from 'react';
import pluginStyles from 'styles.css';
return <Component className={pluginStyles.overrideClass} />
```
developer_docs/fetching-data-with-graphql.asciidoc
[[js-fetching-data-with-graphql]]
# Fetching data with GraphQL
Foreman uses Apollo client to talk to GraphQL endpoint. All the components within react routes have access to the Apollo client as routes are wrapped with ApolloProvider (see ReactApp component for details). Queries can be executed simply by using a hook:
```js
import { useQuery, gql } from '@apollo/client';
// query any resource for which there is a defined GraphQL type in Foreman, for example Architecture
const myQuery = gql`
query {
architectures {
edges {
nodes {
name
id
}
}
}
}
`
const MyComponent = props => {
const { loading, error, data } = useQuery(myQuery);
//...
}
```
Even though it is possible to use `gql` template literal tag to define queries, it is not recommended. Much better approach is to place your query into a separate file:
```js
// myQuery.gql
query {
architectures {
edges {
nodes {
name
id
}
}
}
}
// MyComponent.js
import myQuery from './myQuery.gql';
```
If your component is mounted directly into erb view, you need to wrap it with `ApolloProvider`:
```js
import { ApolloProvider } from '@apollo/client';
import apolloClient from 'foremanReact/Root/apollo';
import MyComponent from './MyComponent';
const MyComponentWithApollo = props => {
return (
<ApolloProvider client={apolloClient}>
<MyComponent {...props } />
</ApolloProvider>
)
}
export default MyComponentWithApollo;
```
developer_docs/foreman-context.asciidoc
[[foreman-context]]
# ForemanContext
:toc: right
:toclevels: 5
ForemanContext is an implementation of the new `React Context` API.
Global metadata (such as version, pagination, user, taxonomies, theme, foreman's settings, etc...) can be shared over all react nodes
without any redux integration nor API request.
### How to apply it
`Foreman Context` comes with every react component by default, like redux's store.
### Reading values from the context
Like selectors, you can consume context values by custom hooks:
```js
// ....
import {
useForemanSettings,
useForemanVersion,
useForemanOrganization,
useForemanLocation,
useForemanUser,
useForemanDocUrl,
} from '../../Root/Context/ForemanContext';
const { perPage } = useForemanSettings();
const foremanVersion = useForemanVersion();
const { id, title } = useForemanLocation();
const { id, title } = useForemanOrganization();
const { id, login, firstname, lastname, admin } = useForemanUser();
```
## How to add a value to the context
Keys and values are set in the `app_metadata` method of `/application_helper.rb`.
The `app_metadata` object is passed down and becomes ForemanContext.
If you add a new value to `app_metadata`, it will also be useful to add a new custom hook in `webpack/assets/javascripts/react_app/ReactApp/Context/ForemanContext.js`
so that the value can be easily consumed on the front end.
For example, Adding a new hook:
```js
// Context/ForemanContext.js
export const useForemanFeature = () => useForemanContext().feature;
```
developer_docs/getting-started.asciidoc
[[js-getting-started]]
# Getting started with frontend development
:toc: right
:toclevels: 5
## Development setup
Following steps are required to setup a webpack development environment:
1. **Settings**
There are 2 relevant settings in `config/settings.yml`. At least `webpack_dev_server` should be set to true:
+
[source,yaml]
----
# Use the webpack development server?
# Should be set to true if you want to conveniently develop webpack-processed code.
# Make sure to run `rake webpack:compile` if disabled.
:webpack_dev_server: true
# If you run Foreman in development behind some proxy or use HTTPS you need
# to enable HTTPS for webpack dev server too, otherwise you'd get mixed content
# errors in your browser
:webpack_dev_server_https: true
----
+
2. **Dependencies**
Make sure you have all npm dependencies up to date:
`npm install`
Alternatively you can run the install command with option `--no-optional` which skips packages that aren't required and can save you some space.
3. **Running webpack**
There are several ways of executing webpack:
- using [foreman runner](https://github.com/ddollar/foreman): `foreman start` (starts both rails and webpack server)
- using `script/foreman-start-dev` (starts rails and webpack server)
- executing rails and webpack processes "manually"
```bash
./node_modules/.bin/webpack-dev-server \
--config config/webpack.config.js \
--port 3808 \
--public $(hostname):3808
```
4. **Additional config**
Both `foreman start` and `foreman-start-dev` support `WEBPACK_OPTS` environment variable for passing additional options. This is handy for example when you have development setup with Katello and want to use correct certificates.
An example of such setup:
+
[source,bash]
----
./node_modules/.bin/webpack-dev-server \
--config config/webpack.config.js \
--port 3808 \
--public $(hostname):3808 \
--https \
--key /etc/pki/katello/private/katello-apache.key \
--cert /etc/pki/katello/certs/katello-apache.crt \
--cacert /etc/pki/katello/certs/katello-default-ca.crt \
--watch-poll 1000 # only use for NFS https://community.theforeman.org/t/webpack-watch-over-nfs/10922
----
+
Additionally you can set `NOTIFICATIONS_POLLING` variable to extend the notification polling interval that is 10s by default and can clutter the console.
+
[source,bash]
----
NOTIFICATIONS_POLLING=${polling_interval_in_ms}
----
+
Webpack stats can be changed by `WEBPACK_STATS`. Default value is `minimal`.
+
[source,bash]
----
WEBPACK_STATS=${verbose}
----
## Directory structure
The webpack processed code is placed in the following folder structure:
```
─ webpack/ ┈ all webpack processed code
╰─ assets/javascripts/ ┈ es6 code for erb pages, some still contain jQuery
╰─ react_app/ ┈ react components and related code
```
More detailed description of a folder structure for components is in chapter https://github.com/theforeman/foreman/blob/develop/developer_docs/adding-new-components.asciidoc[Adding new component].
There are still obsolete `redux` folders at some places. They used to be a place for files containing Redux actions and reducers before a standardized folder structure was introduced. We're migrating away from them. Please don't put additional code there.
## Useful tools
There are some useful extensions that can be used on top of the standard browser's developer tools. Their Firefox mutations are available too.
- [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi)
- [Redux DevTools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd)
developer_docs/hoc.asciidoc
[[react-hoc]]
# Legacy Higher Order Components
*Foremans `withRenderHandler` is not maintained anymore, and should not be used in new code.*
A higher-order component (HOC) is an advanced technique in React for reusing component logic. We have created several HOCs which makes it easier to write, read and maintain new code.
## `withRenderHandler({ Component, LoadingComponent, ErrorComponent, EmptyComponent})`
This HOC help us get rid of conditional rendering inside a component.
It renders the right component based on its state
the following root Component props are required
`{ isLoading, hasData, hasError }`
If the default Error and Empty Components are used
the following props are also required:
`{ message: { type, text }}`
Here we can see all the optional states and their chosen view.
|===
| isLoading | hasData | hasError | VIEW | Comments
| 💚TRUE| FALSE🔴 | FALSE🔴 | **Loading** | Initial Loading **OR** after Message
| 🔴FALSE | TRUE💚 | FALSE🔴 | **Component** | Show Data
| 💚TRUE | TRUE💚 | FALSE🔴 | **Component** | Outdated data exists, should not show loading to avoid flickering so we still show the component with the outdated data until the new data arrives
| 🔴FALSE | FALSE🔴 | FALSE🔴 | **Empty** | query returned 0 results, this is not an error, and we display a message
| 🔴FALSE | FALSE🔴 | TRUE💚 | **Error** | query failed, and we display a message
|===
### Example
```js
// before
const Statistics = ({ isLoading, hasError, hasData, statisticsMeta }) => {
if (isLoading && !hasData) return <StatisticsLoadingPage />;
if (hasError) return <StatisticsErrorPage />;
if (hasData) return <StatisticsChartsList data={statisticsMeta} />;
return StatisticsEmptyPage;
};
export default Statistics;
```
```js
// after
const Statistics = ({ statisticsMeta }) => (
<StatisticsChartsList data={statisticsMeta} />
);
export default withRenderHandler({
Component: Statistics,
// Empty, Error, and Loading has a default Component if not received
});
```
## `callOnMount(callback(props))`
This HOC runs a function on the initial mount of the component using useEffect
### Example
```js
// export connected component
export default compose(
connect(mapStateToProps, mapDispatchToProps),
callOnMount(({ getStatisticsMeta }) => getStatisticsMeta())
)(StatisticsPage);
```
## `callOnPopState(callback(props))`
This HOC runs a function onPopState if search query has changed, assuming the Page is wrapped withRouter
### Example
```js
// export connected component
export default compose(
connect(mapStateToProps, mapDispatchToProps),
callOnPopState(({ getStatisticsOnPop }) => getStatisticsOnPop())
)(StatisticsPage);
```
developer_docs/interval-middleware.asciidoc
# Interval Middleware
:toc: right
:toclevels: 5
This middleware will run an action for every given interval miliseconds,
and will manage all running intervals in the Redux store so we could clear the interval later.
## How to start
To start an interval, you should add `interval` and a unique `key` into you action, for example:
```js
// MyComponent/MyComponentActions.js
...
dispatch({
type: API_OPERATIONS.GET,
key: MY_SPECIAL_KEY, // use a special key which will be used later to clear the interval.
interval: 3000 // or 'true' which will use the default interval milisec.
url,
payload: data,
});
```
it could be also a simple action such as:
```js
const sendMail = () => ({
type: SEND_MAIL,
key: MY_SPECIAL_MAIL_KEY,
interval: 10000000,
payload: data,
});
```
## How to stop
There are several ways to stop the interval:
In case you are using the API middleware actions,
the `stopInterval` for your `key` will be passed as the second param in your `handleSuccess`/ `handleError` callbacks.
Another option is to use the `stopInterval` Action from IntervalMiddlware directly.
`stopInterval` is defined in `webpack/assets/javascripts/react_app/redux/middlewares/IntervalMiddleware`
or `foremanReact/redux/middlewares/IntervalMiddleware` for plugins
```js
// MyComponent/MyComponentActions.js
....
import { stopInterval } from '../../redux/middlewares/IntervalMiddleware';
...
// use the same key you used to start the interval.
export const stopAPIInterval = () => stopInterval(key);
```
Then it will be available in your component:
```js
// MyComponent/MyComponent.js
componentWillUnmount() {
// use the same key you used to start the interval.
this.props.stopAPIInterval(key)
}
```
Another option is to add the action to redux `connect` in the index file through `mapDispatchToProps`:
```js
// MyComponent/index.js
import { stopInterval } from "../../redux/middlewares/IntervalMiddleware";
// import { stopInterval } from "foremanReact/redux/middlewares/IntervalMiddleware"; in plugins
...
const mapDispatchToProps = dispatch => bindActionCreators( { ...actions, stopInterval }, dispatch)
```
Then it will be available in your component:
```js
// MyComponent/MyComponent.js
cleanUpPolling = () => {
const { stopInterval } = this.props;
// use the same key you used to start the interval.
stopInterval(key);
};
```
You could also call it with `useDispatch` hook:
```js
// MyComponent/MyComponent.js
import { useDispatch } from 'react-redux'
import { stopInterval } from "../../redux/middlewares/IntervalMiddleware";
// import { stopInterval } from "foremanReact/redux/middlewares/IntervalMiddleware"; in plugins
...
cleanUpPolling = () => {
const dispatch = useDispatch()
// use the same key you used to start the interval.
dispatch(stopInterval(key))
}
```
Components that use Hooks such as `useEffect`, should call for cleanup same as in `componentWillUnmount`:
```js
// MyComponent/MyComponent.js
...
useEffect(() => {
... // start polling
return cleanUpPolling;
}, []);
```
You can change the `DEFAULT_INTERVAL` from the console by setting DEFAULT_INTERVAL=5000.
developer_docs/legacy-js.asciidoc
[[legacy-js]]
# Legacy JS
:toc: right
:toclevels: 5
Foreman's legacy javascript is based on ruby on rails assets pipeline and located in `assets/javascripts`
New features are most welcome on Webpack, but you can still get data directly from Redux's state and observe the state within the legacy JS.
- ⚠️ **Warning**: Do not update react content with jquery or other DOM manipulation
- ⚠️ **Warning**: Do not alter the state manually, only via actions
- ⚠️ **Warning**: Avoid changing the actual DOM with react
image::./images/foreman-frontend-infra.png["Foreman Frontend Infrastructure"]
## Access webpack's javascript
In order to access new js logic in old js files, we created a global object -`tfm`, which contains a set of functions and located in `/webpack/assets/javascripts/bundle.js`
Please use this object instead of using the `window` object directly.
## How to invoke an action
Import the desired action in `/foreman_actions` file
dispatch the action in a legacy js file:
```js
tfm.store.dispatch('actionName', arg1, arg2);
```
## Observing the store
With `observeStore` you can listen for changes in the store:
```javascript
tfm.store.observeStore('notifications.items', tfm.doSomething);
```
`observeStore` accepts two parameters:
1. the part of the store to be observed.
2. a function to run when a change is detected.
```javascript
const doSomething = (items, unsubscribe) => {
if (items.length) {
doSomething();
} else {
unsubscribe();
}
};
```
This function has two parameters as well:
1. the observed store.
2. an unsubscribe function to stop the observation (optional).
developer_docs/plugins.asciidoc
[[js-plugins]]
# Plugins
:toc: right
:toclevels: 5
## Using components from Foreman core
There are three ways components provided by Foreman core can be re-used:
[[mounting-components-into-erb]]
### 1. Mounting components into ERB
No special setup is required and you can re-use React components that are available in `componentRegistry` even when your plugin doesn't use Webpack.
Components can be mounted into ERB using the `react_component` helper:
```ruby
react_component(component_name, props)
```
**Example:**
```erb
<%= react_component('PowerStatus', id: host.id, url: power_host_path(host.id)) %>
```
will render the following HTML:
```html
<foreman-react-component
name="PowerStatus"
data-props="<%= {
id: host.id,
url: power_host_path(host.id)
}.to_json %>"
></foreman-react-component>
```
(Note that the React component is rendered as a [web component](https://developer.mozilla.org/en-US/docs/Web/Web_Components).)
The list of available components is [here](https://github.com/theforeman/foreman/blob/develop/webpack/assets/javascripts/react_app/components/componentRegistry.js).
### 2. Re-using core code in Webpack
If your plugin uses Webpack, you can import and the core functionality from `foremanReact`.
**Example:**
```js
// import helpers from foremanReact:
import { noop } from 'foremanReact/common/helpers';
// import components from foremanReact:
import { MessageBox } from 'foremanReact/components/common/MessageBox';
```
## Using Webpack in plugins
There are 3 conditions that a plugin has to fulfill to share the Webpack infrastructure from Foreman core:
- A `./webpack/` folder containing all the Webpack-processed code
- A `package.json` file with dependencies
- A defined main entry point in `package.json` or just have `./webpack/index.js`
The Webpack config is shared with core, so there's no need for custom configuration.
Once all the above is set up, then the script `npm run install` executed from root of the core's git checkout installs dependencies for plugins too.
Also `npm run lint` and `npm run test` behaves similarly.
### Entry points
The Webpack config respects the main entry point defined in `package.json`. On top of that it loads all files matching `./webpack/*index.js`. That allows plugins to define multiple independent entry points. This can be useful in special use-cases. For example when you need to mix some parts of Webpack-processed code into pages that use asset pipeline only.
### Troubleshooting
You can make sure Webpack knows about your plugin by executing script `plugin_webpack_directories.rb` that prints json-formatted info about all recognized plugins.
```bash
> ./script/plugin_webpack_directories.rb | json_reformat
{
"entries": {
"katello": "/home/vagrant/foreman/katello/webpack/index.js"
},
"paths": [
"/home/vagrant/foreman/katello/webpack"
],
"plugins": {
"katello": {
"root": "/home/vagrant/foreman/katello",
"entry": "/home/vagrant/foreman/katello/webpack/index.js"
}
}
}
```
## How to extend core functionaly
You can use https://github.com/theforeman/foreman/blob/develop/developer_docs/slot-and-fill.asciidoc[Slot&Fill] to extend react components from core.
developer_docs/slot-and-fill.asciidoc
[[slot-and-fill]]
# Slot And Fill
:toc: right
:toclevels: 5
Slot & Fill allows plugins to extend foreman core functionality in the UI.
## Current Slots List
You can find current slots by search which React component use the `<Slot>` component.
You can find .erb pages that use `<Slot>` component by searching for `slot` helper.
## Components
### Slot
`<Slot>` is an opinionated extension point which responsible of rendering its fills
a Slot can support multiple fills, rendering by weight order by the `multi` prop
if there is no such prop, it will render the max weighted fill.
### Fill
a fill is the filled object of a slot
#### Component fill
a fill that contains a child component, which rendered by a dedicated slot
_core_
```js
<Slot id="slot-id">
a default child // can be empty
</Slot>
```
_plugin A_
```js
<Fill slotId="slot-id" id="some-id" weight={100}>
<div> some text </div>
</Fill>
```
_plugin B_
```js
<Fill slotId="slot-id" id="some-id" weight={200}>
<div> some text </div>
</Fill>
```
Plugin B has a fill with a higher weight, therefore it will be rendered in a dedicated slot.
#### Component fill - remdering multiple fills
If a slot has a `multi` prop, and it has multiple fills, it will render these fills by weight order
_core_
```js
<Slot multi id="slot-id" />
```
_plugin A_
```js
<Fill slotId="slot-id" id="some-id" weight={100}>
<div> some text </div>
</Fill>
```
_plugin B_
```js
<Fill slotId="slot-id" id="some-id" weight={200}>
<div> some text </div>
</Fill>
```
Plugin B's fill will be render first
#### Props fill
a fill that contains an `overrideProps` object
those props are given to the slot's children
_core_
```js
const TextWrapper = ({ text }) => <div>{text}</div>;
<Slot id="slot-id">
<TextWrapper text="some default text" />
</Slot>;
```
_plugin A_
```js
<Fill
slotId="slot-id"
id="some-id"
weight={200}
overrideProps={{ text: '[Plugin A] this text given by a prop' }}
/>
```
_plugin B_
```js
<Fill
slotId="slot-id"
id="some-id"
weight={100}
overrideProps={{ text: '[Plugin B] this text given by a prop' }}
/>
```
In this case, the slot doesn't have `multi` prop, therefore it will take the max weight which is Plugin A's fill.
#### Global fill
This fill is available on each and every core page
A plugin should use global fills when it doesn't have access to that area (i.e adding a component in the about page)
If a plugin wants to extend foreman's layout, or whether it has a partial in a core page via facets, a regular fill is enough.
create a `fills_index.js` file under webpack directory:
```js
import React from 'react';
import SomeComponent from './components/SomeComponent';
import { addGlobalFill } from 'foremanReact/components/common/Fill/GlobalFill';
// if some of the components are connected to redux, registering a reducer is required
registerReducer('[plugin]-extends', extendReducer);
addGlobalFill('slotId', 'fillId', <SomeComponent key="some-key" />, 300);
// instead of a component, you can also override props
addGlobalFill(
'slotId',
'fillId',
{ someProp: 'this is an override prop' },
300
);
```
Register `fills` global file in `plugin.rb` file:
```ruby
Foreman::Plugin.register :<plugin> do
# content
register_global_file 'fills'
end
```
Finally, add a slot in foreman core:
```ruby
<%= slot('slotId', true) %>
```
developer_docs/state-management.asciidoc
[[state-management]]
# State Management in Foreman & Plugins
:toc: right
:toclevels: 5
Where do I put data so that it's accessible by the React components that need it?
Currently Foreman has a few places for storing data:
* Component state
* Redux store
* ForemanContext
* Global `window` object
* Other React Context
* localStorage and sessionStorage
* Routing
The following sections will outline
* The architecture around how we use each method
* Current opinions on best practices.
## Local component state
This includes anything that uses `useState` or `useReducer` hooks. Or, in class components, `this.state`. See the React docs for more info.
This is the best place for any data that is _used only locally by the component and its children_: for example
* input value for a controlled UI component
* current form values for form elements
* Other UI element states
Local state is also good for data that affects the component's direct children and can be easily passed down as props.
## Redux store
The Redux store is used to store data that needs to be shared across many components, especially ones that don't share a common ancestry. Another case is when one component can trigger an action that could impact the state of another component that isn't directly in its ancestry.
Currently API responses are also stored in Redux by the API middleware.
Don't default to using Redux when deciding where to put new data. Keep other options in mind as well, and see if local state or context would make more sense.
On the other hand, state management is not the only use for Redux. It can also be useful for logging what happened and in what order, or triggering actions such as API calls.
## ForemanContext
Data that doesn't often change but can impact many components across the application is stored in a special React Context object called ForemanContext. For more info on how to use it, see https://github.com/theforeman/foreman/blob/develop/developer_docs/foreman-context.asciidoc[Foreman Context].
This data includes:
* Current taxonomy (organization, location)
* Current user
* Pagination settings
* Foreman version
* UI Settings
* Documantation url
## Global `window` object
Because Foreman and plugins use both modern and legacy JS frameworks, Foreman has a unique way of storing some data on the global `window` object. (See `webpack/assets/javascripts/bundle.js`.) This `tfm` object includes
* A https://github.com/theforeman/foreman/blob/develop/webpack/assets/javascripts/react_app/components/componentRegistry.js[`componentRegistry`] that makes React components accessible for mounting on Rails ERB pages. For more info, see https://github.com/theforeman/foreman/blob/develop/developer_docs/plugins.asciidoc#mounting-components-into-erb[Mounting components into ERB]
* A method for https://github.com/theforeman/foreman/blob/develop/developer_docs/legacy-js.asciidoc[legacy JS] frameworks to access Foreman's Redux store
* Several other utilities and helpers written in jQuery and vanilla JavaScript
Should be used only if legacy code needs access to such data or methods. Avoid using it if possible.
Other than the `tfm` object, it is not advisable to store anything on the global `window` object.
## Other React Contexts
Consider the `ForemanModal` component, which is a composite React component which needs to pass data down to its subcomponents. However, this data is not used or needed by anything other than ForemanModal internals. For these specific use cases, it makes sense to use a React Context instance that is not connected to `ForemanContext`.
React Router also uses React Context under the hood.
## localStorage and sessionStorage
The browser's `sessionStorage` is currently used to store some user preferences; see `LayoutSessionStorage.js`, `HostSessionStorage.js`, and `NotificationDrawerSessionStorage.js`.
`sessionStorage` should only be used for volatile, temporary state-- i.e. things that won't impact the user if they get lost.
Currently the browser `localStorage` is not used by Foreman. It is recommended to use the Redux store instead.
package-exclude.json
"@sheerun/mutationobserver-shim",
"@theforeman/env",
"@theforeman/eslint-plugin-foreman",
"@theforeman/stories",
"@theforeman/test",
"@theforeman/vendor-dev",
"@theforeman/find-foreman",
......
],
"EXCLUDE_NPM_PREFIXES": [
"@babel/eslint-",
"@storybook/",
"@testing-library/",
"enzyme",
"eslint",
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff