Skip to content
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

Bug 1797027: Monitoring Dashboards: Fix variables handling #4184

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
204 changes: 132 additions & 72 deletions frontend/public/components/monitoring/dashboards/index.tsx
Expand Up @@ -5,6 +5,7 @@ import { Helmet } from 'react-helmet';
import { connect } from 'react-redux';
import { Map as ImmutableMap } from 'immutable';

import { RedExclamationCircleIcon } from '@console/shared';
import ErrorAlert from '@console/shared/src/components/alerts/error';
import Dashboard from '@console/shared/src/components/dashboard/Dashboard';
import DashboardCard from '@console/shared/src/components/dashboard/dashboard-card/DashboardCard';
Expand All @@ -30,53 +31,128 @@ const evaluateTemplate = (s: string, variables: VariablesMap) =>
_.reduce(
variables,
(result: string, v: Variable, k: string): string => {
return result.replace(new RegExp(`\\$${k}`, 'g'), v.value);
return result.replace(new RegExp(`\\$${k}`, 'g'), v.value === undefined ? '' : v.value);
Copy link
Member

Choose a reason for hiding this comment

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

Assuming null should also be '', we could use

Suggested change
return result.replace(new RegExp(`\\$${k}`, 'g'), v.value === undefined ? '' : v.value);
return result.replace(new RegExp(`\\$${k}`, 'g'), v.value || '');

Copy link
Member Author

Choose a reason for hiding this comment

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

👍 Thanks. v.value is a string so we don't need to worry about 0 values, etc.

Changed by #4221

},
s,
);

const VariableDropdown: React.FC<VariableDropdownProps> = ({
buttonClassName = 'monitoring-dashboards__dropdown-button',
isError = false,
items,
label,
onChange,
selectedKey,
title,
}) => (
<div className="form-group monitoring-dashboards__dropdown-wrap">
<label className="monitoring-dashboards__dropdown-title">{title}</label>
<Dropdown
buttonClassName={buttonClassName}
items={items}
onChange={onChange}
selectedKey={selectedKey}
/>
<label className="monitoring-dashboards__dropdown-title">{label}</label>
{isError ? (
<Dropdown
disabled
items={{}}
title={
<>
<RedExclamationCircleIcon /> Error loading options
</>
}
/>
) : (
<Dropdown
buttonClassName={buttonClassName}
items={items}
onChange={onChange}
selectedKey={selectedKey}
/>
)}
</div>
);

const SingleVariableDropdown: React.FC<SingleVariableDropdownProps> = ({
name,
options,
patchVariable,
query,
timespan,
value,
}) => {
const safeFetch = React.useCallback(useSafeFetch(), []);

const [isError, setIsError] = React.useState(false);

React.useEffect(() => {
if (query) {
// Convert label_values queries to something Prometheus can handle
// TODO: Once the Prometheus /series endpoint is available through the API proxy, this should
// be converted to use that instead
const prometheusQuery = query.replace(/label_values\((.*), (.*)\)/, 'count($1) by ($2)');

const url = getPrometheusURL({
endpoint: PrometheusEndpoint.QUERY_RANGE,
query: prometheusQuery,
samples: 30,
timeout: '5s',
timespan,
});

safeFetch(url)
.then(({ data }) => {
setIsError(false);
const newOptions = _.flatMap(data?.result, ({ metric }) => _.values(metric)).sort();
patchVariable(name, { options: newOptions });
})
.catch((err) => {
if (err.name !== 'AbortError') {
setIsError(true);
}
});
}
}, [name, patchVariable, query, safeFetch, timespan]);

const onChange = React.useCallback((v: string) => patchVariable(name, { value: v }), [
name,
patchVariable,
]);

if (!isError && !options.length) {
return null;
}

return (
<VariableDropdown
isError={isError}
items={_.zipObject(options, options)}
label={name}
onChange={onChange}
selectedKey={value}
/>
);
};

const AllVariableDropdowns_: React.FC<AllVariableDropdownsProps> = ({
patchVariable,
timespan,
variables,
}) => (
<>
{_.map(variables.toJS(), ({ options, value }, k) =>
_.isEmpty(options) ? null : (
<VariableDropdown
items={_.zipObject(options, options)}
key={k}
onChange={(v: string) => patchVariable(k, { value: v })}
selectedKey={value}
title={k}
}) => {
const vars = variables.toJS();
return (
<>
{_.map(vars, (v, name) => (
<SingleVariableDropdown
key={name}
name={name}
options={v.options}
patchVariable={patchVariable}
query={v.query ? evaluateTemplate(v.query, vars) : undefined}
timespan={timespan}
value={v.value}
/>
),
)}
</>
);
const AllVariableDropdowns = connect(
({ UI }: RootState) => ({
variables: UI.getIn(['monitoringDashboards', 'variables']),
}),
{ patchVariable: UIActions.monitoringDashboardsPatchVariable },
)(AllVariableDropdowns_);
))}
</>
);
};
const AllVariableDropdowns = connect(({ UI }: RootState) => ({
variables: UI.getIn(['monitoringDashboards', 'variables']),
}))(AllVariableDropdowns_);

