From 811082c6b39c752c1683b09fa8e664d78dec992e Mon Sep 17 00:00:00 2001 From: Devan Goodwin Date: Tue, 14 Jul 2026 13:29:03 -0300 Subject: [PATCH 1/8] Fix hook ordering --- sippy-ng/src/App.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/sippy-ng/src/App.js b/sippy-ng/src/App.js index 6f1a4ab94..e7405ae9c 100644 --- a/sippy-ng/src/App.js +++ b/sippy-ng/src/App.js @@ -260,12 +260,23 @@ const JobsWrapper = () => { } const FeatureGateDetailWrapper = () => { - let { release, feature_gate } = useParams() + const { release, feature_gate } = useParams() const navigate = useNavigate() + const releases = React.useContext(ReleasesContext) + + const effectiveRelease = + release === 'latest' ? findFirstNonGARelease(releases) : release + + React.useEffect(() => { + if (release === 'latest' && effectiveRelease) { + navigate(`/feature_gates/${effectiveRelease}/${feature_gate}`, { + replace: true, + }) + } + }, [release, effectiveRelease, feature_gate, navigate]) if (release === 'latest') { - release = findFirstNonGARelease(React.useContext(ReleasesContext)) - navigate(`/feature_gates/${release}/${feature_gate}`, { replace: true }) + return null } return ( From 249eaba834f722ed3ff0e153667792df0a9fe3b0 Mon Sep 17 00:00:00 2001 From: Devan Goodwin Date: Tue, 14 Jul 2026 13:29:13 -0300 Subject: [PATCH 2/8] Fix ui_details links for feature gates --- pkg/sippyserver/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/sippyserver/server.go b/pkg/sippyserver/server.go index a25e7b2da..e168b55f8 100644 --- a/pkg/sippyserver/server.go +++ b/pkg/sippyserver/server.go @@ -760,7 +760,7 @@ func injectFeatureGateHATEOASLinks(fg *apitype.FeatureGate, release, baseAPIURL, fg.Links["tests_by_capability"] = buildFilteredTestsURL(baseAPIURL, release, capabilityFilter) fg.Links["ui_detail"] = fmt.Sprintf( - "%s/feature_gates/%s/%s", + "%s/sippy-ng/feature_gates/%s/%s", baseFrontendURL, release, url.PathEscape(fg.FeatureGate)) } From 49c3856b954b36772aee9628e169d986c661f53e Mon Sep 17 00:00:00 2001 From: Devan Goodwin Date: Wed, 15 Jul 2026 10:44:18 -0300 Subject: [PATCH 3/8] Continue using install success for install feature gates We want to eventually move these to using tests should all work, but that requires these jobs to move to more minimal test suites that do not yet exist --- cmd/sippy/seed_data.go | 17 +++++++++++++++ pkg/sippyserver/server.go | 9 +++++++- sippy-ng/src/tests/FeatureGates.js | 4 +++- test/e2e/feature_gates_test.go | 35 +++++++++++++++++++++++++++++- 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/cmd/sippy/seed_data.go b/cmd/sippy/seed_data.go index 2d438af6d..f4c319bfd 100644 --- a/cmd/sippy/seed_data.go +++ b/cmd/sippy/seed_data.go @@ -185,12 +185,22 @@ var syntheticJobs = []syntheticJobDef{ "Capability": "NetworkSegmentation", }, }, + { + nameTemplate: "periodic-ci-openshift-release-master-ci-%s-e2e-aws-ovn-amd64-capability-awsdualstackinstall", + variants: map[string]string{ + "Platform": "aws", "Architecture": "amd64", "Network": "ovn", + "Topology": "ha", "Installer": "ipi", "FeatureSet": "default", + "Suite": "parallel", "Upgrade": "none", "LayeredProduct": "none", + "Capability": "AWSDualStackInstall", + }, + }, } // Job template constants for referencing specific jobs in test specs. const awsAmd64Parallel = "periodic-ci-openshift-release-master-ci-%s-e2e-aws-ovn-amd64" const awsArm64Parallel = "periodic-ci-openshift-release-master-ci-%s-e2e-aws-ovn-arm64" const gcpAmd64Parallel = "periodic-ci-openshift-release-master-ci-%s-e2e-gcp-ovn-amd64" +const awsAmd64CapabilityAWSDualStackInstall = "periodic-ci-openshift-release-master-ci-%s-e2e-aws-ovn-amd64-capability-awsdualstackinstall" // allJobTemplates returns name templates from syntheticJobs for use in test specs // that should run on every job (e.g. install tests). @@ -363,6 +373,13 @@ var syntheticTests = []syntheticTestSpec{ awsAmd64Parallel: {"4.22": {50, 48, 0}}, }, }, + { + testID: "test-cap-aws-dual-stack-install", testName: "install should succeed: overall", + component: "Installer / openshift-installer", capabilities: []string{"install"}, + jobCounts: map[string]map[string]testCount{ + awsAmd64CapabilityAWSDualStackInstall: {"4.22": {50, 47, 0}}, + }, + }, // --- Install / health indicator tests: run on every job, every release --- { testID: "test-install-overall", testName: "install should succeed: overall", diff --git a/pkg/sippyserver/server.go b/pkg/sippyserver/server.go index e168b55f8..ff1065436 100644 --- a/pkg/sippyserver/server.go +++ b/pkg/sippyserver/server.go @@ -750,9 +750,16 @@ func injectFeatureGateHATEOASLinks(fg *apitype.FeatureGate, release, baseAPIURL, } fg.Links["tests_by_annotation"] = buildFilteredTestsURL(baseAPIURL, release, annotationFilter) + // Installer gates currently run a broad conformance suite where full passes aren't + // required, so "install should succeed" is the meaningful signal. Switch to + // "openshift-tests should work" once installer jobs run a minimal conformance suite. + capabilityTestName := "openshift-tests should work" + if strings.Contains(fg.FeatureGate, "Install") { + capabilityTestName = "install should succeed" + } capabilityFilter := filter.Filter{ Items: []filter.FilterItem{ - {Field: "name", Operator: filter.OperatorContains, Value: "openshift-tests should work"}, + {Field: "name", Operator: filter.OperatorContains, Value: capabilityTestName}, {Field: "variants", Operator: filter.OperatorContains, Value: fmt.Sprintf("Capability:%s", fg.FeatureGate)}, }, LinkOperator: filter.LinkOperatorAnd, diff --git a/sippy-ng/src/tests/FeatureGates.js b/sippy-ng/src/tests/FeatureGates.js index c366d2f74..f4f6370ff 100644 --- a/sippy-ng/src/tests/FeatureGates.js +++ b/sippy-ng/src/tests/FeatureGates.js @@ -296,7 +296,9 @@ export default function FeatureGates(props) { if (params.row.links && params.row.links.ui_detail) { try { const url = new URL(params.row.links.ui_detail) - return url.pathname + const basename = '/sippy-ng' + const path = url.pathname + return path.startsWith(basename) ? path.slice(basename.length) : path } catch (e) { // fall through } diff --git a/test/e2e/feature_gates_test.go b/test/e2e/feature_gates_test.go index f622bbb5f..68238f165 100644 --- a/test/e2e/feature_gates_test.go +++ b/test/e2e/feature_gates_test.go @@ -1,6 +1,7 @@ package e2e import ( + "strings" "testing" "github.com/openshift/sippy/pkg/apis/api" @@ -54,8 +55,14 @@ func TestFeatureGatesHATEOASLinks(t *testing.T) { testsCapability, ok := fg.Links["tests_by_capability"] assert.True(t, ok, "missing tests_by_capability link") assert.Contains(t, testsCapability, "/api/tests?release="+util.Release) - assert.Contains(t, testsCapability, "openshift-tests+should+work") assert.Contains(t, testsCapability, "Capability%3A"+fg.FeatureGate) + if strings.Contains(fg.FeatureGate, "Install") { + assert.Contains(t, testsCapability, "install+should+succeed", + "Install gates should use install test for capability link") + } else { + assert.Contains(t, testsCapability, "openshift-tests+should+work", + "non-Install gates should use openshift-tests for capability link") + } uiDetail, ok := fg.Links["ui_detail"] assert.True(t, ok, "missing ui_detail link") @@ -114,6 +121,32 @@ func TestFeatureGatesCapabilityLinkFollowable(t *testing.T) { } } +func TestFeatureGatesInstallCapabilityLinkFollowable(t *testing.T) { + var gates []api.FeatureGate + err := util.SippyGet("/api/feature_gates?release="+util.Release, &gates) + require.NoError(t, err) + + gatesByName := make(map[string]api.FeatureGate) + for _, g := range gates { + gatesByName[g.FeatureGate] = g + } + + fg, ok := gatesByName["AWSDualStackInstall"] + require.True(t, ok, "AWSDualStackInstall not found") + + link := fg.Links["tests_by_capability"] + require.NotEmpty(t, link) + + var tests []api.Test + err = util.SippyGetAbsolute(link, &tests) + require.NoError(t, err, "failed to follow tests_by_capability link") + assert.Greater(t, len(tests), 0, "expected tests when following tests_by_capability link for AWSDualStackInstall") + for _, test := range tests { + assert.Contains(t, test.Name, "install should succeed", + "install gate capability tests should match install tests") + } +} + func TestFeatureGatesFilterByName(t *testing.T) { var gates []api.FeatureGate filterJSON := `{"items":[{"columnField":"feature_gate","operatorValue":"equals","value":"NetworkSegmentation"}]}` From 1e48e685ce57932dbe6c410d9ccdafb87c53755a Mon Sep 17 00:00:00 2001 From: Devan Goodwin Date: Wed, 15 Jul 2026 10:49:58 -0300 Subject: [PATCH 4/8] Fix clickthrough on test details from fg details page --- sippy-ng/src/tests/FeatureGateDetail.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/sippy-ng/src/tests/FeatureGateDetail.js b/sippy-ng/src/tests/FeatureGateDetail.js index cb2e22321..08e44a998 100644 --- a/sippy-ng/src/tests/FeatureGateDetail.js +++ b/sippy-ng/src/tests/FeatureGateDetail.js @@ -10,14 +10,14 @@ import { Typography, } from '@mui/material' import { DataGrid } from '@mui/x-data-grid' +import { filterFor, multiple, safeEncodeURIComponent } from '../helpers' import { Link } from 'react-router-dom' -import { pathForTestByVariant, safeEncodeURIComponent } from '../helpers' import Alert from '@mui/material/Alert' import PropTypes from 'prop-types' import React, { Fragment, useEffect } from 'react' import SimpleBreadcrumbs from '../components/SimpleBreadcrumbs' -function TestResultsSection({ title, apiUrl, release }) { +function TestResultsSection({ title, apiUrl, release, extraFilters = [] }) { const [rows, setRows] = React.useState([]) const [isLoaded, setLoaded] = React.useState(false) const [fetchError, setFetchError] = React.useState('') @@ -56,11 +56,12 @@ function TestResultsSection({ title, apiUrl, release }) { field: 'name', headerName: 'Test Name', flex: 4, - renderCell: (params) => ( - - {params.value} - - ), + renderCell: (params) => { + const path = + `/tests/${release}/details?` + + multiple(filterFor('name', 'equals', params.value), ...extraFilters) + return {params.value} + }, }, { field: 'current_successes', @@ -126,6 +127,7 @@ TestResultsSection.propTypes = { title: PropTypes.string.isRequired, apiUrl: PropTypes.string, release: PropTypes.string.isRequired, + extraFilters: PropTypes.array, } export default function FeatureGateDetail(props) { @@ -266,6 +268,13 @@ export default function FeatureGateDetail(props) { title="Tests matching capability variant" apiUrl={gate.links.tests_by_capability} release={release} + extraFilters={[ + filterFor( + 'variants', + 'has entry containing', + `Capability:${featureGate}` + ), + ]} /> )} From c066822435b015fcf0e67e84e34f0adf098c6053 Mon Sep 17 00:00:00 2001 From: Devan Goodwin Date: Wed, 15 Jul 2026 10:55:43 -0300 Subject: [PATCH 5/8] Re-use test details table on feature gate details page --- sippy-ng/src/tests/FeatureGateDetail.js | 176 +++++++----------------- 1 file changed, 46 insertions(+), 130 deletions(-) diff --git a/sippy-ng/src/tests/FeatureGateDetail.js b/sippy-ng/src/tests/FeatureGateDetail.js index 08e44a998..e1936452d 100644 --- a/sippy-ng/src/tests/FeatureGateDetail.js +++ b/sippy-ng/src/tests/FeatureGateDetail.js @@ -9,126 +9,13 @@ import { Tabs, Typography, } from '@mui/material' -import { DataGrid } from '@mui/x-data-grid' -import { filterFor, multiple, safeEncodeURIComponent } from '../helpers' import { Link } from 'react-router-dom' +import { safeEncodeURIComponent } from '../helpers' import Alert from '@mui/material/Alert' import PropTypes from 'prop-types' import React, { Fragment, useEffect } from 'react' import SimpleBreadcrumbs from '../components/SimpleBreadcrumbs' - -function TestResultsSection({ title, apiUrl, release, extraFilters = [] }) { - const [rows, setRows] = React.useState([]) - const [isLoaded, setLoaded] = React.useState(false) - const [fetchError, setFetchError] = React.useState('') - const [sortModel, setSortModel] = React.useState([ - { field: 'current_pass_percentage', sort: 'asc' }, - ]) - - useEffect(() => { - if (!apiUrl) { - setRows([]) - setLoaded(true) - return - } - setLoaded(false) - setFetchError('') - - fetch(apiUrl) - .then((response) => { - if (response.status !== 200) { - throw new Error('server returned ' + response.status) - } - return response.json() - }) - .then((json) => { - setRows(json || []) - setLoaded(true) - }) - .catch((error) => { - setFetchError('Could not retrieve ' + title + ': ' + error) - setLoaded(true) - }) - }, [apiUrl, title]) - - const columns = [ - { - field: 'name', - headerName: 'Test Name', - flex: 4, - renderCell: (params) => { - const path = - `/tests/${release}/details?` + - multiple(filterFor('name', 'equals', params.value), ...extraFilters) - return {params.value} - }, - }, - { - field: 'current_successes', - headerName: 'Current Successes', - type: 'number', - flex: 1, - }, - { - field: 'current_failures', - headerName: 'Current Failures', - type: 'number', - flex: 1, - }, - { - field: 'current_flakes', - headerName: 'Current Flakes', - type: 'number', - flex: 1, - }, - { - field: 'current_pass_percentage', - headerName: 'Current Pass %', - type: 'number', - flex: 1, - renderCell: (params) => - params.value != null ? params.value.toFixed(2) + '%' : '', - }, - { - field: 'current_runs', - headerName: 'Current Runs', - type: 'number', - flex: 1, - }, - ] - - if (fetchError) { - return {fetchError} - } - - return ( - - - {title} ({isLoaded ? rows.length : '...'} tests) - - row.name} - getRowHeight={() => 'auto'} - autoHeight={true} - rowsPerPageOptions={[10, 25, 50]} - pageSize={25} - sortModel={sortModel} - onSortModelChange={setSortModel} - disableSelectionOnClick - /> - - ) -} - -TestResultsSection.propTypes = { - title: PropTypes.string.isRequired, - apiUrl: PropTypes.string, - release: PropTypes.string.isRequired, - extraFilters: PropTypes.array, -} +import TestTable from './TestTable' export default function FeatureGateDetail(props) { const { release, featureGate } = props @@ -180,6 +67,40 @@ export default function FeatureGateDetail(props) { }) }, [release, featureGate]) + const annotationFilter = { + items: [ + { + columnField: 'name', + operatorValue: 'contains', + value: `FeatureGate:${featureGate}]`, + }, + ], + } + + // Installer gates currently run a broad conformance suite where full passes + // aren't required, so "install should succeed" is the meaningful signal. + // Switch to "openshift-tests should work" once installer jobs run a minimal + // conformance suite. + const capabilityTestName = featureGate.includes('Install') + ? 'install should succeed' + : 'openshift-tests should work' + + const capabilityFilter = { + items: [ + { + columnField: 'name', + operatorValue: 'contains', + value: capabilityTestName, + }, + { + columnField: 'variants', + operatorValue: 'has entry containing', + value: `Capability:${featureGate}`, + }, + ], + linkOperator: 'and', + } + if (fetchError) { return ( @@ -255,26 +176,21 @@ export default function FeatureGateDetail(props) { - {activeTab === 0 && gate.links && ( - )} - {activeTab === 1 && gate.links && ( - )} From 1f25eec3215881918d9cb6c37234a845abe5131d Mon Sep 17 00:00:00 2001 From: Devan Goodwin Date: Wed, 15 Jul 2026 13:45:35 -0300 Subject: [PATCH 6/8] Tooltip for why test count differs from table --- sippy-ng/src/tests/FeatureGateDetail.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sippy-ng/src/tests/FeatureGateDetail.js b/sippy-ng/src/tests/FeatureGateDetail.js index e1936452d..3b1bba5f2 100644 --- a/sippy-ng/src/tests/FeatureGateDetail.js +++ b/sippy-ng/src/tests/FeatureGateDetail.js @@ -7,11 +7,13 @@ import { Container, Tab, Tabs, + Tooltip, Typography, } from '@mui/material' import { Link } from 'react-router-dom' import { safeEncodeURIComponent } from '../helpers' import Alert from '@mui/material/Alert' +import HelpOutlineIcon from '@mui/icons-material/HelpOutline' import PropTypes from 'prop-types' import React, { Fragment, useEffect } from 'react' import SimpleBreadcrumbs from '../components/SimpleBreadcrumbs' @@ -154,6 +156,16 @@ export default function FeatureGateDetail(props) { Total Test Count: {gate.unique_test_count} + + + Enabled:{' '} From 99607ae105c3dfa76ca3c0f585feca13e02fb037 Mon Sep 17 00:00:00 2001 From: Devan Goodwin Date: Wed, 15 Jul 2026 14:04:57 -0300 Subject: [PATCH 7/8] Auto-display tests by capability if annotations tab is empty --- sippy-ng/src/tests/FeatureGateDetail.js | 18 ++++++++++++++---- sippy-ng/src/tests/TestTable.js | 15 +++++++++------ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/sippy-ng/src/tests/FeatureGateDetail.js b/sippy-ng/src/tests/FeatureGateDetail.js index 3b1bba5f2..f75547630 100644 --- a/sippy-ng/src/tests/FeatureGateDetail.js +++ b/sippy-ng/src/tests/FeatureGateDetail.js @@ -69,6 +69,15 @@ export default function FeatureGateDetail(props) { }) }, [release, featureGate]) + const tabAutoSelected = React.useRef(false) + + const handleAnnotationDataLoaded = React.useCallback((count) => { + if (!tabAutoSelected.current && count <= 1) { + setActiveTab(1) + tabAutoSelected.current = true + } + }, []) + const annotationFilter = { items: [ { @@ -188,23 +197,24 @@ export default function FeatureGateDetail(props) { - {activeTab === 0 && ( + - )} + - {activeTab === 1 && ( + - )} + ) diff --git a/sippy-ng/src/tests/TestTable.js b/sippy-ng/src/tests/TestTable.js index efc901a10..a2f06d3f7 100644 --- a/sippy-ng/src/tests/TestTable.js +++ b/sippy-ng/src/tests/TestTable.js @@ -962,13 +962,13 @@ function TestTable(props) { return response.json() }) .then((json) => { - if (json != null) { - setRows(json) - } else { - setRows([]) - } + const data = json != null ? json : [] + setRows(data) setLoaded(true) setSearching(false) + if (props.onDataLoaded) { + props.onDataLoaded(data.length) + } }) .catch((error) => { setFetchError( @@ -991,10 +991,12 @@ function TestTable(props) { setSearching(true) fetchData() } else { - // Mark as loaded so we don't show loading spinner, and clear any stale data setRows([]) setLoaded(true) setSearching(false) + if (props.onDataLoaded) { + props.onDataLoaded(0) + } } prevLocation.current = location @@ -1178,6 +1180,7 @@ TestTable.propTypes = { sortField: PropTypes.string, rowsPerPageOptions: PropTypes.array, view: PropTypes.string, + onDataLoaded: PropTypes.func, } export default withStyles(generateClasses(TEST_THRESHOLDS))(TestTable) From 8d9ec0f15becde036aa12788ad4e4ee81f8dac99 Mon Sep 17 00:00:00 2001 From: Devan Goodwin Date: Wed, 15 Jul 2026 15:08:06 -0300 Subject: [PATCH 8/8] Review fixes --- cmd/sippy/seed_data.go | 2 +- sippy-ng/src/tests/FeatureGateDetail.js | 1 + test/e2e/feature_gates_test.go | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/sippy/seed_data.go b/cmd/sippy/seed_data.go index f4c319bfd..d5a685b39 100644 --- a/cmd/sippy/seed_data.go +++ b/cmd/sippy/seed_data.go @@ -374,7 +374,7 @@ var syntheticTests = []syntheticTestSpec{ }, }, { - testID: "test-cap-aws-dual-stack-install", testName: "install should succeed: overall", + testID: "test-cap-aws-dual-stack-install", testName: "install should succeed: infrastructure", component: "Installer / openshift-installer", capabilities: []string{"install"}, jobCounts: map[string]map[string]testCount{ awsAmd64CapabilityAWSDualStackInstall: {"4.22": {50, 47, 0}}, diff --git a/sippy-ng/src/tests/FeatureGateDetail.js b/sippy-ng/src/tests/FeatureGateDetail.js index f75547630..919078727 100644 --- a/sippy-ng/src/tests/FeatureGateDetail.js +++ b/sippy-ng/src/tests/FeatureGateDetail.js @@ -71,6 +71,7 @@ export default function FeatureGateDetail(props) { const tabAutoSelected = React.useRef(false) + // <= 1 because the API always returns an implicit "Overall" row const handleAnnotationDataLoaded = React.useCallback((count) => { if (!tabAutoSelected.current && count <= 1) { setActiveTab(1) diff --git a/test/e2e/feature_gates_test.go b/test/e2e/feature_gates_test.go index 68238f165..4e442e688 100644 --- a/test/e2e/feature_gates_test.go +++ b/test/e2e/feature_gates_test.go @@ -143,7 +143,7 @@ func TestFeatureGatesInstallCapabilityLinkFollowable(t *testing.T) { assert.Greater(t, len(tests), 0, "expected tests when following tests_by_capability link for AWSDualStackInstall") for _, test := range tests { assert.Contains(t, test.Name, "install should succeed", - "install gate capability tests should match install tests") + "install gate capability tests should contain install tests") } }