Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions frontend/packages/console-demo-plugin/src/dashboards/inventory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from 'react';
import { Icon } from 'patternfly-react';

import { StatusGroupMapper } from '@console/internal/components/dashboard/inventory-card/inventory-item';

export const getRouteStatusGroups: StatusGroupMapper = (resources) => ({
'demo-inventory-group': {
statusIDs: ['Accepted'],
count: resources.length,
filterType: 'route-status',
},
});

export const DemoGroupIcon: React.FC<{}> = () => (
<Icon
type="fa"
name="address-book"
className="co-inventory-card__status-icon co-inventory-card__status-icon--warn"
/>
);
28 changes: 26 additions & 2 deletions frontend/packages/console-demo-plugin/src/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import {
DashboardsCard,
DashboardsTab,
DashboardsOverviewCapacityQuery,
DashboardsOverviewInventoryItem,
DashboardsInventoryItemGroup,
} 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 { PodModel, RouteModel } from '@console/internal/models';
import { FLAGS } from '@console/internal/const';
import { GridPosition } from '@console/internal/components/dashboard/grid';
import { CapacityQuery } from '@console/internal/components/dashboards-page/overview-dashboard/capacity-query-types';
Expand All @@ -29,6 +31,7 @@ import { FooBarModel } from './models';
import { yamlTemplates } from './yaml-templates';
import TestIcon from './components/test-icon';
import { getFooHealthState, getBarHealthState } from './dashboards/health';
import { getRouteStatusGroups, DemoGroupIcon } from './dashboards/inventory';

type ConsumedExtensions =
| ModelDefinition
Expand All @@ -45,7 +48,9 @@ type ConsumedExtensions =
| DashboardsOverviewHealthURLSubsystem<any>
| DashboardsTab
| DashboardsCard
| DashboardsOverviewCapacityQuery;
| DashboardsOverviewCapacityQuery
| DashboardsOverviewInventoryItem
| DashboardsInventoryItemGroup;

const plugin: Plugin<ConsumedExtensions> = [
{
Expand Down Expand Up @@ -205,6 +210,25 @@ const plugin: Plugin<ConsumedExtensions> = [
query: 'barQuery',
},
},
{
type: 'Dashboards/Overview/Inventory/Item',
properties: {
resource: {
isList: true,
kind: RouteModel.kind,
prop: 'routes',
},
model: RouteModel,
mapper: getRouteStatusGroups,
},
},
{
type: 'Dashboards/Inventory/Item/Group',
properties: {
id: 'demo-inventory-group',
icon: <DemoGroupIcon />,
},
},
];

export default plugin;
10 changes: 10 additions & 0 deletions frontend/packages/console-plugin-sdk/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
isDashboardsCard,
isDashboardsTab,
isDashboardsOverviewCapacityQuery,
isDashboardsOverviewInventoryItem,
isDashboardsInventoryItemGroup,
} from './typings';

/**
Expand Down Expand Up @@ -74,4 +76,12 @@ export class ExtensionRegistry {
public getDashboardsOverviewCapacityQueries() {
return this.extensions.filter(isDashboardsOverviewCapacityQuery);
}

public getDashboardsOverviewInventoryItems() {
return this.extensions.filter(isDashboardsOverviewInventoryItem);
}

public getDashboardsInventoryItemGroups() {
return this.extensions.filter(isDashboardsInventoryItemGroup);
}
}
46 changes: 46 additions & 0 deletions frontend/packages/console-plugin-sdk/src/typings/dashboards.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { SubsystemHealth } from '@console/internal/components/dashboards-page/overview-dashboard/health-card';
import { GridPosition } from '@console/internal/components/dashboard/grid';
import { CapacityQuery } from '@console/internal/components/dashboards-page/overview-dashboard/capacity-query-types';
import { FirehoseResource } from '@console/internal/components/utils';
import { K8sKind } from '@console/internal/module/k8s';
import { StatusGroupMapper } from '@console/internal/components/dashboard/inventory-card/inventory-item';

import { Extension } from './extension';
import { LazyLoader } from './types';
Expand Down Expand Up @@ -62,6 +65,31 @@ namespace ExtensionProperties {
/** The Prometheus query */
query: string;
}

