-
Notifications
You must be signed in to change notification settings - Fork 593
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
Added insights-plugin #6660
Added insights-plugin #6660
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
reviewers: | ||
- bond95 | ||
- tisnik | ||
approvers: | ||
- bond95 | ||
- tisnik | ||
labels: | ||
- component/insights | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You'll need a PR to the openshift/release repo to add this label. See openshift/release#4526 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I created PR for that openshift/release#12398 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
"name": "@console/insights-plugin", | ||
"version": "0.0.0-fixed", | ||
"description": "Insights - provide cluster health data and integrate with OpenShift Cluster Manager", | ||
"private": true, | ||
"main": "src/index.ts", | ||
"scripts": { | ||
"test": "yarn --cwd ../.. test packages/insights-plugin" | ||
}, | ||
"dependencies": { | ||
"@console/internal": "0.0.0-fixed", | ||
"@console/patternfly": "0.0.0-fixed", | ||
"@console/plugin-sdk": "0.0.0-fixed", | ||
"@console/shared": "0.0.0-fixed" | ||
}, | ||
"consolePlugin": { | ||
"entry": "src/plugin.tsx" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import * as React from 'react'; | ||
|
||
const CriticalIcon = (props) => ( | ||
<svg viewbox="0 0 10 10" {...props}> | ||
<polygon points="10 10, 10 3, 5 0, 0 3, 0 10, 5 8" /> | ||
</svg> | ||
); | ||
|
||
export default CriticalIcon; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import * as React from 'react'; | ||
import { ChartDonut, ChartLegend, ChartLabel } from '@patternfly/react-charts'; | ||
import { riskIcons, colorScale, legendColorScale, riskSorting, mapMetrics } from './mappers'; | ||
import { PrometheusHealthPopupProps } from '@console/plugin-sdk'; | ||
import { K8sResourceKind } from '@console/internal/module/k8s'; | ||
import { ExternalLink } from '@console/internal/components/utils'; | ||
import './style.scss'; | ||
|
||
const DataComponent: React.FC<DataComponentProps> = ({ x, y, datum }) => { | ||
const Icon = riskIcons[datum.id]; | ||
return <Icon x={x} y={y - 5} fill={legendColorScale[datum.id]} />; | ||
}; | ||
|
||
export const InsightsPopup: React.FC<PrometheusHealthPopupProps> = ({ responses, k8sResult }) => { | ||
const resource = mapMetrics(responses[0].response); | ||
const clusterID = (k8sResult as K8sResourceKind)?.data?.spec?.clusterID || ''; | ||
const riskEntries = Object.entries(resource).sort( | ||
([k1], [k2]) => riskSorting[k1] - riskSorting[k2], | ||
); | ||
const numberOfIssues = Object.values(resource).reduce((acc, cur) => acc + cur, 0); | ||
const hasIssues = riskEntries.length > 0 && numberOfIssues > 0; | ||
|
||
return ( | ||
<div className="co-insights__box"> | ||
<div className="co-status-popup__section"> | ||
Insights identifies and prioritizes risks to security, performance, availability, and | ||
stability of your clusters. | ||
</div> | ||
<div className="co-status-popup__section"> | ||
{hasIssues && ( | ||
<div> | ||
<ChartDonut | ||
data={riskEntries.map(([k, v]) => ({ | ||
label: `${v} ${k}`, | ||
x: k, | ||
y: v, | ||
}))} | ||
title={`${numberOfIssues}`} | ||
subTitle="Total issues" | ||
legendData={Object.entries(resource).map(([k, v]) => ({ name: `${k}: ${v}` }))} | ||
legendOrientation="vertical" | ||
width={304} | ||
height={152} | ||
colorScale={colorScale} | ||
constrainToVisibleArea | ||
legendComponent={ | ||
<ChartLegend | ||
title="Total Risk" | ||
titleComponent={ | ||
<ChartLabel dx={13} style={{ fontWeight: 'bold', fontSize: '14px' }} /> | ||
} | ||
data={riskEntries.map(([k, v]) => ({ | ||
name: `${v} ${k}`, | ||
id: k, | ||
}))} | ||
dataComponent={<DataComponent />} | ||
x={-13} | ||
/> | ||
} | ||
padding={{ | ||
bottom: 20, | ||
left: 145, | ||
right: 20, // Adjusted to accommodate legend | ||
top: 0, | ||
}} | ||
/> | ||
</div> | ||
)} | ||
{!hasIssues && <div className="co-insights__no-rules">No Insights data to display.</div>} | ||
</div> | ||
<div className="co-status-popup__section"> | ||
{hasIssues && ( | ||
<> | ||
<h6 className="pf-c-title pf-m-md">Fixable issues</h6> | ||
<div> | ||
<ExternalLink | ||
href={`https://cloud.redhat.com/openshift/details/${clusterID}`} | ||
text="View all in OpenShift Cluster Manager" | ||
/> | ||
</div> | ||
</> | ||
)} | ||
{!hasIssues && ( | ||
<ExternalLink | ||
href="https://docs.openshift.com/container-platform/latest/support/getting-support.html" | ||
text="More about Insights" | ||
/> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export type DataComponentProps = { | ||
x?: number; | ||
y?: number; | ||
datum?: { | ||
id: string; | ||
}; | ||
}; | ||
|
||
InsightsPopup.displayName = 'InsightsPopup'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import * as _ from 'lodash'; | ||
|
||
import { global_palette_blue_50 as blue50 } from '@patternfly/react-tokens/dist/js/global_palette_blue_50'; | ||
import { global_palette_blue_300 as blue300 } from '@patternfly/react-tokens/dist/js/global_palette_blue_300'; | ||
import { global_palette_gold_400 as gold400 } from '@patternfly/react-tokens/dist/js/global_palette_gold_400'; | ||
import { global_palette_orange_300 as orange300 } from '@patternfly/react-tokens/dist/js/global_palette_orange_300'; | ||
import { global_palette_red_200 as red200 } from '@patternfly/react-tokens/dist/js/global_palette_red_200'; | ||
|
||
import { AngleDoubleDownIcon, AngleDoubleUpIcon, EqualsIcon } from '@patternfly/react-icons'; | ||
import CriticalIcon from './CriticalIcon'; | ||
import { PrometheusResponse } from '@console/internal/components/graphs'; | ||
|
||
export const riskIcons = { | ||
low: AngleDoubleDownIcon, | ||
moderate: EqualsIcon, | ||
important: AngleDoubleUpIcon, | ||
critical: CriticalIcon, | ||
}; | ||
|
||
export const colorScale = [blue50.value, gold400.value, orange300.value, red200.value]; | ||
|
||
export const legendColorScale = { | ||
low: blue300.value, | ||
moderate: gold400.value, | ||
important: orange300.value, | ||
critical: red200.value, | ||
}; | ||
|
||
export const riskSorting = { | ||
low: 0, | ||
moderate: 1, | ||
important: 2, | ||
critical: 3, | ||
}; | ||
|
||
type Metrics = { | ||
critical?: number; | ||
important?: number; | ||
low?: number; | ||
moderate?: number; | ||
}; | ||
|
||
export const mapMetrics = (response: PrometheusResponse): Metrics => { | ||
const values: Metrics = {}; | ||
for (let i = 0; i < response.data.result.length; i++) { | ||
const value = response.data?.result?.[i]?.value?.[1]; | ||
if (_.isNil(value)) { | ||
return null; | ||
} | ||
const metricName = response.data?.result?.[i]?.metric?.metric; | ||
values[metricName] = parseInt(value, 10); | ||
} | ||
|
||
return values; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import * as _ from 'lodash'; | ||
import { PrometheusHealthHandler, SubsystemHealth } from '@console/plugin-sdk'; | ||
import { HealthState } from '@console/shared/src/components/dashboard/status-card/states'; | ||
import { PrometheusResponse } from '@console/internal/components/graphs'; | ||
import { mapMetrics } from './mappers'; | ||
|
||
export const getClusterInsightsComponentStatus = ( | ||
response: PrometheusResponse, | ||
error, | ||
): SubsystemHealth => { | ||
if (error) { | ||
return { | ||
state: HealthState.NOT_AVAILABLE, | ||
message: 'Not available', | ||
}; | ||
} | ||
if (!response) { | ||
return { state: HealthState.LOADING }; | ||
} | ||
const values = mapMetrics(response); | ||
if (_.isNil(values)) { | ||
return { state: HealthState.UNKNOWN, message: 'Not available' }; | ||
} | ||
const issuesNumber = Object.values(values).reduce((acc, cur) => acc + cur, 0); | ||
const issueStr = `${issuesNumber} issues found`; | ||
if (values.critical > 0) { | ||
return { state: HealthState.ERROR, message: issueStr }; | ||
} | ||
if (issuesNumber > 0) { | ||
return { state: HealthState.WARNING, message: issueStr }; | ||
} | ||
return { state: HealthState.OK, message: issueStr }; | ||
}; | ||
|
||
export const getClusterInsightsStatus: PrometheusHealthHandler = (responses, cluster) => { | ||
const componentHealth = getClusterInsightsComponentStatus( | ||
responses[0].response, | ||
responses[0].error, | ||
); | ||
if (componentHealth.state === HealthState.LOADING || !_.get(cluster, 'loaded')) { | ||
return { state: HealthState.LOADING }; | ||
} | ||
|
||
return componentHealth; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.co-insights__no-rules { | ||
color: var(--pf-global--Color--200); | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,31 @@ | ||||||||||||
import { Plugin, DashboardsOverviewHealthPrometheusSubsystem } from '@console/plugin-sdk'; | ||||||||||||
import { ClusterVersionModel } from '@console/internal/models'; | ||||||||||||
import { referenceForModel } from '@console/internal/module/k8s'; | ||||||||||||
import { getClusterInsightsStatus } from './components/InsightsPopup/status'; | ||||||||||||
|
||||||||||||
type ConsumedExtensions = DashboardsOverviewHealthPrometheusSubsystem; | ||||||||||||
|
||||||||||||
const plugin: Plugin<ConsumedExtensions> = [ | ||||||||||||
{ | ||||||||||||
type: 'Dashboards/Overview/Health/Prometheus', | ||||||||||||
properties: { | ||||||||||||
title: 'Insights', | ||||||||||||
queries: ["health_statuses_insights{metric=~'low|moderate|important|critical'}"], | ||||||||||||
healthHandler: getClusterInsightsStatus, | ||||||||||||
additionalResource: { | ||||||||||||
kind: referenceForModel(ClusterVersionModel), | ||||||||||||
namespaced: false, | ||||||||||||
name: 'version', | ||||||||||||
isList: false, | ||||||||||||
prop: 'cluster', | ||||||||||||
}, | ||||||||||||
popupComponent: () => | ||||||||||||
import('./components/InsightsPopup/index' /* webpackChunkName: "insights-plugin" */).then( | ||||||||||||
(m) => m.InsightsPopup, | ||||||||||||
), | ||||||||||||
popupTitle: 'Insights status', | ||||||||||||
}, | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You need to gate the extension by some model contributed by Insights operator. To do so:
{
type: 'ModelDefinition',
properties: {
models: [YourModel],
},
},
const INSIGHTS_FLAG = 'INSIGHTS_FLAG'; {
type: 'FeatureFlag/Model',
properties: {
model: YourModel,
flag: INSIGHTS_FLAG,
},
},
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bond95 As @rawagner wrote, we should enable the plugin's extensions only when the cluster has the right capability (i.e. Insights operator is installed). Detecting Insights operator can be done via CRD detection. Use Every other extension can then use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rawagner @vojtechszocs What if we don't have any CRD? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another option is to implement your own detection logic and use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rawagner We had discussion with @smarterclayton and @spadgett, they suggested not to hide tab even if insights-operator is disabled. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bond95 Assuming that Insights integration is implemented via cluster-level operator (i.e. not via OLM operator), it makes sense to have Insights plugin extensions active by default 👍 |
||||||||||||
}, | ||||||||||||
]; | ||||||||||||
|
||||||||||||
export default plugin; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bond95 @spadgett FYI, this means Insights plugin will be included in all Console builds by default.