-
Notifications
You must be signed in to change notification settings - Fork 670
Bug 1795398: Monitoring Dashboards: Load available dashboards dynamically #4388
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
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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -14,7 +14,6 @@ import DashboardCardHeader from '@console/shared/src/components/dashboard/dashbo | |||||
| import DashboardCardTitle from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardTitle'; | ||||||
|
|
||||||
| import * as UIActions from '../../../actions/ui'; | ||||||
| import { k8sBasePath } from '../../../module/k8s'; | ||||||
| import { ErrorBoundaryFallback } from '../../error'; | ||||||
| import { RootState } from '../../../redux'; | ||||||
| import { getPrometheusURL, PrometheusEndpoint } from '../../graphs/helpers'; | ||||||
|
|
@@ -268,30 +267,21 @@ const PollIntervalDropdown = connect( | |||||
| }, | ||||||
| )(PollIntervalDropdown_); | ||||||
|
|
||||||
| // TODO: Dynamically load the list of dashboards | ||||||
| const boards = [ | ||||||
| 'etcd', | ||||||
| 'k8s-resources-cluster', | ||||||
| 'k8s-resources-namespace', | ||||||
| 'k8s-resources-workloads-namespace', | ||||||
| 'k8s-resources-node', | ||||||
| 'k8s-resources-pod', | ||||||
| 'k8s-resources-workload', | ||||||
| 'cluster-total', | ||||||
| 'prometheus', | ||||||
| 'node-cluster-rsrc-use', | ||||||
| 'node-rsrc-use', | ||||||
| ]; | ||||||
| const boardItems = _.zipObject(boards, boards); | ||||||
|
|
||||||
| // Matches Prometheus labels surrounded by {{ }} in the graph legend label templates | ||||||
| const legendTemplateOptions = { interpolate: /{{([a-zA-Z_][a-zA-Z0-9_]*)}}/g }; | ||||||
|
|
||||||
| const CardBody_: React.FC<CardBodyProps> = ({ panel, pollInterval, variables }) => { | ||||||
| const formatLegendLabel = React.useCallback( | ||||||
| (labels, i) => { | ||||||
| const compiled = _.template(panel.targets?.[i]?.legendFormat, legendTemplateOptions); | ||||||
| return compiled(labels); | ||||||
| const legendFormat = panel.targets?.[i]?.legendFormat; | ||||||
| const compiled = _.template(legendFormat, legendTemplateOptions); | ||||||
| try { | ||||||
| return compiled(labels); | ||||||
| } catch (e) { | ||||||
| // If we can't format the label (e.g. if one of it's variables is missing from `labels`), | ||||||
| // show the template string instead | ||||||
| return legendFormat; | ||||||
| } | ||||||
| }, | ||||||
| [panel], | ||||||
| ); | ||||||
|
|
@@ -370,98 +360,94 @@ const Card: React.FC<CardProps> = ({ panel }) => { | |||||
| ); | ||||||
| }; | ||||||
|
|
||||||
| const Board: React.FC<BoardProps> = ({ board, patchVariable }) => { | ||||||
| const [data, setData] = React.useState(); | ||||||
| const [error, setError] = React.useState<string>(); | ||||||
| const Board: React.FC<BoardProps> = ({ rows }) => ( | ||||||
| <> | ||||||
| {_.map(rows, (row, i) => ( | ||||||
| <div className="row monitoring-dashboards__row" key={i}> | ||||||
| {_.map(row.panels, (panel) => ( | ||||||
| <Card key={panel.id} panel={panel} /> | ||||||
| ))} | ||||||
| </div> | ||||||
| ))} | ||||||
| </> | ||||||
| ); | ||||||
|
|
||||||
| const MonitoringDashboardsPage_: React.FC<MonitoringDashboardsPageProps> = ({ | ||||||
| clearVariables, | ||||||
| deleteAll, | ||||||
| match, | ||||||
| patchVariable, | ||||||
| }) => { | ||||||
| const [board, setBoard] = React.useState(); | ||||||
| const [boards, setBoards] = React.useState([]); | ||||||
| const [error, setError] = React.useState(); | ||||||
| const [isLoading, , , setLoaded] = useBoolean(true); | ||||||
|
|
||||||
| const safeFetch = React.useCallback(useSafeFetch(), []); | ||||||
|
|
||||||
| // Clear queries on unmount | ||||||
| React.useEffect(() => deleteAll, [deleteAll]); | ||||||
|
|
||||||
| React.useEffect(() => { | ||||||
| if (!board) { | ||||||
| return; | ||||||
| } | ||||||
| setData(undefined); | ||||||
| setError(undefined); | ||||||
| const path = `${k8sBasePath}/api/v1/namespaces/openshift-monitoring/configmaps/grafana-dashboard-${board}`; | ||||||
| safeFetch(path) | ||||||
| safeFetch('/api/console/monitoring-dashboard-config') | ||||||
| .then((response) => { | ||||||
| const json = _.get(response, ['data', `${board}.json`]); | ||||||
| if (!json) { | ||||||
| setError('Dashboard definition JSON not found'); | ||||||
| } else { | ||||||
| const newData = JSON.parse(json); | ||||||
|
|
||||||
| _.each(newData?.templating?.list as TemplateVariable[], (v) => { | ||||||
| if (v.type === 'query' || v.type === 'interval') { | ||||||
| patchVariable(v.name, { | ||||||
| isHidden: v.hide !== 0, | ||||||
| options: _.map(v.options, 'value'), | ||||||
| query: v.type === 'query' ? v.query : undefined, | ||||||
| value: _.find(v.options, { selected: true })?.value || v.options?.[0]?.value, | ||||||
| }); | ||||||
| } | ||||||
| }); | ||||||
| setLoaded(); | ||||||
| setError(undefined); | ||||||
|
|
||||||
| // Call this after creating the board's variables so we don't trigger a component update | ||||||
| // that will fail due to missing variables | ||||||
| setData(newData); | ||||||
| } | ||||||
| const getBoardData = (item) => ({ | ||||||
| data: JSON.parse(_.values(item?.data)[0]), | ||||||
| name: item.metadata.name, | ||||||
| }); | ||||||
| const newBoards = _.sortBy(_.map(response.items, getBoardData), (v) => | ||||||
| _.toLower(v?.data?.title), | ||||||
| ); | ||||||
| setBoards(newBoards); | ||||||
| }) | ||||||
| .catch((err) => { | ||||||
| setLoaded(); | ||||||
| if (err.name !== 'AbortError') { | ||||||
| setError(_.get(err, 'json.error', err.message)); | ||||||
| } | ||||||
| }); | ||||||
| }, [board, patchVariable, safeFetch]); | ||||||
|
|
||||||
| if (!board) { | ||||||
| return null; | ||||||
| } | ||||||
| if (error) { | ||||||
| return <ErrorAlert message={error} />; | ||||||
| } | ||||||
| if (!data) { | ||||||
| return <LoadingInline />; | ||||||
| } | ||||||
| }, [safeFetch, setLoaded]); | ||||||
|
|
||||||
| const rows = _.isEmpty(data.rows) ? [{ panels: data.panels }] : data.rows; | ||||||
| const boardItems = React.useMemo(() => _.mapValues(_.mapKeys(boards, 'name'), 'data.title'), [ | ||||||
| boards, | ||||||
| ]); | ||||||
|
|
||||||
| return ( | ||||||
| <> | ||||||
| {_.map(rows, (row, i) => ( | ||||||
| <div className="row monitoring-dashboards__row" key={i}> | ||||||
| {_.map(row.panels, (panel, j) => ( | ||||||
| <Card key={j} panel={panel} /> | ||||||
| ))} | ||||||
| </div> | ||||||
| ))} | ||||||
| </> | ||||||
| ); | ||||||
| }; | ||||||
| const changeBoard = (newBoard: string) => { | ||||||
| if (newBoard !== board) { | ||||||
| clearVariables(); | ||||||
|
|
||||||
| const MonitoringDashboardsPage_: React.FC<MonitoringDashboardsPageProps> = ({ | ||||||
| clearVariables, | ||||||
| deleteAll, | ||||||
| match, | ||||||
| patchVariable, | ||||||
| }) => { | ||||||
| const { board } = match.params; | ||||||
| const data = _.find(boards, { name: newBoard })?.data; | ||||||
|
|
||||||
| // Clear queries on unmount | ||||||
| React.useEffect(() => deleteAll, [deleteAll]); | ||||||
| _.each(data?.templating?.list as TemplateVariable[], (v) => { | ||||||
|
Member
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. We should avoid
Suggested change
Do we have a type we can assign to
Member
Author
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. Addressed by adding a |
||||||
| if (v.type === 'query' || v.type === 'interval') { | ||||||
| patchVariable(v.name, { | ||||||
| isHidden: v.hide !== 0, | ||||||
| options: _.map(v.options, 'value'), | ||||||
| query: v.type === 'query' ? v.query : undefined, | ||||||
| value: _.find(v.options, { selected: true })?.value || v.options?.[0]?.value, | ||||||
| }); | ||||||
| } | ||||||
| }); | ||||||
|
|
||||||
| const setBoard = (newBoard: string) => { | ||||||
| if (newBoard !== board) { | ||||||
| clearVariables(); | ||||||
| setBoard(newBoard); | ||||||
| history.replace(`/monitoring/dashboards/${newBoard}`); | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| if (!board && boards?.[0]) { | ||||||
| setBoard(boards[0]); | ||||||
| return null; | ||||||
| if (!board && !_.isEmpty(boards)) { | ||||||
| changeBoard(match.params.board || boards?.[0]?.name); | ||||||
| } | ||||||
|
|
||||||
| if (error) { | ||||||
| return <ErrorAlert message={error} />; | ||||||
| } | ||||||
|
|
||||||
| const data = _.find(boards, { name: board })?.data; | ||||||
| const rows = _.isEmpty(data?.rows) ? [{ panels: data?.panels }] : data?.rows; | ||||||
|
|
||||||
| return ( | ||||||
| <> | ||||||
| <Helmet> | ||||||
|
|
@@ -474,18 +460,18 @@ const MonitoringDashboardsPage_: React.FC<MonitoringDashboardsPageProps> = ({ | |||||
| </div> | ||||||
| <h1 className="co-m-pane__heading">Dashboards</h1> | ||||||
| <div className="monitoring-dashboards__variables"> | ||||||
| <VariableDropdown | ||||||
| items={boardItems} | ||||||
| label="Dashboard" | ||||||
| onChange={setBoard} | ||||||
| selectedKey={board} | ||||||
| /> | ||||||
| <AllVariableDropdowns /> | ||||||
| {!_.isEmpty(boardItems) && ( | ||||||
| <VariableDropdown | ||||||
| items={boardItems} | ||||||
| label="Dashboard" | ||||||
| onChange={changeBoard} | ||||||
| selectedKey={board} | ||||||
| /> | ||||||
| )} | ||||||
| <AllVariableDropdowns key={board} /> | ||||||
| </div> | ||||||
| </div> | ||||||
| <Dashboard> | ||||||
| <Board board={board} patchVariable={patchVariable} /> | ||||||
| </Dashboard> | ||||||
| <Dashboard>{isLoading ? <LoadingInline /> : <Board key={board} rows={rows} />}</Dashboard> | ||||||
| </> | ||||||
| ); | ||||||
| }; | ||||||
|
|
@@ -533,8 +519,7 @@ type SingleVariableDropdownProps = { | |||||
| }; | ||||||
|
|
||||||
| type BoardProps = { | ||||||
| board: string; | ||||||
| patchVariable: (key: string, patch: Variable) => undefined; | ||||||
| rows: Panel[]; | ||||||
| }; | ||||||
|
|
||||||
| type AllVariableDropdownsProps = { | ||||||
|
|
||||||
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.
We'll need to fix this endpoint for off-cluster mode since we won't have a service account token.
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.
@kyoto I opened #4407 to make it work off-cluster for development