Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add plugin infra for adding new dashboard tabs/cards #1742

Merged
merged 1 commit into from Jun 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 18 additions & 0 deletions frontend/packages/console-demo-plugin/src/dashboards/foo-card.tsx
@@ -0,0 +1,18 @@
import * as React from 'react';
import {
DashboardCard,
DashboardCardTitle,
DashboardCardBody,
DashboardCardHeader,
} from '@console/internal/components/dashboard/dashboard-card';

export const FooCard: React.FC<{}> = () => (
<DashboardCard>
<DashboardCardHeader>
<DashboardCardTitle>Foo Card</DashboardCardTitle>
</DashboardCardHeader>
<DashboardCardBody>
<div>foo content</div>
</DashboardCardBody>
</DashboardCard>
);
23 changes: 22 additions & 1 deletion frontend/packages/console-demo-plugin/src/plugin.tsx
Expand Up @@ -14,11 +14,14 @@ import {
RoutePage,
DashboardsOverviewHealthPrometheusSubsystem,
DashboardsOverviewHealthURLSubsystem,
DashboardsCard,
DashboardsTab,
} from '@console/plugin-sdk';

// TODO(vojtech): internal code needed by plugins should be moved to console-shared package
import { PodModel } from '@console/internal/models';
import { FLAGS } from '@console/internal/const';
import { GridPosition } from '@console/internal/components/dashboard/grid';

import { FooBarModel } from './models';
import { yamlTemplates } from './yaml-templates';
Expand All @@ -37,7 +40,9 @@ type ConsumedExtensions =
| YAMLTemplate
| RoutePage
| DashboardsOverviewHealthPrometheusSubsystem
| DashboardsOverviewHealthURLSubsystem<any>;
| DashboardsOverviewHealthURLSubsystem<any>
| DashboardsTab
| DashboardsCard;

const plugin: Plugin<ConsumedExtensions> = [
{
Expand Down Expand Up @@ -167,6 +172,22 @@ const plugin: Plugin<ConsumedExtensions> = [
render: () => <h1>Test Page</h1>,
},
},
{
type: 'Dashboards/Tab',
properties: {
id: 'foo-tab',
title: 'Foo',
},
},
{
type: 'Dashboards/Card',
properties: {
tab: 'foo-tab',
position: GridPosition.MAIN,
loader: () =>
import('./dashboards/foo-card' /* webpackChunkName: "demo-card" */).then((m) => m.FooCard),
},
},
];

export default plugin;
10 changes: 10 additions & 0 deletions frontend/packages/console-plugin-sdk/src/registry.ts
Expand Up @@ -12,6 +12,8 @@ import {
isYAMLTemplate,
isRoutePage,
isDashboardsOverviewHealthSubsystem,
isDashboardsCard,
isDashboardsTab,
} from './typings';

/**
Expand Down Expand Up @@ -59,4 +61,12 @@ export class ExtensionRegistry {
public getDashboardsOverviewHealthSubsystems() {
return this.extensions.filter(isDashboardsOverviewHealthSubsystem);
}

public getDashboardsTabs() {
return this.extensions.filter(isDashboardsTab);
}

public getDashboardsCards() {
return this.extensions.filter(isDashboardsCard);
}
}
36 changes: 36 additions & 0 deletions frontend/packages/console-plugin-sdk/src/typings/dashboards.ts
@@ -1,5 +1,8 @@
import { SubsystemHealth } from '@console/internal/components/dashboards-page/overview-dashboard/health-card';
import { GridPosition } from '@console/internal/components/dashboard/grid';

import { Extension } from './extension';
import { LazyLoader } from './types';

namespace ExtensionProperties {
interface DashboardsOverviewHealthSubsystem<R> {
Expand Down Expand Up @@ -31,6 +34,25 @@ namespace ExtensionProperties {
/** The Prometheus query */
query: string;
}

export interface DashboardsTab {
/** The tab's ID which will be used as part of href within dashboards page */
id: string;

/** The tab title */
title: string;
}

export interface DashboardsCard {
/** The tab's ID where this card should be rendered */
tab: string;

/** The card position in the tab */
position: GridPosition;

/** Loader for the corresponding dashboard card component. */
loader: LazyLoader<any>;
}
}

export interface DashboardsOverviewHealthURLSubsystem<R>
Expand Down Expand Up @@ -60,3 +82,17 @@ export const isDashboardsOverviewHealthSubsystem = (
e: Extension<any>,
): e is DashboardsOverviewHealthSubsystem =>
isDashboardsOverviewHealthURLSubsystem(e) || isDashboardsOverviewHealthPrometheusSubsystem(e);

