If you plan to create your own Overview page for your OKD installation, this blog will help you get started.
The Overview page has a standardized layout consisting of three columns. The left column contains the Details and Inventory cards, the center column consists of the Status and Utilization cards, and the right column holds a single Activity card. You can of course use any other custom cards in your overview page and I will show you how in this blog.
To learn more about the design and high level concepts, you can check out this blog. This post focuses on the code.
Building Blocks of the Overview Page
Let’s take a look at components which you will be using to build your own Overview page.
const FooOverview = () => (
<Dashboard>
<DashboardGrid mainCards={[]} leftCards={[]} rightCards={[]} />
</Dashboard>
);
You probably noticed that components used for overview pages are prefixed with Dashboard. Overview pages were originally called dashboard pages. They were later renamed to avoid confusion with monitoring dashboards, but the code hasn’t yet been updated to reflect this change.
The root Dashboard component simply adds some CSS styling, such as a gray background and padding around the page. DashboardGrid places the card props passed into it (mainCards, leftCards, rightCards) into a grid layout, and ensures that the grid is responsive to screen size changes. To achieve this responsiveness, it measures the actual grid component width (not browser’s viewport) and will render the grid as a single column if the width is smaller than PatternFly’s defined LG breakpoint (992px). In the single column presentation, the main cards are shown first, followed by the left and right cards.
Now, we have an overall page layout, which means it’s time for the fun part: creating content! In our case, the content is a series of cards.
Generally speaking, every card looks like this:
const FooCard = () => (
<DashboardCard>
<DashboardCardHeader>
<DashboardCardTitle>Foo</DashboardCardTitle>
<DashboardCardLink to=”/foo-location”>Foo action</DashboardCardLink>
</DashboardCardHeader>
<DashboardCardBody>
{// your content here}
</DashboardCardBody>
</DashboardCard>
);
The root DashboardCard component is using the withFallback higher-order component (HOC), which makes sure that if something goes wrong with a particular card, the misbehaving card will be hidden instead of ruining the whole page.
At the top of each card is a DashboardCardHeader. It has a title and optionally, some actions. Nothing fancy here.
The DashboardCardBody is the place where you will want to show the interesting stuff. Usually, you will fetch some data from the backend (k8s, prometheus, or some other service running in the cluster) and somehow render them here.
Getting Data from the Backend
Since you will have multiple cards on your page, with all of them making some remote requests, it may happen that some cards will require the same data from the same service. If you just blindly create these requests from the cards, you will fetch the same data multiple times, for each card separately, and due to various resolution times the results won't be synchronized. Imagine a case where you, for whatever reason, want to show the data for the same prometheus metric in two cards. Every card will create its own request and every request is resolved after a different amount of time which leads to data not being the same and you also needlessly overfetched.
To avoid these issues, you will want to use the withDashboardResources HOC (we don't have a hook yet, but contributions are welcome!) which injects functions for fetching data from prometheus or any other in-cluster service and makes sure there are no duplicate requests. In order to fetch k8s resources, you will use useK8sWatchResource hook.
A Card that Actually Does Something
Let's put it all together and create a card that actually gets some data from the backend and renders it.
FooCard will fetch data from various sources - CPU metrics from prometheus, cluster health from the healthz endpoint, and a Pod resource from k8s.
import * as React from 'react';
import { getPrometheusQueryResponse } from '@console/internal/actions/dashboards';
import { withDashboardResources } from '@console/internal/components/dashboard/with-dashboard-resources';
import { humanizePercentage } from '@console/internal/components/utils';
import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook';
import { PodModel } from '@console/internal/models';
import { PodKind } from '@console/internal/module/k8s';
import DashboardCard from '@console/shared/src/components/dashboard/dashboard-card/DashboardCard';
import DashboardCardHeader from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardHeader';
import DashboardCardTitle from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardTitle';
import DashboardCardLink from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardLink';
import DashboardCardBody from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardBody';
import { fetchK8sHealth } from '@console/app/src/components/dashboards-page/status';
const healthzEndpoint = 'healthz';
const cpuMetric = 'cluster:cpu_usage_cores:sum';
const FooCard = withDashboardResources(({ watchPrometheus, stopWatchPrometheusQuery, prometheusResults, watchURL, stopWatchURL, urlResults }) => {
React.useEffect(() => {
watchPrometheus(cpuMetric);
watchURL(healthzEndpoint, fetchK8sHealth);
return () => {
stopWatchPrometheusQuery(cpuMetric);
stopWatchURL(healthzEndpoint);
};
});
const healthz = urlResults.getIn([healthzEndpoint, 'data']);
const healthzError = urlResults.getIn([healthzEndpoint, 'loadError']);
const [cpuResults, cpuMetricError] = getPrometheusQueryResponse(prometheusResults, cpuMetric);
const [pods, podsLoaded, podsLoadError] = useK8sWatchResource<PodKind[]>({
kind: PodModel.kind,
isList: true,
});
return (
<DashboardCard>
<DashboardCardHeader>
<DashboardCardTitle>Foo</DashboardCardTitle>
<DashboardCardLink to="/foo-location">Foo action</DashboardCardLink>
</DashboardCardHeader>
<DashboardCardBody>
<div>Cluster health: {healthzError ? 'error' : !healthz ? 'loading' : healthz}</div>
<div>CPU usage: {cpuMetricError ? 'error' : !cpuResults ? 'loading' : humanizePercentage(cpuResults.data.result[0].value[1]).string}</div>
<div>Pods count: {podsLoadError ? 'error' : !podsLoaded ? 'loading' : pods.length}</div>
</DashboardCardBody>
</DashboardCard>
);
});
export default FooCard;
The only thing that remains is to add the card to the FooOverview page.
import * as React from 'react';
import Dashboard from '@console/shared/src/components/dashboard/Dashboard';
import DashboardGrid from '@console/shared/src/components/dashboard/DashboardGrid';
import FooCard from './FooCard';
const FooOverview = () => (
<Dashboard>
<DashboardGrid mainCards={[{ Card: FooCard }]} leftCards={[{ Card: FooCard }]} rightCards={[{ Card: FooCard }]} />
</Dashboard>
);
export default FooOverview;
In the above snippet, I've actually added the FooCard to every column so you can verify (via the browser's network tab, for example) that the same fetch request is executed only once.
Yay! Our fancy FooOverview page.
Exposing the FooOverview Page
To expose your newly created FooOverview page, you will need to create a static plugin for the OpenShift Console. We have a nice readme that will help you get started in our github repository.
Every static plugin has a plugin.ts file that exports all plugin extensions. Console provides a few extension points that you can choose to expose FooOverview as a tab next to Cluster overview, as a page that will appear in main navigation, or as a standalone page.
To expose FooOverview as a page, your plugin.ts will look like this:
import { Plugin, RoutePage, HrefNavItem } from '@console/plugin-sdk';
type ConsumedExtensions = RoutePage | HrefNavItem;
const plugin: Plugin<ConsumedExtensions> = [
// expose as page & add item to navigation
{
type: 'Page/Route',
properties: {
exact: true,
path: 'foo-overview',
loader: () => import('./components/FooOverview' /* webpackChunkName: "foo-overview" */).then((m) => m.default),
},
},
{
type: 'NavItem/Href',
properties: {
id: 'fooNav',
componentProps: {
name: 'Foo Overview',
href: '/foo-overview',
},
},
},
];
export default plugin;
That's a Wrap
I hope this gives you a solid idea of how to create your own Overview page. The React components that you will be using can be found in the console-shared package.
Happy hacking!
Sull'autore
Altri risultati simili a questo
Ford's keyless strategy for managing 200+ Red Hat OpenShift clusters
F5 BIG-IP Virtual Edition is now validated for Red Hat OpenShift Virtualization
Can Kubernetes Help People Find Love? | Compiler
Scaling For Complexity With Container Adoption | Code Comments
Ricerca per canale
Automazione
Novità sull'automazione IT di tecnologie, team e ambienti
Intelligenza artificiale
Aggiornamenti sulle piattaforme che consentono alle aziende di eseguire carichi di lavoro IA ovunque
Hybrid cloud open source
Scopri come affrontare il futuro in modo più agile grazie al cloud ibrido
Sicurezza
Le ultime novità sulle nostre soluzioni per ridurre i rischi nelle tecnologie e negli ambienti
Edge computing
Aggiornamenti sulle piattaforme che semplificano l'operatività edge
Infrastruttura
Le ultime novità sulla piattaforma Linux aziendale leader a livello mondiale
Applicazioni
Approfondimenti sulle nostre soluzioni alle sfide applicative più difficili
Virtualizzazione
Il futuro della virtualizzazione negli ambienti aziendali per i carichi di lavoro on premise o nel cloud