diff --git a/pkg/handler/response.go b/pkg/handler/response.go index 6abbb219e..f40801b58 100644 --- a/pkg/handler/response.go +++ b/pkg/handler/response.go @@ -11,7 +11,11 @@ import ( "github.com/netobserv/network-observability-console-plugin/pkg/model" ) -const codePrometheusUnsupported = 901 // code to use internally to notify a Bad Request, unsupported for prometheus queries +const ( + codePrometheusUnsupported = 901 // code to use internally to notify a Bad Request, unsupported for prometheus queries + codePrometheusDisabledMetrics = 902 // code to use internally to notify a Bad Request, disabled metrics for prometheus queries + codePrometheusMissingLabels = 903 // code to use internally to notify a Bad Request, missing labels for prometheus queries +) func writeText(w http.ResponseWriter, code int, bytes []byte) { w.Header().Set("Content-Type", "text/plain") @@ -65,16 +69,25 @@ func writeCSV(w http.ResponseWriter, code int, qr *model.AggregatedQueryResponse } type errorResponse struct { - Message string `json:"message,omitempty"` - PromUnsupported string `json:"promUnsupported,omitempty"` + Message string `json:"message,omitempty"` + PromUnsupported string `json:"promUnsupported,omitempty"` + PromDisabledMetrics string `json:"promDisabledMetrics,omitempty"` + PromMissingLabels string `json:"promMissingLabels,omitempty"` } func writeError(w http.ResponseWriter, code int, message string) { var resp errorResponse - if code == codePrometheusUnsupported { + switch code { + case codePrometheusUnsupported: code = http.StatusBadRequest resp = errorResponse{PromUnsupported: message} - } else { + case codePrometheusDisabledMetrics: + code = http.StatusBadRequest + resp = errorResponse{PromDisabledMetrics: message} + case codePrometheusMissingLabels: + code = http.StatusBadRequest + resp = errorResponse{PromMissingLabels: message} + default: resp = errorResponse{Message: message} } response, err := json.Marshal(resp) diff --git a/pkg/handler/topology.go b/pkg/handler/topology.go index 7843ffc3e..f1a2e850a 100644 --- a/pkg/handler/topology.go +++ b/pkg/handler/topology.go @@ -291,12 +291,12 @@ func buildTopologyQuery( if search != nil { if len(search.Candidates) > 0 { // Some candidate metrics exist but they are disabled; tell the user - return "", nil, codePrometheusUnsupported, fmt.Errorf( + return "", nil, codePrometheusDisabledMetrics, fmt.Errorf( "this request requires any of the following metric(s) to be enabled: %s."+ " Metrics can be configured in the FlowCollector resource via 'spec.processor.metrics.includeList'."+ " Alternatively, you may also install and enable Loki", search.FormatCandidates()) } else if len(search.MissingLabels) > 0 { - return "", nil, codePrometheusUnsupported, fmt.Errorf( + return "", nil, codePrometheusMissingLabels, fmt.Errorf( "this request could not be performed with Prometheus metrics, as they are missing some of the required labels."+ " Try using different filters and/or aggregations. For example, try removing these dependencies from your query: %s."+ " Alternatively, you may also install and enable Loki", search.FormatMissingLabels()) diff --git a/web/locales/en/plugin__netobserv-plugin.json b/web/locales/en/plugin__netobserv-plugin.json index 8462b37e7..75d597d54 100644 --- a/web/locales/en/plugin__netobserv-plugin.json +++ b/web/locales/en/plugin__netobserv-plugin.json @@ -361,6 +361,7 @@ "Show filters": "Show filters", "Collapse": "Collapse", "Expand": "Expand", + "Some filters have been automatically disabled": "Some filters have been automatically disabled", "Equals": "Equals", "Not equals": "Not equals", "More than": "More than", diff --git a/web/src/components/drawer/netflow-traffic-drawer.tsx b/web/src/components/drawer/netflow-traffic-drawer.tsx index 6ca7ef358..5d405ad22 100644 --- a/web/src/components/drawer/netflow-traffic-drawer.tsx +++ b/web/src/components/drawer/netflow-traffic-drawer.tsx @@ -20,7 +20,7 @@ import { FlowScope, Match, MetricType, RecordType, StatFunction } from '../../mo import { ScopeConfigDef } from '../../model/scope'; import { Warning } from '../../model/warnings'; import { Column, ColumnSizeMap } from '../../utils/columns'; -import { isPromUnsupportedError } from '../../utils/errors'; +import { isPromError } from '../../utils/errors'; import { OverviewPanel } from '../../utils/overview-panels'; import { TruncateLength } from '../dropdowns/truncate-dropdown'; import { Error, Size } from '../messages/error'; @@ -239,7 +239,7 @@ export const NetflowTrafficDrawer: React.FC = React.f item: props.currentState.includes('configLoadError') ? t('config') : props.selectedViewId })} error={props.error} - isLokiRelated={!props.currentState.includes('configLoadError') && !isPromUnsupportedError(props.error)} + isLokiRelated={!props.currentState.includes('configLoadError') && !isPromError(props.error)} /> ); } else { diff --git a/web/src/components/messages/error.tsx b/web/src/components/messages/error.tsx index 836b2ded4..5b82ed2d9 100644 --- a/web/src/components/messages/error.tsx +++ b/web/src/components/messages/error.tsx @@ -21,7 +21,7 @@ import { Link } from 'react-router-dom'; import { Status } from '../../api/loki'; import { getBuildInfo, getLimits, getLokiMetrics, getStatus } from '../../api/routes'; import { ContextSingleton } from '../../utils/context'; -import { getHTTPErrorDetails, getPromUnsupportedError, isPromUnsupportedError } from '../../utils/errors'; +import { getHTTPErrorDetails, getPromError, isPromError } from '../../utils/errors'; import './error.css'; import { SecondaryAction } from './secondary-action'; import { StatusTexts } from './status-texts'; @@ -102,7 +102,7 @@ export const Error: React.FC = ({ title, error, isLokiRelated }) => ); const getDisplayError = React.useCallback(() => { - return isPromUnsupportedError(error) ? getPromUnsupportedError(error) : error; + return isPromError(error) ? getPromError(error) : error; }, [error]); React.useEffect(() => { diff --git a/web/src/components/netflow-traffic.css b/web/src/components/netflow-traffic.css index ad1062506..03a191a7c 100644 --- a/web/src/components/netflow-traffic.css +++ b/web/src/components/netflow-traffic.css @@ -245,4 +245,8 @@ span.pf-c-button__icon.pf-m-start { .netobserv-tab-container { margin-top: 1.5rem; +} + +.chips-popover-close-button { + padding: 0 !important; } \ No newline at end of file diff --git a/web/src/components/netflow-traffic.tsx b/web/src/components/netflow-traffic.tsx index 0342ce4ee..8acb0833b 100644 --- a/web/src/components/netflow-traffic.tsx +++ b/web/src/components/netflow-traffic.tsx @@ -15,7 +15,7 @@ import { ColumnsId, getDefaultColumns } from '../utils/columns'; import { loadConfig } from '../utils/config'; import { ContextSingleton } from '../utils/context'; import { computeStepInterval } from '../utils/datetime'; -import { getHTTPErrorDetails } from '../utils/errors'; +import { getHTTPErrorDetails, getPromError, isPromMissingLabelError } from '../utils/errors'; import { checkFilterAvailable, getFilterDefinitions } from '../utils/filter-definitions'; import { defaultArraySelectionOptions, @@ -55,6 +55,7 @@ import './netflow-traffic.css'; import { SearchHandle } from './search/search'; import TabsContainer from './tabs/tabs-container'; import { FiltersToolbar } from './toolbar/filters-toolbar'; +import ChipsPopover from './toolbar/filters/chips-popover'; import HistogramToolbar from './toolbar/histogram-toolbar'; import ViewOptionsToolbar from './toolbar/view-options-toolbar'; @@ -453,10 +454,37 @@ export const NetflowTraffic: React.FC = ({ model.setStats(stats); }) .catch(err => { + const errStr = getHTTPErrorDetails(err, true); + const promErrStr = getPromError(errStr); + + // check if it's a prom missing label error and remove filters + // when the prom error is different to the new one + if (isPromMissingLabelError(errStr) && promErrStr !== model.chipsPopoverMessage) { + let filtersDisabled = false; + model.filters.list.forEach(filter => { + const fieldName = model.config.columns.find(col => col.filter === filter.def.id)?.field; + if (!fieldName || errStr.includes(fieldName)) { + filtersDisabled = true; + filter.values.forEach(fv => { + fv.disabled = true; + }); + } + }); + if (filtersDisabled) { + // update filters to retrigger query without showing the error + updateTableFilters({ ...model.filters }); + model.setChipsPopoverMessage(promErrStr); + return; + } + } + + // clear flows and metrics + show error + // always clear chip message to focus on the error model.setFlows([]); model.setMetrics(defaultNetflowMetrics); - model.setError(getHTTPErrorDetails(err, true)); + model.setError(errStr); model.setWarning(undefined); + model.setChipsPopoverMessage(undefined); }) .finally(() => { const endDate = new Date(); @@ -465,6 +493,9 @@ export const NetflowTraffic: React.FC = ({ model.setLastDuration(endDate.getTime() - startDate.getTime()); }) ); + } else if (model.error) { + // recall tick after drawer rendering to ensure query is properly loaded + setTimeout(tick); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ @@ -913,6 +944,10 @@ export const NetflowTraffic: React.FC = ({ /> )} + ) : null; }; diff --git a/web/src/components/tabs/netflow-topology/netflow-topology.tsx b/web/src/components/tabs/netflow-topology/netflow-topology.tsx index 8268288ef..bca2c92bd 100644 --- a/web/src/components/tabs/netflow-topology/netflow-topology.tsx +++ b/web/src/components/tabs/netflow-topology/netflow-topology.tsx @@ -28,7 +28,7 @@ import { ScopeConfigDef } from '../../../model/scope'; import { GraphElementPeer, LayoutName, TopologyOptions } from '../../../model/topology'; import { Warning } from '../../../model/warnings'; import { TimeRange } from '../../../utils/datetime'; -import { getHTTPErrorDetails, getPromUnsupportedError, isPromUnsupportedError } from '../../../utils/errors'; +import { getHTTPErrorDetails, getPromError, isPromError } from '../../../utils/errors'; import { observeDOMRect } from '../../../utils/metrics-helper'; import { SearchEvent, SearchHandle } from '../../search/search'; import { ScopeSlider } from '../../slider/scope-slider'; @@ -162,8 +162,8 @@ export const NetflowTopology: React.FC = React.forwardRef( // Error might occur for instance when fetching node-based topology with drop feature enabled, and Loki disabled // We don't want to break the whole topology due to missing drops enrichement let strErr = getHTTPErrorDetails(err, true); - if (isPromUnsupportedError(strErr)) { - strErr = getPromUnsupportedError(strErr); + if (isPromError(strErr)) { + strErr = getPromError(strErr); } setWarning({ type: 'cantfetchdrops', diff --git a/web/src/components/toolbar/filters/chips-popover.tsx b/web/src/components/toolbar/filters/chips-popover.tsx new file mode 100644 index 000000000..f5ec36bf6 --- /dev/null +++ b/web/src/components/toolbar/filters/chips-popover.tsx @@ -0,0 +1,41 @@ +import { Button, Flex, FlexItem, Popover, Text } from '@patternfly/react-core'; +import { TimesIcon } from '@patternfly/react-icons'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +export interface ChipsPopoverProps { + chipsPopoverMessage?: string; + setChipsPopoverMessage: (v?: string) => void; +} + +export const ChipsPopover: React.FC = ({ chipsPopoverMessage, setChipsPopoverMessage }) => { + const { t } = useTranslation('plugin__netobserv-plugin'); + + return ( + + {t('Some filters have been automatically disabled')} + + + + + } + bodyContent={ {chipsPopoverMessage}} + reference={() => document.getElementsByClassName('custom-chip-group disabled-group')?.[0] as HTMLElement} + /> + ); +}; + +export default ChipsPopover; diff --git a/web/src/model/netflow-traffic.ts b/web/src/model/netflow-traffic.ts index 7b35ed3ee..24c032e8e 100644 --- a/web/src/model/netflow-traffic.ts +++ b/web/src/model/netflow-traffic.ts @@ -122,6 +122,7 @@ export function netflowTrafficModel() { const [isShowQuerySummary, setShowQuerySummary] = React.useState(false); const [lastRefresh, setLastRefresh] = React.useState(undefined); const [lastDuration, setLastDuration] = React.useState(undefined); + const [chipsPopoverMessage, setChipsPopoverMessage] = React.useState(); const [error, setError] = React.useState(); const [isTRModalOpen, setTRModalOpen] = React.useState(false); const [isOverviewModalOpen, setOverviewModalOpen] = React.useState(false); @@ -241,6 +242,8 @@ export function netflowTrafficModel() { setLastRefresh, lastDuration, setLastDuration, + chipsPopoverMessage, + setChipsPopoverMessage, error, setError, isTRModalOpen, diff --git a/web/src/utils/errors.ts b/web/src/utils/errors.ts index 01c334e74..d385d258b 100644 --- a/web/src/utils/errors.ts +++ b/web/src/utils/errors.ts @@ -1,10 +1,16 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const getHTTPErrorDetails = (err: any, checkPromUnsupported = false) => { +export const getHTTPErrorDetails = (err: any, checkPromErrors = false) => { if (err?.response?.data) { const header = err.toString === Object.prototype.toString ? '' : `${err}\n`; if (typeof err.response.data === 'object') { - if (checkPromUnsupported && err.response.data.promUnsupported) { - return 'promUnsupported:' + String(err.response.data.promUnsupported); + if (checkPromErrors) { + if (err.response.data.promUnsupported) { + return 'promUnsupported:' + String(err.response.data.promUnsupported); + } else if (err.response.data.promDisabledMetrics) { + return 'promDisabledMetrics:' + String(err.response.data.promDisabledMetrics); + } else if (err.response.data.promMissingLabels) { + return 'promMissingLabels:' + String(err.response.data.promMissingLabels); + } } return ( header + @@ -22,6 +28,26 @@ export const isPromUnsupportedError = (err: string) => { return err.startsWith('promUnsupported:'); }; -export const getPromUnsupportedError = (err: string) => { - return err.substring('promUnsupported:'.length); +export const isPromDisabledMetricsError = (err: string) => { + return err.startsWith('promDisabledMetrics:'); +}; + +export const isPromMissingLabelError = (err: string) => { + return err.startsWith('promMissingLabels:'); +}; + +export const isPromError = (err: string) => { + return isPromUnsupportedError(err) || isPromDisabledMetricsError(err) || isPromMissingLabelError(err); +}; + +export const getPromError = (err: string) => { + if (isPromUnsupportedError(err)) { + return err.substring('promUnsupported:'.length); + } else if (isPromDisabledMetricsError(err)) { + return err.substring('promDisabledMetrics:'.length); + } else if (isPromMissingLabelError(err)) { + return err.substring('promMissingLabels:'.length); + } else { + return err; + } };