export interface DashboardsOverviewInventoryItem {
/** Resource which will be fetched and grouped by `mapper` function. */
resource: FirehoseResource;
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems that resource.kind value can fall back to model.kind unless the developer wants to be explicit about it.

More generally, we should try to keep the extension properties (API surface) as small and lean as possible.

With these typings added to public/declarations.d.ts (source):

type OptionalExceptFor<T, K extends keyof T> = Partial<T> & Pick<T, K>;
type RequiredExceptFor<T, K extends keyof T> = Pick<T, Diff<keyof T, K>> & Partial<T>;

We can now modify resource.kind to be optional:

resource: RequiredExceptFor<FirehoseResource, 'kind'>;

which makes the extension properties declaration shorter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

RequiredExceptFor definition isn't correct as we are using different Diff definition than expected. Sticking to FirehoseResource for now

Copy link
Contributor

Choose a reason for hiding this comment

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

OK, we can improve this later on.


/** Additional resources which will be fetched and passed to `mapper` function. */
additionalResources?: FirehoseResource[];

/** The model for `resource` which will be fetched. The model is used for getting model's label or abbr. */
model: K8sKind;

/** Defines whether model's label or abbr should be used when rendering the item. Defaults to false (label). */
useAbbr?: boolean;

/** Function which will map various statuses to groups. */
mapper: StatusGroupMapper;
}

export interface DashboardsInventoryItemGroup {
/** The ID of status group. */
id: string;

/** React component representing status group icon. */
icon: React.ReactElement;
}
}

export interface DashboardsOverviewHealthURLSubsystem<R>
Expand Down Expand Up @@ -114,3 +142,21 @@ export interface DashboardsOverviewCapacityQuery
export const isDashboardsOverviewCapacityQuery = (
e: Extension<any>,
): e is DashboardsOverviewCapacityQuery => e.type === 'Dashboards/Overview/Capacity/Query';

export interface DashboardsOverviewInventoryItem
extends Extension<ExtensionProperties.DashboardsOverviewInventoryItem> {
type: 'Dashboards/Overview/Inventory/Item';
}

export const isDashboardsOverviewInventoryItem = (
e: Extension<any>,
): e is DashboardsOverviewInventoryItem => e.type === 'Dashboards/Overview/Inventory/Item';

export interface DashboardsInventoryItemGroup
extends Extension<ExtensionProperties.DashboardsInventoryItemGroup> {
type: 'Dashboards/Inventory/Item/Group';
}

export const isDashboardsInventoryItemGroup = (
e: Extension<any>,
): e is DashboardsInventoryItemGroup => e.type === 'Dashboards/Inventory/Item/Group';
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
.co-inventory-card__item {
align-items: center;
border-bottom: 1px solid $pf-color-black-300;
display: flex;
font-size: 1rem;
justify-content: space-between;
padding: 1em 0;
}

.co-inventory-card__item-status {
align-items: center;
display: flex;
flex-wrap: wrap;

:last-child {
margin-right: 0;
}
}

.co-inventory-card__item-title {
margin-right: 0.5em;
}

.co-inventory-card__status {
align-items: center;
display: flex;
flex-shrink: 0;
margin-right: 0.5em;
}

.co-inventory-card__status-icon {
font-size: 1.125rem;
}

.co-inventory-card__status-icon--error {
color: $pf-color-red-100;
}

.co-inventory-card__status-icon--question {
color: $pf-color-black-300;
}

.co-inventory-card__status-icon--ok {
color: $pf-color-light-green-400;
}

.co-inventory-card__status-icon--progress {
color: $pf-color-black-600;
}

.co-inventory-card__status-icon--warn {
color: $pf-color-gold-400;
}

.co-inventory-card__status-text {
margin-left: 0.25em;
}
121 changes: 121 additions & 0 deletions frontend/public/components/dashboard/inventory-card/inventory-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import * as React from 'react';
import { Icon } from 'patternfly-react';
import { Link } from 'react-router-dom';

