diff --git a/pkg/api/componentreadiness/query/querygenerators.go b/pkg/api/componentreadiness/query/querygenerators.go index 2fa115fc1..f2c64e87b 100644 --- a/pkg/api/componentreadiness/query/querygenerators.go +++ b/pkg/api/componentreadiness/query/querygenerators.go @@ -374,6 +374,15 @@ func BuildComponentReportQuery( Value: reqOptions.Capabilities, }) } + if isSample && len(reqOptions.Lifecycles) > 0 { + // filter by lifecycle : only applied to sample, not basis + // treat NULL or empty lifecycle as "blocking" + queryString += ` AND COALESCE(NULLIF(lifecycle, ''), 'blocking') IN UNNEST(@Lifecycles)` + commonParams = append(commonParams, bigquery.QueryParameter{ + Name: "Lifecycles", + Value: reqOptions.Lifecycles, + }) + } // In this context, a component report, multiple test ID options should not be specified. Thus // here we assume just one for the filtering purposes here. This code triggers as you drill down diff --git a/pkg/api/componentreadiness/utils/queryparamparser.go b/pkg/api/componentreadiness/utils/queryparamparser.go index db7e761ee..f86604f8e 100644 --- a/pkg/api/componentreadiness/utils/queryparamparser.go +++ b/pkg/api/componentreadiness/utils/queryparamparser.go @@ -73,6 +73,7 @@ func ParseComponentReportRequest( // free-form, not "safe" - used in query filters opts.TestFilters.Capabilities = req.URL.Query()["testCapabilities"] + opts.TestFilters.Lifecycles = req.URL.Query()["testLifecycles"] var variantWarnings []string if opts.VariantOption, variantWarnings, err = parseVariantOptions(req, allJobVariants, overrides); err != nil { @@ -159,7 +160,7 @@ func getRequestedView(req *http.Request, views []crview.View) (*crview.View, err "includeVariant", "compareVariant", "variantCrossCompare", // variants "confidence", "pity", "minFail", "passRateNewTests", "passRateAllTests", "ignoreMissing", "ignoreDisruption", // advanced opts - "testCapabilities", // test filters + "testCapabilities", "testLifecycles", // test filters } found := []string{} for _, p := range incompatible { diff --git a/pkg/api/tests.go b/pkg/api/tests.go index 36eca9b1d..a6bc19ded 100644 --- a/pkg/api/tests.go +++ b/pkg/api/tests.go @@ -543,3 +543,36 @@ func GetTestCapabilitiesFromDB(bqClient *bq.Client) ([]string, error) { return row.Capabilities, nil } + +// GetTestLifecyclesFromDB returns a sorted list of lifecycles from the BQ junit table +func GetTestLifecyclesFromDB(bqClient *bq.Client) ([]string, error) { + if bqClient == nil || bqClient.BQ == nil { + return []string{}, nil + } + + // Query recent data (last 7 days) to satisfy partition filter requirement on modified_time + qFmt := `SELECT ARRAY_AGG(DISTINCT lifecycle ORDER BY lifecycle) AS lifecycles + FROM %s.junit + WHERE modified_time >= DATETIME_SUB(CURRENT_DATETIME(), INTERVAL 7 DAY) + AND lifecycle IS NOT NULL AND lifecycle != ''` + q := bqClient.BQ.Query(fmt.Sprintf(qFmt, bqClient.Dataset)) + + log.Infof("Fetching test lifecycles with:\n%s\n", q.Q) + + it, err := q.Read(context.Background()) + if err != nil { + log.WithError(err).Error("error querying test lifecycles from bigquery") + return []string{}, err + } + + var row struct { + Lifecycles []string `bigquery:"lifecycles"` + } + err = it.Next(&row) + if err != nil { + log.WithError(err).Error("error retrieving test lifecycles from bigquery") + return []string{}, errors.Wrap(err, "error retrieving test lifecycles from bigquery") + } + + return row.Lifecycles, nil +} diff --git a/pkg/apis/api/componentreport/reqopts/types.go b/pkg/apis/api/componentreport/reqopts/types.go index 29a5594ce..04567d180 100644 --- a/pkg/apis/api/componentreport/reqopts/types.go +++ b/pkg/apis/api/componentreport/reqopts/types.go @@ -59,7 +59,7 @@ type RelativeRelease struct { // TestFilters are query filters on attributes of the tests themselves, as opposed to the jobs they run in type TestFilters struct { Capabilities []string `json:"capabilities,omitempty" yaml:"capabilities,omitempty"` - // we will likely have more later + Lifecycles []string `json:"lifecycles,omitempty" yaml:"lifecycles,omitempty"` } // TestIdentification handles options used in the test details report when we focus in diff --git a/pkg/sippyserver/server.go b/pkg/sippyserver/server.go index cfe9be144..a50f87262 100644 --- a/pkg/sippyserver/server.go +++ b/pkg/sippyserver/server.go @@ -895,6 +895,17 @@ func (s *Server) jsonTestCapabilitiesFromDB(w http.ResponseWriter, req *http.Req api.RespondWithJSON(http.StatusOK, w, capabilities) } +func (s *Server) jsonTestLifecyclesFromDB(w http.ResponseWriter, req *http.Request) { + lifecycles, err := api.GetTestLifecyclesFromDB(s.bigQueryClient) + if err != nil { + log.WithError(err).Error("error querying test lifecycles") + failureResponse(w, http.StatusInternalServerError, "error querying test lifecycles") + return + } + + api.RespondWithJSON(http.StatusOK, w, lifecycles) +} + func (s *Server) jsonHealthReportFromDB(w http.ResponseWriter, req *http.Request) { release := s.getParamOrFail(w, req, "release") if release != "" { @@ -2150,6 +2161,13 @@ func (s *Server) Serve() { CacheTime: 1 * time.Hour, HandlerFunc: s.jsonTestCapabilitiesFromDB, }, + { + EndpointPath: "/api/tests/lifecycles", + Description: "Returns list of available test lifecycles", + Capabilities: []string{ComponentReadinessCapability}, + CacheTime: 1 * time.Hour, + HandlerFunc: s.jsonTestLifecyclesFromDB, + }, { EndpointPath: "/api/install", Description: "Reports on installations", diff --git a/sippy-ng/src/component_readiness/CompReadyEnvCapabilities.js b/sippy-ng/src/component_readiness/CompReadyEnvCapabilities.js index bbd240fd2..4b807ad0f 100644 --- a/sippy-ng/src/component_readiness/CompReadyEnvCapabilities.js +++ b/sippy-ng/src/component_readiness/CompReadyEnvCapabilities.js @@ -203,7 +203,10 @@ export default function CompReadyEnvCapabilities(props) { return ( - +

diff --git a/sippy-ng/src/component_readiness/CompReadyUtils.js b/sippy-ng/src/component_readiness/CompReadyUtils.js index d5ed37d72..471901312 100644 --- a/sippy-ng/src/component_readiness/CompReadyUtils.js +++ b/sippy-ng/src/component_readiness/CompReadyUtils.js @@ -441,6 +441,9 @@ export function getUpdatedUrlParts(vars) { vars.testCapabilities.forEach((item) => { queryParams.append('testCapabilities', item) }) + vars.testLifecycles.forEach((item) => { + queryParams.append('testLifecycles', item) + }) // Stringify and put the begin param character. queryParams.sort() // ensure they always stay in sorted order to prevent url history changes diff --git a/sippy-ng/src/component_readiness/CompReadyVars.js b/sippy-ng/src/component_readiness/CompReadyVars.js index 0884973fd..f6b1eec2e 100644 --- a/sippy-ng/src/component_readiness/CompReadyVars.js +++ b/sippy-ng/src/component_readiness/CompReadyVars.js @@ -107,6 +107,7 @@ export const CompReadyVarsProvider = ({ children }) => { environment: StringParam, capability: StringParam, testCapabilities: ArrayParam, // Multiple capabilities selected for filtering + testLifecycles: ArrayParam, // Multiple lifecycles selected for filtering testId: StringParam, testName: StringParam, testBasisRelease: StringParam, @@ -224,6 +225,7 @@ export const CompReadyVarsProvider = ({ children }) => { const [environment, setEnvironment] = React.useState(undefined) const [capability, setCapability] = React.useState(undefined) const [testCapabilities, setTestCapabilities] = React.useState([]) + const [testLifecycles, setTestLifecycles] = React.useState([]) const [testId, setTestId] = React.useState(undefined) const [testName, setTestName] = React.useState(undefined) const [testBasisRelease, setTestBasisRelease] = React.useState(undefined) @@ -322,6 +324,7 @@ export const CompReadyVarsProvider = ({ children }) => { setEnvironment(params.environment) setCapability(params.capability) setTestCapabilities(params.testCapabilities || []) + setTestLifecycles(params.testLifecycles || []) setTestId(params.testId) setTestName(params.testName) setTestBasisRelease(params.testBasisRelease) @@ -362,6 +365,7 @@ export const CompReadyVarsProvider = ({ children }) => { environment, capability, testCapabilities, + testLifecycles, testId, testName, testBasisRelease, @@ -611,6 +615,8 @@ export const CompReadyVarsProvider = ({ children }) => { environment, testCapabilities, setTestCapabilities, + testLifecycles, + setTestLifecycles, testId, testName, testBasisRelease, diff --git a/sippy-ng/src/component_readiness/ComponentReadiness.js b/sippy-ng/src/component_readiness/ComponentReadiness.js index 7038f6d41..15b50a509 100644 --- a/sippy-ng/src/component_readiness/ComponentReadiness.js +++ b/sippy-ng/src/component_readiness/ComponentReadiness.js @@ -183,6 +183,7 @@ function TriageWrapper() { export const ComponentReadinessStyleContext = React.createContext({}) export const TestCapabilitiesContext = React.createContext([]) +export const TestLifecyclesContext = React.createContext([]) // Big query requests take a while so give the user the option to // abort in case they inadvertently requested a huge dataset. @@ -434,8 +435,29 @@ export default function ComponentReadiness(props) { }) } + const [testLifecycles, setTestLifecycles] = React.useState([]) + function fetchLifecycles() { + fetch(process.env.REACT_APP_API_URL + '/api/tests/lifecycles') + .then((response) => { + if (response.status !== 200) { + throw new Error('server returned ' + response.status) + } + return response.json() + }) + .then((lifecycles) => { + // Ensure we always set an array + setTestLifecycles(Array.isArray(lifecycles) ? lifecycles : []) + }) + .catch((error) => { + // Don't fail the whole page for lifecycle fetch errors, just log it + console.error('could not retrieve lifecycles:', error) + setTestLifecycles([]) + }) + } + useEffect(() => { fetchCapabilities() + fetchLifecycles() setIsLoaded(false) if ( location.pathname.endsWith('/component_readiness/main') || @@ -465,265 +487,278 @@ export default function ComponentReadiness(props) { return ( - - - {isMainRoute && } - {/* eslint-disable react/prop-types */} - - } /> - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - } /> - } /> - - - + + + {isMainRoute && } + {/* eslint-disable react/prop-types */} + + } /> + } + /> + - {data === initialPageTable ? ( - - To get started, make your filter selections on the left, - left, then click Generate Report - - ) : ( -
- - - - - - - - Name - - - {columnNames - .filter( - (column, idx) => - column.match( - new RegExp( - escapeRegex(searchColumnRegex), - 'i' - ) - ) && keepColumnsList[idx] - ) - .map((column, idx) => { - if (column !== 'Name') { - return ( - - + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + } /> + } /> + + + + {data === initialPageTable ? ( + + To get started, make your filter selections on the left, + left, then click Generate Report + + ) : ( +
+ + +
+ + + + + Name + + + {columnNames + .filter( + (column, idx) => + column.match( + new RegExp( + escapeRegex(searchColumnRegex), + 'i' + ) + ) && keepColumnsList[idx] + ) + .map((column, idx) => { + if (column !== 'Name') { + return ( + - - {' '} - {column} - - - - ) - } - })} - - - - {data.rows - ? Object.keys(data.rows) - .filter((componentIndex) => - data.rows[componentIndex].component.match( - new RegExp( - escapeRegex(searchRowRegex), - 'i' + + {' '} + {column} + + + + ) + } + })} + + + + {data.rows + ? Object.keys(data.rows) + .filter((componentIndex) => + data.rows[componentIndex].component.match( + new RegExp( + escapeRegex(searchRowRegex), + 'i' + ) ) ) - ) - .filter((componentIndex) => - redOnlyChecked - ? data.rows[componentIndex].columns.some( - // Filter for rows where any of their columns have status <= -2 and accepted by the regex. - (column) => - column.status <= -2 && + .filter((componentIndex) => + redOnlyChecked + ? data.rows[ + componentIndex + ].columns.some( + // Filter for rows where any of their columns have status <= -2 and accepted by the regex. + (column) => + column.status <= -2 && + formColumnName(column).match( + new RegExp( + escapeRegex( + searchColumnRegex + ), + 'i' + ) + ) + ) + : true + ) + .map((componentIndex) => ( + formColumnName(column).match( new RegExp( escapeRegex(searchColumnRegex), 'i' ) - ) - ) - : true - ) - .map((componentIndex) => ( - - formColumnName(column).match( - new RegExp( - escapeRegex(searchColumnRegex), - 'i' - ) - ) && - keepColumnsList && - keepColumnsList[idx] - )} - columnNames={columnNames.filter( - (column, idx) => - column.match( - new RegExp( - escapeRegex(searchColumnRegex), - 'i' - ) - ) && - keepColumnsList && - keepColumnsList[idx] - )} - grayFactor={redOnlyChecked ? 100 : 0} - filterVals={getUpdatedUrlParts( - varsContext - )} - /> - )) - : null} - -
-
- - -
- )} - - } - /> -
-
+ ) && + keepColumnsList && + keepColumnsList[idx] + )} + columnNames={columnNames.filter( + (column, idx) => + column.match( + new RegExp( + escapeRegex(searchColumnRegex), + 'i' + ) + ) && + keepColumnsList && + keepColumnsList[idx] + )} + grayFactor={redOnlyChecked ? 100 : 0} + filterVals={getUpdatedUrlParts( + varsContext + )} + /> + )) + : null} + + + + + + + )} + + } + /> +
+
+
) diff --git a/sippy-ng/src/component_readiness/SidebarTestFilters.js b/sippy-ng/src/component_readiness/SidebarTestFilters.js index 97fef8793..da2bb8d00 100644 --- a/sippy-ng/src/component_readiness/SidebarTestFilters.js +++ b/sippy-ng/src/component_readiness/SidebarTestFilters.js @@ -12,18 +12,25 @@ import { import { CompReadyVarsContext } from './CompReadyVars' import { ExpandMore } from '@mui/icons-material' import { makeStyles } from '@mui/styles' -import { TestCapabilitiesContext } from './ComponentReadiness' +import { + TestCapabilitiesContext, + TestLifecyclesContext, +} from './ComponentReadiness' import PropTypes from 'prop-types' import React, { Fragment, useContext } from 'react' import Typography from '@mui/material/Typography' export default function SidebarTestFilters(props) { - if (!props.controlsOpts?.filterByCapabilities) { - // if we have no filters to show, omit the whole component; for now we only have capabilities as a filter + if ( + !props.controlsOpts?.filterByCapabilities && + !props.controlsOpts?.filterByLifecycles + ) { + // if we have no filters to show, omit the whole component return } const varsContext = useContext(CompReadyVarsContext) const testCapabilities = useContext(TestCapabilitiesContext) + const testLifecycles = useContext(TestLifecyclesContext) const useStyles = makeStyles((theme) => ({ formControl: { margin: theme.spacing(1), @@ -43,10 +50,14 @@ export default function SidebarTestFilters(props) { const classes = useStyles() - const handleChange = (event, newValue) => { + const handleCapabilitiesChange = (event, newValue) => { varsContext.setTestCapabilities(newValue || []) } + const handleLifecyclesChange = (event, newValue) => { + varsContext.setTestLifecycles(newValue || []) + } + return ( (
  • )} + {props.controlsOpts?.filterByLifecycles && ( + ( +
  • + {option} +
  • + )} + renderTags={(value, getTagProps) => + value.map((option, index) => ( + + + + )) + } + renderInput={(params) => ( + + )} + /> + )}