export interface DashboardsTab extends Extension<ExtensionProperties.DashboardsTab> {
type: 'Dashboards/Tab';
}

export const isDashboardsTab = (e: Extension<any>): e is DashboardsTab =>
e.type === 'Dashboards/Tab';

export interface DashboardsCard extends Extension<ExtensionProperties.DashboardsCard> {
type: 'Dashboards/Card';
}

export const isDashboardsCard = (e: Extension<any>): e is DashboardsCard =>
e.type === 'Dashboards/Card';
4 changes: 1 addition & 3 deletions frontend/packages/console-plugin-sdk/src/typings/pages.ts
@@ -1,9 +1,7 @@
import * as React from 'react';
import { RouteProps, RouteComponentProps } from 'react-router-dom';
import { K8sKind, K8sResourceKindReference } from '@console/internal/module/k8s';
import { Extension } from './extension';

type LazyLoader<T extends {}> = () => Promise<React.ComponentType<Partial<T>>>;
import { LazyLoader } from './types';

namespace ExtensionProperties {
export interface ResourcePage<T> {
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/console-plugin-sdk/src/typings/types.ts
@@ -0,0 +1 @@
export type LazyLoader<T extends {}> = () => Promise<React.ComponentType<Partial<T>>>;
2 changes: 1 addition & 1 deletion frontend/public/components/app-contents.tsx
Expand Up @@ -118,7 +118,7 @@ const AppContents = withRouter(React.memo(() => (

<Route path={['/all-namespaces', '/ns/:ns']} component={RedirectComponent} />

<LazyRoute path="/dashboards" exact loader={() => import('./dashboards-page/dashboards' /* webpackChunkName: "dashboards" */).then(m => m.DashboardsPage)} />
<LazyRoute path="/dashboards" loader={() => import('./dashboards-page/dashboards' /* webpackChunkName: "dashboards" */).then(m => m.DashboardsPage)} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI @spadgett plugins can add new tabs into existing Dashboards page, with tab ID reflected into the URL.

<LazyRoute path="/cluster-status" exact loader={() => import('./cluster-overview' /* webpackChunkName: "cluster-overview" */).then(m => m.ClusterOverviewPage)} />
<Redirect from="/overview/all-namespaces" to="/cluster-status" />
<Redirect from="/overview/ns/:ns" to="/k8s/cluster/projects/:ns/workloads" />
Expand Down
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import classNames from 'classnames';
import { CardBody, CardBodyProps } from '@patternfly/react-core';

import { LoadingInline } from '../../utils';
import { LoadingInline } from '../../utils/status-box';

export const DashboardCardBody: React.FC<DashboardCardBodyProps> = React.memo(({ isLoading, classname, children, ...props }) => (
<CardBody className={classNames('co-dashboard-card__body', classname)} {...props}>
Expand Down
45 changes: 34 additions & 11 deletions frontend/public/components/dashboard/grid.tsx
@@ -1,35 +1,58 @@
import * as React from 'react';
import { Grid, GridItem } from '@patternfly/react-core';

import { useRefWidth } from '../utils';
import { useRefWidth } from '../utils/ref-width-hook';

export const MEDIA_QUERY_LG = 992;

export const DashboardGrid: React.FC<DashboardGridProps> = ({ mainCards, leftCards, rightCards }) => {
export enum GridPosition {
MAIN = 'MAIN',
LEFT = 'LEFT',
RIGHT = 'RIGHT',
}

const mapCardsToGrid = (cards: React.ComponentType<any>[], keyPrefix: string): React.ReactNode[] =>
cards.map((Card, index) => (
<GridItem key={`${keyPrefix}-${index}`} span={12}><Card /></GridItem>
));

export const DashboardGrid: React.FC<DashboardGridProps> = ({ mainCards, leftCards = [], rightCards = [] }) => {
const [containerRef, width] = useRefWidth();
const grid = width <= MEDIA_QUERY_LG ?
(
<Grid className="co-dashboard-grid">
<GridItem lg={12} md={12} sm={12}>
{mainCards}
<Grid className="co-dashboard-grid">
{mapCardsToGrid(mainCards, 'main')}
</Grid>
</GridItem>
<GridItem key="left" lg={12} md={12} sm={12}>
{leftCards}
<Grid className="co-dashboard-grid">
{mapCardsToGrid(leftCards, 'left')}
</Grid>
</GridItem>
<GridItem key="right" lg={12} md={12} sm={12}>
{rightCards}
<Grid className="co-dashboard-grid">
{mapCardsToGrid(rightCards, 'right')}
</Grid>
</GridItem>
</Grid>
) : (
<Grid className="co-dashboard-grid">
<GridItem key="left" lg={3} md={3} sm={3}>
{leftCards}
<Grid className="co-dashboard-grid">
{mapCardsToGrid(leftCards, 'left')}
</Grid>
</GridItem>
<GridItem lg={6} md={6} sm={6}>
{mainCards}
<Grid className="co-dashboard-grid">
{mapCardsToGrid(mainCards, 'main')}
</Grid>
</GridItem>
<GridItem key="right" lg={3} md={3} sm={3}>
{rightCards}
<Grid className="co-dashboard-grid">
{mapCardsToGrid(rightCards, 'right')}
</Grid>
</GridItem>
</Grid>
);
Expand All @@ -38,7 +61,7 @@ export const DashboardGrid: React.FC<DashboardGridProps> = ({ mainCards, leftCar
};

type DashboardGridProps = {
mainCards: React.ReactNode,
leftCards?: React.ReactNode,
rightCards?: React.ReactNode,
mainCards: React.ComponentType<any>[],
leftCards?: React.ComponentType<any>[],
rightCards?: React.ComponentType<any>[],
};
36 changes: 32 additions & 4 deletions frontend/public/components/dashboards-page/dashboards.tsx
Expand Up @@ -2,18 +2,46 @@ import * as React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { connect } from 'react-redux';

import * as plugins from '../../plugins';
import { OverviewDashboard } from './overview-dashboard/overview-dashboard';
import { HorizontalNav, PageHeading, LoadingBox } from '../utils';
import { HorizontalNav, PageHeading, LoadingBox, Page, AsyncComponent } from '../utils';
import { Dashboard } from '../dashboard/dashboard';
import { DashboardGrid, GridPosition } from '../dashboard/grid';
import { DashboardsCard } from '@console/plugin-sdk';

const tabs = [
const getCardsOnPosition = (cards: DashboardsCard[], position: GridPosition): React.ComponentType<any>[] =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To render lazy (async) components, you can use Promise-based AsyncComponent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using AsyncComponent now

cards.filter(c => c.properties.position === position).map(c => () => <AsyncComponent loader={c.properties.loader} />);

const getPluginTabPages = (): Page[] => {
const cards = plugins.registry.getDashboardsCards();
return plugins.registry.getDashboardsTabs().map(tab => {
const tabCards = cards.filter(c => c.properties.tab === tab.properties.id);
return {
href: tab.properties.id,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also write an extension check that ensures tab IDs are unique (and non-empty as well), I'll put it on my task list.

In general, every time we add new extension types, we should also consider writing the corresponding extension checks to guard against potential conflicts.

name: tab.properties.title,
component: () => (
<Dashboard>
<DashboardGrid
mainCards={getCardsOnPosition(tabCards, GridPosition.MAIN)}
leftCards={getCardsOnPosition(tabCards, GridPosition.LEFT)}
rightCards={getCardsOnPosition(tabCards, GridPosition.RIGHT)}
/>
</Dashboard>
),
};
});
};

const tabs: Page[] = [
{
href: '',
name: 'Overview',
component: OverviewDashboard,
},
...getPluginTabPages(),
];

const _DashboardsPage: React.FC<DashboardsPageProps> = ({ match, kindsInFlight }) => {
const DashboardsPage_: React.FC<DashboardsPageProps> = ({ match, kindsInFlight }) => {
return kindsInFlight
? <LoadingBox />
: (
Expand All @@ -28,7 +56,7 @@ const mapStateToProps = ({k8s}) => ({
kindsInFlight: k8s.getIn(['RESOURCES', 'inFlight']),
});

export const DashboardsPage = connect(mapStateToProps)(_DashboardsPage);
export const DashboardsPage = connect(mapStateToProps)(DashboardsPage_);

type DashboardsPageProps = RouteComponentProps & {
kindsInFlight: boolean;
Expand Down
Expand Up @@ -5,13 +5,8 @@ import { HealthCard } from './health-card';
import { DetailsCard } from './details-card';

export const OverviewDashboard: React.FC<{}> = () => {
const mainCards = [
<HealthCard key="health" />,
];

const leftCards = [
<DetailsCard key="details" />,
];
const mainCards = [HealthCard];
const leftCards = [DetailsCard];

return (
<Dashboard>
Expand Down
2 changes: 1 addition & 1 deletion frontend/public/components/utils/horizontal-nav.tsx
Expand Up @@ -26,7 +26,7 @@ class PodsComponent extends React.PureComponent<PodsComponentProps> {
}
}

type Page = {
export type Page = {
href: string;
name: string;
component?: React.ComponentType<any>;
Expand Down