const timespanOptions = {
'5m': '5 mintutes',
Expand Down Expand Up @@ -147,7 +223,8 @@ const Card_: React.FC<CardProps> = ({ panel, pollInterval, timespan, variables }
if (!rawQueries.length) {
return null;
}
const queries = rawQueries.map((expr) => evaluateTemplate(expr, variables.toJS()));
const variablesJS = variables.toJS();
const queries = rawQueries.map((expr) => evaluateTemplate(expr, variablesJS));

return (
<div className={`col-xs-12 col-sm-${colSpanSm} col-lg-${colSpan}`}>
Expand Down Expand Up @@ -206,29 +283,6 @@ const Board: React.FC<BoardProps> = ({ board, patchVariable, pollInterval, times

const safeFetch = React.useCallback(useSafeFetch(), []);

const loadVariableValues = React.useCallback(
(name: string, rawQuery: string) => {
// Convert label_values queries to something Prometheus can handle
// TODO: Once the Prometheus /series endpoint is available through the API proxy, this should
// be converted to use that instead
const query = rawQuery.replace(/label_values\((.*), (.*)\)/, 'count($1) by ($2)');
const url = getPrometheusURL({
endpoint: PrometheusEndpoint.QUERY_RANGE,
query,
samples: 30,
timeout: '5s',
timespan,
});

safeFetch(url).then((response) => {
const result = _.get(response, 'data.result');
const options = _.flatMap(result, ({ metric }) => _.values(metric)).sort();
patchVariable(name, options.length ? { options, value: options[0] } : { value: '' });
});
},
[patchVariable, safeFetch, timespan],
);

React.useEffect(() => {
if (!board) {
return;
Expand All @@ -245,19 +299,13 @@ const Board: React.FC<BoardProps> = ({ board, patchVariable, pollInterval, times
const newData = JSON.parse(json);
setData(newData);

const newVars = _.get(newData, 'templating.list') as TemplateVariable[];
const optionsVars = _.filter(newVars, (v) => v.type === 'query' || v.type === 'interval');

_.each(optionsVars, (v) => {
if (v.options.length === 1) {
patchVariable(v.name, { value: v.options[0].value });
} else if (v.options.length > 1) {
const options = _.map(v.options, 'value');
const selected = _.find(v.options, { selected: true });
const value = (selected || v.options[0]).value;
patchVariable(v.name, { options, value });
} else if (!_.isEmpty(v.query)) {
loadVariableValues(v.name, v.query);
_.each(newData?.templating?.list as TemplateVariable[], (v) => {
if (v.type === 'query' || v.type === 'interval') {
patchVariable(v.name, {
options: _.map(v.options, 'value'),
query: v.type === 'query' ? v.query : undefined,
value: _.find(v.options, { selected: true })?.value,
});
}
});
}
Expand All @@ -267,7 +315,7 @@ const Board: React.FC<BoardProps> = ({ board, patchVariable, pollInterval, times
setError(_.get(err, 'json.error', err.message));
}
});
}, [board, loadVariableValues, patchVariable, safeFetch]);
}, [board, patchVariable, safeFetch]);

if (!board) {
return null;
Expand Down Expand Up @@ -331,28 +379,28 @@ const MonitoringDashboardsPage_: React.FC<MonitoringDashboardsPageProps> = ({
<div className="monitoring-dashboards__options">
<VariableDropdown
items={timespanOptions}
label="Time Range"
onChange={(v: string) => setTimespan(parsePrometheusDuration(v))}
selectedKey={defaultTimespan}
title="Time Range"
/>
<VariableDropdown
items={pollIntervalOptions}
label="Refresh Interval"
onChange={(v: string) =>
setPollInterval(v === pollOffText ? null : parsePrometheusDuration(v))
}
selectedKey={defaultPollInterval}
title="Refresh Interval"
/>
</div>
<h1 className="co-m-pane__heading">Dashboards</h1>
<div className="monitoring-dashboards__variables">
<VariableDropdown
items={boardItems}
label="Dashboard"
onChange={setBoard}
selectedKey={board}
title="Dashboard"
/>
<AllVariableDropdowns />
<AllVariableDropdowns patchVariable={patchVariable} timespan={timespan} />
</div>
</div>
<Dashboard>
Expand Down Expand Up @@ -381,17 +429,28 @@ type TemplateVariable = {

type Variable = {
options?: string[];
query?: string;
value?: string;
};

type VariablesMap = { [key: string]: Variable };

type VariableDropdownProps = {
buttonClassName?: string;
isError?: boolean;
items: { [key: string]: string };
label: string;
onChange: (v: string) => void;
selectedKey: string;
title: string;
};

type SingleVariableDropdownProps = {
name: string;
options?: string[];
patchVariable: (key: string, patch: Variable) => undefined;
query?: string;
timespan: number;
value?: string;
};

type BoardProps = {
Expand All @@ -403,6 +462,7 @@ type BoardProps = {

type AllVariableDropdownsProps = {
patchVariable: (key: string, patch: Variable) => undefined;
timespan: number;
variables: ImmutableMap<string, Variable>;
};

Expand Down
17 changes: 12 additions & 5 deletions frontend/public/reducers/ui.ts
Expand Up @@ -142,12 +142,19 @@ export default (state: UIState, action: UIAction): UIState => {
case ActionType.MonitoringDashboardsClearVariables:
return state.setIn(['monitoringDashboards', 'variables'], ImmutableMap());

case ActionType.MonitoringDashboardsPatchVariable:
return state.mergeIn(
['monitoringDashboards', 'variables', action.payload.key],
action.payload.patch,
);
case ActionType.MonitoringDashboardsPatchVariable: {
const { key, patch } = action.payload;

// If we don't have a value, but do have options, use the first option as the value
if (
patch.value === undefined &&
patch.options?.length &&
state.getIn(['monitoringDashboards', 'variables', key, 'value']) === undefined
) {
patch.value = patch.options[0];
}
return state.mergeIn(['monitoringDashboards', 'variables', key], patch);
}
case ActionType.SetMonitoringData: {
const alerts =
action.payload.key === 'alerts'
Expand Down