From dc217201ea870ee27aca3adc9f90c984b30ba0c7 Mon Sep 17 00:00:00 2001 From: Rastislav Wagner Date: Fri, 21 Jun 2019 13:01:28 +0200 Subject: [PATCH] Add Inventory card --- .../src/dashboards/inventory.tsx | 20 +++ .../console-demo-plugin/src/plugin.tsx | 28 +++- .../console-plugin-sdk/src/registry.ts | 10 ++ .../src/typings/dashboards.ts | 46 +++++++ .../inventory-card/inventory-card.scss | 57 +++++++++ .../inventory-card/inventory-item.tsx | 121 ++++++++++++++++++ .../dashboard/inventory-card/status-group.ts | 7 + .../dashboard/inventory-card/utils.ts | 50 ++++++++ .../components/dashboards-page/dashboards.tsx | 7 +- .../overview-dashboard/inventory-card.tsx | 117 +++++++++++++++++ .../overview-dashboard/overview-dashboard.tsx | 3 +- .../components/persistent-volume-claim.jsx | 2 +- frontend/public/style.scss | 1 + 13 files changed, 463 insertions(+), 6 deletions(-) create mode 100644 frontend/packages/console-demo-plugin/src/dashboards/inventory.tsx create mode 100644 frontend/public/components/dashboard/inventory-card/inventory-card.scss create mode 100644 frontend/public/components/dashboard/inventory-card/inventory-item.tsx create mode 100644 frontend/public/components/dashboard/inventory-card/status-group.ts create mode 100644 frontend/public/components/dashboard/inventory-card/utils.ts create mode 100644 frontend/public/components/dashboards-page/overview-dashboard/inventory-card.tsx diff --git a/frontend/packages/console-demo-plugin/src/dashboards/inventory.tsx b/frontend/packages/console-demo-plugin/src/dashboards/inventory.tsx new file mode 100644 index 00000000000..523ffb49cbd --- /dev/null +++ b/frontend/packages/console-demo-plugin/src/dashboards/inventory.tsx @@ -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<{}> = () => ( + +); diff --git a/frontend/packages/console-demo-plugin/src/plugin.tsx b/frontend/packages/console-demo-plugin/src/plugin.tsx index 98f8c10c806..3897131f77d 100644 --- a/frontend/packages/console-demo-plugin/src/plugin.tsx +++ b/frontend/packages/console-demo-plugin/src/plugin.tsx @@ -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'; @@ -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 @@ -45,7 +48,9 @@ type ConsumedExtensions = | DashboardsOverviewHealthURLSubsystem | DashboardsTab | DashboardsCard - | DashboardsOverviewCapacityQuery; + | DashboardsOverviewCapacityQuery + | DashboardsOverviewInventoryItem + | DashboardsInventoryItemGroup; const plugin: Plugin = [ { @@ -205,6 +210,25 @@ const plugin: Plugin = [ 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: , + }, + }, ]; export default plugin; diff --git a/frontend/packages/console-plugin-sdk/src/registry.ts b/frontend/packages/console-plugin-sdk/src/registry.ts index cd416cef81e..a1cea848afc 100644 --- a/frontend/packages/console-plugin-sdk/src/registry.ts +++ b/frontend/packages/console-plugin-sdk/src/registry.ts @@ -15,6 +15,8 @@ import { isDashboardsCard, isDashboardsTab, isDashboardsOverviewCapacityQuery, + isDashboardsOverviewInventoryItem, + isDashboardsInventoryItemGroup, } from './typings'; /** @@ -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); + } } diff --git a/frontend/packages/console-plugin-sdk/src/typings/dashboards.ts b/frontend/packages/console-plugin-sdk/src/typings/dashboards.ts index 72163ab3bce..3f8f0ba074f 100644 --- a/frontend/packages/console-plugin-sdk/src/typings/dashboards.ts +++ b/frontend/packages/console-plugin-sdk/src/typings/dashboards.ts @@ -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'; @@ -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; + + /** 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 @@ -114,3 +142,21 @@ export interface DashboardsOverviewCapacityQuery export const isDashboardsOverviewCapacityQuery = ( e: Extension, ): e is DashboardsOverviewCapacityQuery => e.type === 'Dashboards/Overview/Capacity/Query'; + +export interface DashboardsOverviewInventoryItem + extends Extension { + type: 'Dashboards/Overview/Inventory/Item'; +} + +export const isDashboardsOverviewInventoryItem = ( + e: Extension, +): e is DashboardsOverviewInventoryItem => e.type === 'Dashboards/Overview/Inventory/Item'; + +export interface DashboardsInventoryItemGroup + extends Extension { + type: 'Dashboards/Inventory/Item/Group'; +} + +export const isDashboardsInventoryItemGroup = ( + e: Extension, +): e is DashboardsInventoryItemGroup => e.type === 'Dashboards/Inventory/Item/Group'; diff --git a/frontend/public/components/dashboard/inventory-card/inventory-card.scss b/frontend/public/components/dashboard/inventory-card/inventory-card.scss new file mode 100644 index 00000000000..60a0f22cf39 --- /dev/null +++ b/frontend/public/components/dashboard/inventory-card/inventory-card.scss @@ -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; +} diff --git a/frontend/public/components/dashboard/inventory-card/inventory-item.tsx b/frontend/public/components/dashboard/inventory-card/inventory-item.tsx new file mode 100644 index 00000000000..0efd57d07b8 --- /dev/null +++ b/frontend/public/components/dashboard/inventory-card/inventory-item.tsx @@ -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]: ( + + ), + [InventoryStatusGroup.WARN]: ( + + ), + [InventoryStatusGroup.ERROR]: ( + + ), + [InventoryStatusGroup.PROGRESS]: ( + + ), + [InventoryStatusGroup.NOT_MAPPED]: ( + + ), + ...getPluginStatusGroupIcons(), +}; + +const Status: React.FC = 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 ( +
+ + {groupIcon} + {count} + +
+ ); +}); + +export const InventoryItem: React.FC = 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 ( +
+
{isLoading ? title : `${resources.length} ${title}`}
+ {isLoading ? : ( +
+ {Object.keys(groups).filter(key => groups[key].count > 0).map((key, index) => ( + + ))} +
+ )} +
+ ); +}); + +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; +} diff --git a/frontend/public/components/dashboard/inventory-card/status-group.ts b/frontend/public/components/dashboard/inventory-card/status-group.ts new file mode 100644 index 00000000000..2df76afe628 --- /dev/null +++ b/frontend/public/components/dashboard/inventory-card/status-group.ts @@ -0,0 +1,7 @@ +export enum InventoryStatusGroup { + OK = 'OK', + WARN = 'WARN', + ERROR = 'ERROR', + PROGRESS = 'PROGRESS', + NOT_MAPPED = 'NOT_MAPPED', +} diff --git a/frontend/public/components/dashboard/inventory-card/utils.ts b/frontend/public/components/dashboard/inventory-card/utils.ts new file mode 100644 index 00000000000..f85049363f8 --- /dev/null +++ b/frontend/public/components/dashboard/inventory-card/utils.ts @@ -0,0 +1,50 @@ +import { podPhaseFilterReducer, nodeStatus } from '../../../module/k8s'; +import { pvcPhase } from '../../persistent-volume-claim'; +import { StatusGroupMapper } from './inventory-item'; +import { InventoryStatusGroup } from './status-group'; + +const POD_PHASE_GROUP_MAPPING = { + [InventoryStatusGroup.OK]: ['Running', 'Succeeded'], + [InventoryStatusGroup.ERROR]: ['CrashLoopBackOff', 'Failed'], + [InventoryStatusGroup.PROGRESS]: ['Terminating', 'Pending'], + [InventoryStatusGroup.WARN]: ['Unknown'], +}; + +const PVC_STATUS_GROUP_MAPPING = { + [InventoryStatusGroup.OK]: ['Bound'], + [InventoryStatusGroup.ERROR]: ['Lost'], + [InventoryStatusGroup.PROGRESS]: ['Pending'], +}; + +const NODE_STATUS_GROUP_MAPPING = { + [InventoryStatusGroup.OK]: ['Ready'], + [InventoryStatusGroup.PROGRESS]: ['Not Ready'], +}; + +const getStatusGroups = (resources, mapping, mapper, filterType) => { + const groups = { + [InventoryStatusGroup.NOT_MAPPED]: { + statusIDs: [], + count: 0, + }, + }; + Object.keys(mapping).forEach(key => { + groups[key] = { + statusIDs: [...mapping[key]], + count: 0, + filterType, + }; + }); + + resources.forEach(resource => { + const status = mapper(resource); + const group = Object.keys(mapping).find(key => mapping[key].includes(status)) || InventoryStatusGroup.NOT_MAPPED; + groups[group].count++; + }); + + return groups; +}; + +export const getPodStatusGroups: StatusGroupMapper = resources => getStatusGroups(resources, POD_PHASE_GROUP_MAPPING, podPhaseFilterReducer, 'pod-status'); +export const getNodeStatusGroups: StatusGroupMapper = resources => getStatusGroups(resources, NODE_STATUS_GROUP_MAPPING, nodeStatus, 'node-status'); +export const getPVCStatusGroups: StatusGroupMapper = resources => getStatusGroups(resources, PVC_STATUS_GROUP_MAPPING, pvcPhase, 'pvc-status'); diff --git a/frontend/public/components/dashboards-page/dashboards.tsx b/frontend/public/components/dashboards-page/dashboards.tsx index 4dc53912bc1..37552fde784 100644 --- a/frontend/public/components/dashboards-page/dashboards.tsx +++ b/frontend/public/components/dashboards-page/dashboards.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { connect } from 'react-redux'; +import { Map as ImmutableMap } from 'immutable'; import * as plugins from '../../plugins'; import { OverviewDashboard } from './overview-dashboard/overview-dashboard'; @@ -41,8 +42,8 @@ const tabs: Page[] = [ ...getPluginTabPages(), ]; -const DashboardsPage_: React.FC = ({ match, kindsInFlight }) => { - return kindsInFlight +const DashboardsPage_: React.FC = ({ match, kindsInFlight, k8sModels }) => { + return kindsInFlight && k8sModels.size === 0 ? : ( <> @@ -54,10 +55,12 @@ const DashboardsPage_: React.FC = ({ match, kindsInFlight } const mapStateToProps = ({k8s}) => ({ kindsInFlight: k8s.getIn(['RESOURCES', 'inFlight']), + k8sModels: k8s.getIn(['RESOURCES', 'models']), }); export const DashboardsPage = connect(mapStateToProps)(DashboardsPage_); type DashboardsPageProps = RouteComponentProps & { kindsInFlight: boolean; + k8sModels: ImmutableMap; }; diff --git a/frontend/public/components/dashboards-page/overview-dashboard/inventory-card.tsx b/frontend/public/components/dashboards-page/overview-dashboard/inventory-card.tsx new file mode 100644 index 00000000000..121181eff95 --- /dev/null +++ b/frontend/public/components/dashboards-page/overview-dashboard/inventory-card.tsx @@ -0,0 +1,117 @@ +import * as React from 'react'; +import * as _ from 'lodash-es'; + +import * as plugins from '../../../plugins'; +import { + DashboardCard, + DashboardCardBody, + DashboardCardHeader, + DashboardCardTitle, +} from '../../dashboard/dashboard-card'; +import { InventoryItem } from '../../dashboard/inventory-card/inventory-item'; +import { DashboardItemProps, withDashboardResources } from '../with-dashboard-resources'; +import { PodModel, NodeModel, PersistentVolumeClaimModel } from '../../../models'; +import { K8sResourceKind, PodKind } from '../../../module/k8s'; +import { getPodStatusGroups, getNodeStatusGroups, getPVCStatusGroups } from '../../dashboard/inventory-card/utils'; +import { FirehoseResource } from '../../utils'; + +const k8sResources: FirehoseResource[] = [ + { + isList: true, + kind: PodModel.kind, + prop: 'pods', + }, + { + isList: true, + kind: NodeModel.kind, + prop: 'nodes', + }, + { + isList: true, + kind: PersistentVolumeClaimModel.kind, + prop: 'pvcs', + }, +]; + +const uniqueResource = (resource: FirehoseResource, index: number): FirehoseResource => ({ + ...resource, + prop: `${index}-${resource.prop}`, +}); + +const getResourcesToWatch = (): FirehoseResource[] => { + const allResources = [...k8sResources]; + const pluginItems = plugins.registry.getDashboardsOverviewInventoryItems(); + pluginItems.forEach((item, index) => { + allResources.push(uniqueResource(item.properties.resource, index)); + if (item.properties.additionalResources) { + item.properties.additionalResources.forEach(ar => allResources.push(uniqueResource(ar, index))); + } + }); + return allResources; +}; + +const InventoryCard_: React.FC = ({ watchK8sResource, stopWatchK8sResource, resources }) => { + React.useEffect(() => { + const resourcesToWatch = getResourcesToWatch(); + resourcesToWatch.forEach(r => watchK8sResource(r)); + return () => { + resourcesToWatch.forEach(r => stopWatchK8sResource(r)); + }; + }, [watchK8sResource, stopWatchK8sResource]); + + const nodes = _.get(resources, 'nodes'); + const nodesLoaded = _.get(nodes, 'loaded'); + const nodesData = _.get(nodes, 'data', []) as K8sResourceKind[]; + + const pods = _.get(resources, 'pods'); + const podsLoaded = _.get(pods, 'loaded'); + const podsData = _.get(pods, 'data', []) as PodKind[]; + + const pvcs = _.get(resources, 'pvcs'); + const pvcsLoaded = _.get(pvcs, 'loaded'); + const pvcsData = _.get(pvcs, 'data', []) as K8sResourceKind[]; + + const pluginItems = plugins.registry.getDashboardsOverviewInventoryItems(); + return ( + + + Cluster inventory + + + + + + {pluginItems.map((item, index) => { + const resource = _.get(resources, uniqueResource(item.properties.resource, index).prop); + const resourceLoaded = _.get(resource, 'loaded'); + const resourceData = _.get(resource, 'data', []) as K8sResourceKind[]; + + const additionalResources = {}; + if (item.properties.additionalResources) { + item.properties.additionalResources.forEach(ar => { + additionalResources[ar.prop] = _.get(resources, uniqueResource(ar, index).prop); + }); + } + const additionalResourcesLoaded = Object.keys(additionalResources).every(key => _.get(additionalResources[key], 'loaded')); + const additionalResourcesData = {}; + + Object.keys(additionalResources).forEach(key => additionalResourcesData[key] = _.get(additionalResources[key], 'data', [])); + + return ( + + ); + })} + + + ); +}; + +export const InventoryCard = withDashboardResources(InventoryCard_); diff --git a/frontend/public/components/dashboards-page/overview-dashboard/overview-dashboard.tsx b/frontend/public/components/dashboards-page/overview-dashboard/overview-dashboard.tsx index b03d2721ec1..29af272bb6c 100644 --- a/frontend/public/components/dashboards-page/overview-dashboard/overview-dashboard.tsx +++ b/frontend/public/components/dashboards-page/overview-dashboard/overview-dashboard.tsx @@ -4,10 +4,11 @@ import { Dashboard, DashboardGrid } from '../../dashboard'; import { HealthCard } from './health-card'; import { DetailsCard } from './details-card'; import { CapacityCard } from './capacity-card'; +import { InventoryCard } from './inventory-card'; export const OverviewDashboard: React.FC<{}> = () => { const mainCards = [HealthCard, CapacityCard]; - const leftCards = [DetailsCard]; + const leftCards = [DetailsCard, InventoryCard]; return ( diff --git a/frontend/public/components/persistent-volume-claim.jsx b/frontend/public/components/persistent-volume-claim.jsx index bc1fbd87172..2b612827b9c 100644 --- a/frontend/public/components/persistent-volume-claim.jsx +++ b/frontend/public/components/persistent-volume-claim.jsx @@ -9,7 +9,7 @@ import { DetailsPage, ListPage, Table, TableRow, TableData } from './factory'; import { Kebab, navFactory, ResourceKebab, SectionHeading, ResourceLink, ResourceSummary, Selector, StatusIconAndText } from './utils'; import { ResourceEventStream } from './events'; -const pvcPhase = pvc => pvc.status.phase; +export const pvcPhase = pvc => pvc.status.phase; const { common, ExpandPVC } = Kebab.factory; const menuActions = [ExpandPVC, ...common]; diff --git a/frontend/public/style.scss b/frontend/public/style.scss index 016352a113b..479aaa42545 100644 --- a/frontend/public/style.scss +++ b/frontend/public/style.scss @@ -99,5 +99,6 @@ @import "components/dashboard/health-card/health-card"; @import "components/dashboard/details-card/details-card"; @import "components/dashboard/capacity-card/capacity-card"; +@import "components/dashboard/inventory-card/inventory-card"; @import "components/nav/nav-header";