import * as plugins from '../../../plugins';
import { LoadingInline } from '../../utils';
import { K8sResourceKind, K8sKind } from '../../../module/k8s';
import { InventoryStatusGroup } from './status-group';

const getPluginStatusGroupIcons = () => {
const pluginGroups = {};
plugins.registry.getDashboardsInventoryItemGroups().forEach(group => {
pluginGroups[group.properties.id] = group.properties.icon;
});
return pluginGroups;
};

const statusGroupIcons = {
[InventoryStatusGroup.OK]: (
<Icon
type="fa"
name="check-circle"
className="co-inventory-card__status-icon co-inventory-card__status-icon--ok"
/>
),
[InventoryStatusGroup.WARN]: (
<Icon
type="fa"
name="exclamation-triangle"
className="co-inventory-card__status-icon co-inventory-card__status-icon--warn"
/>
),
[InventoryStatusGroup.ERROR]: (
<Icon
type="fa"
name="exclamation-circle"
className="co-inventory-card__status-icon co-inventory-card__status-icon--error"
/>
),
[InventoryStatusGroup.PROGRESS]: (
<Icon
type="pf"
name="in-progress"
className="co-inventory-card__status-icon co-inventory-card__status-icon--progress"
/>
),
[InventoryStatusGroup.NOT_MAPPED]: (
<Icon
type="fa"
name="question-circle"
className="co-inventory-card__status-icon co-inventory-card__status-icon--question"
/>
),
...getPluginStatusGroupIcons(),
};

const Status: React.FC<StatusProps> = React.memo(({ groupID, count, statusIDs, kind, namespace, filterType}) => {
const statusItems = encodeURIComponent(statusIDs.join(','));
const namespacePath = namespace ? `ns/${namespace}` : 'all-namespaces';
const to = filterType && statusItems.length > 0 ? `/k8s/${namespacePath}/${kind.plural}?rowFilter-${filterType}=${statusItems}` : `/k8s/${namespacePath}/${kind.plural}`;
const groupIcon = statusGroupIcons[groupID] || statusGroupIcons[InventoryStatusGroup.NOT_MAPPED];
return (
<div className="co-inventory-card__status">
<Link to={to} style={{textDecoration: 'none'}}>
{groupIcon}
<span className="co-inventory-card__status-text">{count}</span>
</Link>
</div>
);
});

export const InventoryItem: React.FC<InventoryItemProps> = React.memo(({ kind, useAbbr, resources, additionalResources, isLoading, mapper, namespace }) => {
const groups = mapper(resources, additionalResources);
let title: string;
if (useAbbr) {
title = resources.length !== 1 ? `${kind.abbr}s` : kind.abbr;
} else {
title = resources.length !== 1 ? kind.labelPlural : kind.label;
}
return (
<div className="co-inventory-card__item">
<div className="co-inventory-card__item-title">{isLoading ? title : `${resources.length} ${title}`}</div>
{isLoading ? <LoadingInline /> : (
<div className="co-inventory-card__item-status">
{Object.keys(groups).filter(key => groups[key].count > 0).map((key, index) => (
<Status
key={index}
kind={kind}
namespace={namespace}
groupID={key}
count={groups[key].count}
statusIDs={groups[key].statusIDs}
filterType={groups[key].filterType}
/>
))}
</div>
)}
</div>
);
});

export type StatusGroupMapper = (resources: K8sResourceKind[], additionalResources?: {[key: string]: K8sResourceKind[]}) => {[key in InventoryStatusGroup | string]: {filterType?: string, statusIDs: string[], count: number}};

type StatusProps = {
groupID: InventoryStatusGroup | string;
count: number;
statusIDs: string[];
kind: K8sKind;
namespace?: string;
filterType?: string;
}

type InventoryItemProps = {
resources: K8sResourceKind[];
additionalResources?: {[key: string]: K8sResourceKind[]};
mapper: StatusGroupMapper;
kind: K8sKind;
useAbbr?: boolean;
isLoading: boolean;
namespace?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum InventoryStatusGroup {
OK = 'OK',
WARN = 'WARN',
ERROR = 'ERROR',
PROGRESS = 'PROGRESS',
NOT_MAPPED = 'NOT_MAPPED',
}
Loading