From 414e42201d5bda32fbb585a1b0c8de39fe2ffc49 Mon Sep 17 00:00:00 2001 From: Rastislav Wagner Date: Wed, 5 Jun 2019 13:11:04 +0200 Subject: [PATCH] Add details card. Connect dashboard cards to K8s resources with Firehose --- .../components/utils/firehose.spec.tsx | 2 +- frontend/public/components/about-modal.tsx | 12 +- .../dashboard/details-card/detail-item.tsx | 32 +++++ .../dashboard/details-card/details-body.tsx | 9 ++ .../dashboard/details-card/details-card.scss | 23 ++++ .../dashboard/details-card/index.ts | 2 + .../components/dashboards-page/dashboards.tsx | 29 ++++- .../overview-dashboard/details-card.tsx | 121 ++++++++++++++++++ .../overview-dashboard/health-card.tsx | 4 +- .../overview-dashboard/overview-dashboard.tsx | 7 +- .../with-dashboard-resources.tsx | 75 ++++++++--- .../public/components/factory/details.tsx | 13 +- frontend/public/components/overview/index.tsx | 39 +++--- frontend/public/components/utils/index.tsx | 18 +++ frontend/public/models/index.ts | 15 +++ .../public/module/k8s/cluster-settings.ts | 12 ++ frontend/public/style.scss | 1 + 17 files changed, 344 insertions(+), 70 deletions(-) create mode 100644 frontend/public/components/dashboard/details-card/detail-item.tsx create mode 100644 frontend/public/components/dashboard/details-card/details-body.tsx create mode 100644 frontend/public/components/dashboard/details-card/details-card.scss create mode 100644 frontend/public/components/dashboard/details-card/index.ts create mode 100644 frontend/public/components/dashboards-page/overview-dashboard/details-card.tsx diff --git a/frontend/__tests__/components/utils/firehose.spec.tsx b/frontend/__tests__/components/utils/firehose.spec.tsx index ccd6ab8024e..35d5686965a 100644 --- a/frontend/__tests__/components/utils/firehose.spec.tsx +++ b/frontend/__tests__/components/utils/firehose.spec.tsx @@ -4,9 +4,9 @@ import { Map as ImmutableMap } from 'immutable'; import Spy = jasmine.Spy; import { Firehose } from '../../../public/components/utils/firehose'; -import { FirehoseResource } from '../../../public/components/factory'; import { K8sKind, K8sResourceKindReference } from '../../../public/module/k8s'; import { PodModel, ServiceModel } from '../../../public/models'; +import { FirehoseResource } from '../../../public/components/utils'; // TODO(alecmerdler): Use these once `Firehose` is converted to TypeScript type FirehoseProps = { diff --git a/frontend/public/components/about-modal.tsx b/frontend/public/components/about-modal.tsx index 6428b12dd72..cc89bb7b2c4 100644 --- a/frontend/public/components/about-modal.tsx +++ b/frontend/public/components/about-modal.tsx @@ -8,29 +8,29 @@ import { connectToFlags } from '../reducers/features'; import { getBrandingDetails } from './masthead'; import { Firehose } from './utils'; import { ClusterVersionModel } from '../models'; -import { ClusterVersionKind, referenceForModel, UpdateHistory } from '../module/k8s'; +import { ClusterVersionKind, referenceForModel } from '../module/k8s'; import { k8sVersion } from '../module/status'; -import { hasAvailableUpdates } from '../module/k8s/cluster-settings'; +import { hasAvailableUpdates, getK8sGitVersion, getOpenShiftVersion } from '../module/k8s/cluster-settings'; const AboutModalItems: React.FC = ({closeAboutModal, cv}) => { const [kubernetesVersion, setKubernetesVersion] = React.useState(''); React.useEffect(() => { k8sVersion() - .then(({gitVersion}) => setKubernetesVersion(gitVersion)) + .then(response => setKubernetesVersion(getK8sGitVersion(response) || '-')) .catch(() => setKubernetesVersion('unknown')); }, []); const clusterID: string = _.get(cv, 'data.spec.clusterID'); const channel: string = _.get(cv, 'data.spec.channel'); - const lastUpdate: UpdateHistory = _.get(cv, 'data.status.history[0]'); + const openshiftVersion = getOpenShiftVersion(_.get(cv, 'data')); return ( {hasAvailableUpdates(cv.data) && Update available. View Cluster Settings} />} - {lastUpdate && ( + {openshiftVersion && ( OpenShift Version - {lastUpdate.state === 'Partial' ? `Updating to ${lastUpdate.version}` : lastUpdate.version} + {openshiftVersion} )} Kubernetes Version diff --git a/frontend/public/components/dashboard/details-card/detail-item.tsx b/frontend/public/components/dashboard/details-card/detail-item.tsx new file mode 100644 index 00000000000..dc9c1361a5a --- /dev/null +++ b/frontend/public/components/dashboard/details-card/detail-item.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import * as _ from 'lodash-es'; +import { OverlayTrigger, Tooltip } from 'patternfly-react'; + +import { LoadingInline } from '../../utils'; + +export const DetailItem: React.FC = React.memo(({ title, value, isLoading }) => { + const description = value ? ( + {value}} + placement="top" + trigger={['hover', 'focus']} + rootClose={false} + > + {value} + + ) : ( + '-' + ); + return ( + +
{title}
+
{isLoading ? : description}
+
+ ); +}); + +type DetailItemProps = { + title: string; + value?: string; + isLoading: boolean; +}; diff --git a/frontend/public/components/dashboard/details-card/details-body.tsx b/frontend/public/components/dashboard/details-card/details-body.tsx new file mode 100644 index 00000000000..ac9b0e3db1e --- /dev/null +++ b/frontend/public/components/dashboard/details-card/details-body.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; + +export const DetailsBody: React.FC = ({ children }) => ( +
{children}
+); + +type DetailsBodyProps = { + children: React.ReactNode; +} diff --git a/frontend/public/components/dashboard/details-card/details-card.scss b/frontend/public/components/dashboard/details-card/details-card.scss new file mode 100644 index 00000000000..8858a5306a4 --- /dev/null +++ b/frontend/public/components/dashboard/details-card/details-card.scss @@ -0,0 +1,23 @@ +.co-details-card { + margin-bottom: 1rem; +} + +.co-details-card__body { + display: flex; + flex-wrap: wrap; +} + +.co-details-card__item-title { + flex-basis: 100%; + font-size: 0.875rem; + text-transform: capitalize; +} + +.co-details-card__item-value { + flex-basis: 100%; + font-size: 0.875rem; + margin-bottom: 8px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/frontend/public/components/dashboard/details-card/index.ts b/frontend/public/components/dashboard/details-card/index.ts new file mode 100644 index 00000000000..5bc65d829ed --- /dev/null +++ b/frontend/public/components/dashboard/details-card/index.ts @@ -0,0 +1,2 @@ +export * from './detail-item'; +export * from './details-body'; diff --git a/frontend/public/components/dashboards-page/dashboards.tsx b/frontend/public/components/dashboards-page/dashboards.tsx index 407ab199f3a..bb0379180a9 100644 --- a/frontend/public/components/dashboards-page/dashboards.tsx +++ b/frontend/public/components/dashboards-page/dashboards.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import { RouteComponentProps } from 'react-router-dom'; +import { connect } from 'react-redux'; import { OverviewDashboard } from './overview-dashboard/overview-dashboard'; -import { HorizontalNav, PageHeading } from '../utils'; +import { HorizontalNav, PageHeading, LoadingBox } from '../utils'; const tabs = [ { @@ -12,9 +13,23 @@ const tabs = [ }, ]; -export const DashboardsPage: React.FC = ({ match }) => ( - - - - -); +const _DashboardsPage: React.FC = ({ match, kindsInFlight }) => { + return kindsInFlight + ? + : ( + <> + + + + ); +}; + +const mapStateToProps = ({k8s}) => ({ + kindsInFlight: k8s.getIn(['RESOURCES', 'inFlight']), +}); + +export const DashboardsPage = connect(mapStateToProps)(_DashboardsPage); + +type DashboardsPageProps = RouteComponentProps & { + kindsInFlight: boolean; +}; diff --git a/frontend/public/components/dashboards-page/overview-dashboard/details-card.tsx b/frontend/public/components/dashboards-page/overview-dashboard/details-card.tsx new file mode 100644 index 00000000000..0956eefd006 --- /dev/null +++ b/frontend/public/components/dashboards-page/overview-dashboard/details-card.tsx @@ -0,0 +1,121 @@ +import * as React from 'react'; +import * as _ from 'lodash-es'; + +import { + DashboardCard, + DashboardCardBody, + DashboardCardHeader, + DashboardCardTitle, +} from '../../dashboard/dashboard-card'; +import { DetailsBody, DetailItem } from '../../dashboard/details-card'; +import { withDashboardResources, DashboardItemProps } from '../with-dashboard-resources'; +import { InfrastructureModel, ClusterVersionModel } from '../../../models'; +import { referenceForModel, K8sResourceKind, getOpenShiftVersion, getK8sGitVersion, getClusterName, ClusterVersionKind } from '../../../module/k8s'; +import { FLAGS } from '../../../const'; +import { flagPending, connectToFlags } from '../../../reducers/features'; +import { FirehoseResource } from '../../utils'; + +const getInfrastructurePlatform = (infrastructure: K8sResourceKind): string => _.get(infrastructure, 'status.platform'); + +const clusterVersionResource: FirehoseResource = { + kind: referenceForModel(ClusterVersionModel), + namespaced: false, + name: 'version', + isList: false, + prop: 'cv', +}; + +const infrastructureResource: FirehoseResource = { + kind: referenceForModel(InfrastructureModel), + namespaced: false, + name: 'cluster', + isList: false, + prop: 'infrastructure', +}; + +export const DetailsCard_: React.FC = ({ + watchURL, + stopWatchURL, + watchK8sResource, + stopWatchK8sResource, + resources, + urlResults, + flags, +}) => { + const openshiftFlag = flags[FLAGS.OPENSHIFT]; + React.useEffect(() => { + if (flagPending(openshiftFlag)) { + return; + } + if (openshiftFlag) { + watchK8sResource(clusterVersionResource); + watchK8sResource(infrastructureResource); + return () => { + stopWatchK8sResource(clusterVersionResource); + stopWatchK8sResource(infrastructureResource); + }; + } + watchURL('version'); + return () => { + stopWatchURL('version'); + }; + }, [openshiftFlag, watchK8sResource, stopWatchK8sResource, watchURL, stopWatchURL]); + + const clusterVersion = _.get(resources, 'cv'); + const clusterVersionLoaded = _.get(clusterVersion, 'loaded', false); + const openshiftVersion = getOpenShiftVersion(_.get(clusterVersion, 'data') as ClusterVersionKind); + + const infrastructure = _.get(resources, 'infrastructure'); + const infrastructureLoaded = _.get(infrastructure, 'loaded', false); + const infrastructureData = _.get(infrastructure, 'data') as K8sResourceKind; + + + const kubernetesVersionResponse = urlResults.getIn(['version', 'result']); + + return ( + + + Details + + + + {openshiftFlag ? ( + <> + + + + + ) : ( + + )} + + + + ); +}; + +type DetailsCardProps = DashboardItemProps & { + flags: {[FLAGS.OPENSHIFT]: boolean}; +} + +export const DetailsCard = withDashboardResources(connectToFlags(FLAGS.OPENSHIFT)(DetailsCard_)); diff --git a/frontend/public/components/dashboards-page/overview-dashboard/health-card.tsx b/frontend/public/components/dashboards-page/overview-dashboard/health-card.tsx index 2af1a9957ca..077247de031 100644 --- a/frontend/public/components/dashboards-page/overview-dashboard/health-card.tsx +++ b/frontend/public/components/dashboards-page/overview-dashboard/health-card.tsx @@ -66,7 +66,7 @@ const fetchK8sHealth = async(url) => { return response.text(); }; -const _HealthCard: React.FC = ({ +const HealthCard_: React.FC = ({ watchURL, stopWatchURL, watchPrometheus, @@ -153,7 +153,7 @@ const _HealthCard: React.FC = ({ ); }; -export const HealthCard = withDashboardResources(connect(mapStateToProps)(_HealthCard)); +export const HealthCard = withDashboardResources(connect(mapStateToProps)(HealthCard_)); type ClusterHealth = { state: HealthState; 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 4a613d9d4b9..e62acf723a5 100644 --- a/frontend/public/components/dashboards-page/overview-dashboard/overview-dashboard.tsx +++ b/frontend/public/components/dashboards-page/overview-dashboard/overview-dashboard.tsx @@ -2,15 +2,20 @@ import * as React from 'react'; import { Dashboard, DashboardGrid } from '../../dashboard'; import { HealthCard } from './health-card'; +import { DetailsCard } from './details-card'; export const OverviewDashboard: React.FC<{}> = () => { const mainCards = [ , ]; + const leftCards = [ + , + ]; + return ( - + ); }; diff --git a/frontend/public/components/dashboards-page/with-dashboard-resources.tsx b/frontend/public/components/dashboards-page/with-dashboard-resources.tsx index a809562ab24..76664d9625c 100644 --- a/frontend/public/components/dashboards-page/with-dashboard-resources.tsx +++ b/frontend/public/components/dashboards-page/with-dashboard-resources.tsx @@ -16,6 +16,8 @@ import { StopWatchPrometheusAction, } from '../../actions/dashboards'; import { RootState } from '../../redux'; +import { Firehose, FirehoseResource, FirehoseResult } from '../utils'; +import { K8sResourceKind } from '../../module/k8s'; const mapDispatchToProps = dispatch => ({ watchURL: (url, fetch): WatchURL => dispatch(watchURL(url, fetch)), @@ -30,18 +32,27 @@ const mapStateToProps = (state: RootState) => ({ }); const WithDashboardResources = (WrappedComponent: React.ComponentType) => - class WithDashboardResources_ extends React.Component { + class WithDashboardResources_ extends React.Component { private urls: Array = []; private queries: Array = []; - shouldComponentUpdate(nextProps: WithDashboardResourcesProps) { + constructor(props) { + super(props); + this.state = { + k8sResources: [], + }; + } + + shouldComponentUpdate(nextProps: WithDashboardResourcesProps, nextState: WithDashboardResourcesState) { const urlResultChanged = this.urls.some(urlKey => this.props[RESULTS_TYPE.URL].getIn([urlKey, 'result']) !== nextProps[RESULTS_TYPE.URL].getIn([urlKey, 'result']) ); const queryResultChanged = this.queries.some(query => this.props[RESULTS_TYPE.PROMETHEUS].getIn([query, 'result']) !== nextProps[RESULTS_TYPE.PROMETHEUS].getIn([query, 'result']) ); - return urlResultChanged || queryResultChanged; + const k8sResourcesChanged = this.state.k8sResources !== nextState.k8sResources; + + return urlResultChanged || queryResultChanged || k8sResourcesChanged; } watchURL: WatchURL = (url, fetch) => { @@ -54,16 +65,32 @@ const WithDashboardResources = (WrappedComponent: React.ComponentType { + this.setState((state: WithDashboardResourcesState) => ({ + k8sResources: [...state.k8sResources, resource], + })); + } + + stopWatchK8sResource: StopWatchK8sResource = resource => { + this.setState((state: WithDashboardResourcesState) => ({ + k8sResources: state.k8sResources.filter(r => r.prop !== resource.prop), + })); + } + render() { return ( - + + + ); } }; @@ -75,6 +102,10 @@ export type StopWatchURL = (url: string) => void; export type WatchPrometheus = (query: string) => void; export type StopWatchPrometheus = (query: string) => void; +type WithDashboardResourcesState = { + k8sResources: FirehoseResource[]; +}; + type WithDashboardResourcesProps = { watchURL: WatchURLAction; watchPrometheusQuery: WatchPrometheusQueryAction; @@ -84,11 +115,19 @@ type WithDashboardResourcesProps = { [RESULTS_TYPE.URL]: ImmutableMap; }; +export type WatchK8sResource = (resource: FirehoseResource) => void; +export type StopWatchK8sResource = (resource: FirehoseResource) => void; + export type DashboardItemProps = { - watchURL: WatchURL, - stopWatchURL: StopWatchURL, - watchPrometheus: WatchPrometheus, - stopWatchPrometheusQuery: StopWatchPrometheus, - urlResults: ImmutableMap, - prometheusResults: ImmutableMap, -} + watchURL: WatchURL; + stopWatchURL: StopWatchURL; + watchPrometheus: WatchPrometheus; + stopWatchPrometheusQuery: StopWatchPrometheus; + urlResults: ImmutableMap; + prometheusResults: ImmutableMap; + watchK8sResource: WatchK8sResource; + stopWatchK8sResource: StopWatchK8sResource; + resources?: { + [key: string]: FirehoseResult | FirehoseResult; + }; +}; diff --git a/frontend/public/components/factory/details.tsx b/frontend/public/components/factory/details.tsx index 40f9275bb57..b2f5ae573d5 100644 --- a/frontend/public/components/factory/details.tsx +++ b/frontend/public/components/factory/details.tsx @@ -2,21 +2,12 @@ import * as React from 'react'; import { match } from 'react-router-dom'; import * as _ from 'lodash-es'; -import { Firehose, HorizontalNav, PageHeading } from '../utils'; -import { K8sResourceKindReference, K8sResourceKind, K8sKind, Selector } from '../../module/k8s'; +import { Firehose, HorizontalNav, PageHeading, FirehoseResource } from '../utils'; +import { K8sResourceKindReference, K8sResourceKind, K8sKind } from '../../module/k8s'; import { withFallback } from '../utils/error-boundary'; import { ErrorBoundaryFallback } from '../error'; import { breadcrumbsForDetailsPage } from '../utils/breadcrumbs'; -export type FirehoseResource = { - kind: K8sResourceKindReference; - name?: string; - namespace: string; - isList?: boolean; - selector?: Selector; - prop: string; -}; - export const DetailsPage = withFallback((props) => @@ -901,7 +902,7 @@ class OverviewMainContent_ extends React.Component
= ({mock, match, selectedItem, title, export const Overview = connect(overviewStateToProps, overviewDispatchToProps)(Overview_); -type FirehoseItem = { - data?: K8sResourceKind; - [key: string]: any; -}; - -type FirehoseList = { - data?: T[]; - [key: string]: any; -}; - type OverviewItemAlerts = { [key: string]: { message: string; @@ -1161,23 +1152,23 @@ type OverviewMainContentPropsFromDispatch = { }; type OverviewMainContentOwnProps = { - builds?: FirehoseList; - buildConfigs?: FirehoseList; - daemonSets?: FirehoseList; - deploymentConfigs?: FirehoseList; - deployments?: FirehoseList; + builds?: FirehoseResult; + buildConfigs?: FirehoseResult; + daemonSets?: FirehoseResult; + deploymentConfigs?: FirehoseResult; + deployments?: FirehoseResult; mock: boolean; loaded?: boolean; loadError?: any; namespace: string; - pods?: FirehoseList; - project?: FirehoseItem; - replicationControllers?: FirehoseList; - replicaSets?: FirehoseList; - routes?: FirehoseList; - services?: FirehoseList; + pods?: FirehoseResult; + project?: FirehoseResult; + replicationControllers?: FirehoseResult; + replicaSets?: FirehoseResult; + routes?: FirehoseResult; + services?: FirehoseResult; selectedItem: OverviewItem; - statefulSets?: FirehoseList; + statefulSets?: FirehoseResult; title?: string; }; diff --git a/frontend/public/components/utils/index.tsx b/frontend/public/components/utils/index.tsx index 93fb397e0fe..c3941d22db4 100644 --- a/frontend/public/components/utils/index.tsx +++ b/frontend/public/components/utils/index.tsx @@ -1,3 +1,5 @@ +import { K8sResourceKindReference, Selector, K8sResourceKind } from '../../module/k8s'; + export * from './line-buffer'; export * from './promise-component'; export * from './kebab'; @@ -78,3 +80,19 @@ export const enum EnvType { ENV = 0, ENV_FROM = 1 } + +export type FirehoseResource = { + kind: K8sResourceKindReference; + name?: string; + namespace?: string; + isList?: boolean; + selector?: Selector; + prop: string; + namespaced?: boolean, +}; + +export type FirehoseResult = { + loaded: boolean; + loadError: string; + data: R; +}; diff --git a/frontend/public/models/index.ts b/frontend/public/models/index.ts index f033250df90..146aed53a75 100644 --- a/frontend/public/models/index.ts +++ b/frontend/public/models/index.ts @@ -947,3 +947,18 @@ export const OAuthModel: K8sKind = { id: 'oauth', crd: true, }; + +export const InfrastructureModel: K8sKind = { + label: 'Infrastructure', + labelPlural: 'Infrastructures', + apiVersion: 'v1', + path: 'infrastructures', + apiGroup: 'config.openshift.io', + plural: 'infrastructures', + abbr: 'INF', + namespaced: false, + kind: 'Infrastructure', + id: 'infrastructure', + crd: true, +}; + diff --git a/frontend/public/module/k8s/cluster-settings.ts b/frontend/public/module/k8s/cluster-settings.ts index 580381efcf4..c1ad3bf0eb7 100644 --- a/frontend/public/module/k8s/cluster-settings.ts +++ b/frontend/public/module/k8s/cluster-settings.ts @@ -78,3 +78,15 @@ export const getClusterUpdateStatus = (cv: ClusterVersionKind): ClusterUpdateSta return hasAvailableUpdates(cv) ? ClusterUpdateStatus.UpdatesAvailable : ClusterUpdateStatus.UpToDate; }; + +export const getK8sGitVersion = (k8sVersionResponse): string => _.get(k8sVersionResponse, 'gitVersion'); + +export const getOpenShiftVersion = (cv: ClusterVersionKind): string => { + const lastUpdate: UpdateHistory = _.get(cv, 'status.history[0]'); + if (!lastUpdate) { + return null; + } + return lastUpdate.state === 'Partial' ? `Updating to ${lastUpdate.version}` : lastUpdate.version; +}; + +export const getClusterName = (): string => window.SERVER_FLAGS.kubeAPIServerURL || null; diff --git a/frontend/public/style.scss b/frontend/public/style.scss index 1981626b4ad..7386ba14c66 100644 --- a/frontend/public/style.scss +++ b/frontend/public/style.scss @@ -98,3 +98,4 @@ @import "components/dashboard/dashboard"; @import "components/dashboard/dashboard-card/card"; @import "components/dashboard/health-card/health-card"; +@import "components/dashboard/details-card/details-card";