Skip to content

Commit e2f0a2e

Browse files
authored
Custom useTabHook for Tab Navigation (#621)
* Updates for tab history handling Using WCA@IBM! Add default tab hashes to result paths trying to update the hash after navigation via effect within ResultView ends up making the history ugly regardless of use of navigation hooks on tab selection. Just set the hash on the initial target for summary Create custom useTabHook Move buildTree Convert Run to useTabHook Use #summary hash for run hrefs * useTabHook, restore state and add skipHash When nested, the activeTab should not sync with the location hash. add useTabHook to jenkinsjob analysis Update link composition in filterheatmap provide hash for tabbed views use `${}` syntax Update useTabHook navigation with location include search and path as-is, search is used in a few spots * Catch error on project request from header * useTabHook in jenkinsjobanalysis view
1 parent 3924094 commit e2f0a2e

File tree

12 files changed

+247
-290
lines changed

12 files changed

+247
-290
lines changed

frontend/src/components/classify-failures.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ const resultToClassificationRow = (result, index, filterFunc) => {
7676
'isOpen': false,
7777
'result': result,
7878
'cells': [
79-
{title: <React.Fragment><Link to={`../results/${result.id}`} relative="Path">{result.test_id}</Link> {markers}</React.Fragment>},
79+
{title: <React.Fragment><Link to={`../results/${result.id}#summary`} relative="Path">{result.test_id}</Link> {markers}</React.Fragment>},
8080
{title: <span className={result.result}>{resultIcon} {toTitleCase(result.result)}</span>},
8181
{title: <React.Fragment>{exceptionBadge}</React.Fragment>},
8282
{title: <ClassificationDropdown testResult={result} />},
@@ -165,6 +165,7 @@ const ClassifyFailuresTable = ({ filters, run_id }) => {
165165
hideSummary={hideSummary}
166166
hideTestObject={hideTestObject}
167167
testResult={rows[rowIndex].result}
168+
skipHash={true}
168169
/>
169170
}
170171
]

frontend/src/components/fileupload.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const FileUpload = (props) => {
5050
let action = null;
5151
if (data.metadata.run_id) {
5252
const RunButton = () => (
53-
<AlertActionLink component='a' href={'/project/' + (data.metadata.project_id || primaryObject.id) + '/runs/' + data.metadata.run_id}>
53+
<AlertActionLink component='a' href={`/project/${(data.metadata.project_id || primaryObject.id)}/runs/${data.metadata.run_id}#summary`}>
5454
Go to Run
5555
</AlertActionLink>
5656
);

frontend/src/components/ibutsu-header.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,7 @@ const IbutsuHeader = () => {
8787
}, [darkTheme]);
8888

8989
useEffect(() => {
90-
if (selectedProject?.id !== project_id) {
91-
90+
if (project_id && (selectedProject?.id !== project_id)) {
9291
HttpClient.get([Settings.serverUrl, '/project/', project_id])
9392
.then(response => HttpClient.handleResponse(response))
9493
.then((data) => {
@@ -99,7 +98,8 @@ const IbutsuHeader = () => {
9998
setFilterValue();
10099
setInputValue(data.title);
101100
setIsProjectSelectOpen(false);
102-
});
101+
})
102+
.catch((error) => console.error(error));
103103
}
104104
}, [project_id, selectedProject, setDefaultDashboard, setPrimaryObject, setPrimaryType]);
105105

frontend/src/components/last-passed.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const LastPassed = (props) => {
3939
return (
4040
<React.Fragment>
4141
{resultData &&
42-
<Link target="_blank" rel="noopener noreferrer" to={`../results/${resultData.id}`} relative="Path">
42+
<Link target="_blank" rel="noopener noreferrer" to={`../results/${resultData.id}#summary`} relative="Path">
4343
<Badge isRead>
4444
{new Date(resultData.start_time).toLocaleString()}
4545
</Badge>

frontend/src/components/result.js

Lines changed: 29 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect, useCallback, useContext, useMemo } from 'react';
1+
import React, { useState, useEffect, useContext, useMemo, useCallback } from 'react';
22
import PropTypes from 'prop-types';
33

44
import {
@@ -18,7 +18,7 @@ import {
1818
import { InfoCircleIcon, CodeIcon, SearchIcon, FileAltIcon } from '@patternfly/react-icons';
1919
import { CodeEditor, Language } from '@patternfly/react-code-editor';
2020

21-
import { Link, useNavigate, useLocation } from 'react-router-dom';
21+
import { Link } from 'react-router-dom';
2222
import Linkify from 'react-linkify';
2323

2424
import * as http from '../services/http';
@@ -30,89 +30,40 @@ import TabTitle from './tabs';
3030
import TestHistoryTable from './test-history';
3131
import ArtifactTab from './artifact-tab';
3232
import { IbutsuContext } from '../services/context';
33+
import { useTabHook } from './tabHook';
3334

3435
const ResultView = ({
3536
comparisonResults,
36-
defaultTab,
37+
defaultTab='summary',
3738
hideArtifact=false,
3839
hideSummary=false,
3940
hideTestObject=false,
4041
hideTestHistory=false,
41-
testResult
42+
testResult,
43+
skipHash=false
4244
}) => {
4345
const context = useContext(IbutsuContext);
4446
const { darkTheme } = context;
4547

4648
// State
4749
const [artifacts, setArtifacts] = useState([]);
48-
const [testHistoryTable, setTestHistoryTable] = useState(null);
4950

50-
// Hooks
51-
const navigate = useNavigate();
52-
const location = useLocation();
51+
// https://v5-archive.patternfly.org/components/tabs#tabs-linked-to-nav-elements
5352

54-
const getDefaultTab = () => {
55-
if (defaultTab) {
56-
return defaultTab;
57-
}
58-
else if (!hideSummary) {
59-
return 'summary';
60-
}
61-
else if (!!artifactTabs.length > 0) {
62-
return artifactTabs[0].key;
63-
}
64-
else {
65-
return null;
66-
}
67-
};
68-
69-
const getTabIndex = useCallback((defaultValue) => (!!location && location.hash !== '') ? location.hash.substring(1) : defaultValue,[location]);
70-
71-
const getTestHistoryTable = useCallback(() => {
53+
const testHistoryTable = useMemo(() => {
7254
if (comparisonResults !== undefined) {
73-
setTestHistoryTable(
55+
return(
7456
<TestHistoryTable
7557
comparisonResults={comparisonResults}
7658
testResult={testResult}
7759
/>);
7860
} else {
79-
setTestHistoryTable(
61+
return(
8062
<TestHistoryTable testResult={testResult}/>);
8163
}
8264

83-
}, [setTestHistoryTable, comparisonResults, testResult]);
65+
}, [comparisonResults, testResult]);
8466

85-
const [activeTab, setActiveTab] = useState(getTabIndex(getDefaultTab()));
86-
87-
88-
useEffect(() => {
89-
if (activeTab === 'test-history') {
90-
getTestHistoryTable();
91-
}
92-
}, [activeTab, getTestHistoryTable]);
93-
94-
const handlePopState = useCallback(() => {
95-
// Handle browser navigation buttons click
96-
const tabIndex = getTabIndex('summary');
97-
setActiveTab(tabIndex);
98-
}, [getTabIndex, setActiveTab]);
99-
100-
useEffect(()=>{
101-
if (activeTab === 'test-history') {
102-
getTestHistoryTable();
103-
}
104-
window.addEventListener('popstate', handlePopState);
105-
return () => {
106-
window.removeEventListener('popstate', handlePopState);
107-
};
108-
}, [activeTab, getTestHistoryTable, handlePopState]);
109-
110-
const onTabSelect = (_, tabIndex) => {
111-
if (location) {
112-
navigate(`${location.pathname}${location.search}#${tabIndex}`);
113-
}
114-
setActiveTab(tabIndex);
115-
};
11667

11768
const artifactTabs = useMemo(() => artifacts.map((art) => (
11869
<Tab
@@ -125,6 +76,18 @@ const ResultView = ({
12576
)
12677
), [artifacts]);
12778

79+
const artifactKeys = useCallback(() => {
80+
if (artifactTabs && artifactTabs?.length !== 0) {return(artifactTabs.map((tab) => tab.key));}
81+
else {return([]);}
82+
}, [artifactTabs]);
83+
84+
// Tab state and navigation hooks/effects
85+
const {activeTab, onTabSelect} = useTabHook(
86+
['summary', 'testHistory', 'testObject', ...artifactKeys()],
87+
defaultTab,
88+
skipHash
89+
);
90+
12891
useEffect(() => {
12992
// Get artifacts when the test result changes
13093
if (testResult) {
@@ -137,9 +100,6 @@ const ResultView = ({
137100
}
138101
}, [testResult]);
139102

140-
if (activeTab === null) {
141-
setActiveTab(getDefaultTab());
142-
}
143103
let resultIcon = getIconForResult('pending');
144104
let startTime = new Date();
145105
let parameters = <div/>;
@@ -148,7 +108,7 @@ const ResultView = ({
148108
resultIcon = getIconForResult(testResult.result);
149109
startTime = new Date(testResult.start_time);
150110
parameters = Object.keys(testResult.params).map((key) => <div key={key}>{key} = {testResult.params[key]}</div>);
151-
runLink = <Link to={`../runs/${testResult.run_id}`} relative="Path">{testResult.run_id}</Link>;
111+
runLink = <Link to={`../runs/${testResult.run_id}#summary`} relative="Path">{testResult.run_id}</Link>;
152112
}
153113

154114
const testJson = useMemo(() => JSON.stringify(testResult, null, '\t'), [testResult]);
@@ -425,15 +385,15 @@ const ResultView = ({
425385
</Card>
426386
</Tab>
427387
}
428-
{!hideArtifact &&
429-
artifactTabs}
430388
{!hideTestHistory &&
431-
<Tab key="test-history" eventKey="test-history" title={<TabTitle icon={<SearchIcon/>} text="Test History"/>}>
389+
<Tab key="testHistory" eventKey="testHistory" title={<TabTitle icon={<SearchIcon/>} text="Test History"/>}>
432390
{testHistoryTable}
433391
</Tab>
434392
}
393+
{!hideArtifact &&
394+
artifactTabs}
435395
{!hideTestObject && testJson &&
436-
<Tab key="test-object" eventKey="test-object" title={<TabTitle icon={<CodeIcon/>} text="Test Object" />}>
396+
<Tab key="testObject" eventKey="testObject" title={<TabTitle icon={<CodeIcon/>} text="Test Object"/>}>
437397
<Card>
438398
<CardBody id='object-card-body'>
439399
<CodeEditor
@@ -461,6 +421,7 @@ ResultView.propTypes = {
461421
hideTestObject: PropTypes.bool,
462422
hideTestHistory: PropTypes.bool,
463423
testResult: PropTypes.object,
424+
skipHash: PropTypes.bool,
464425
};
465426

466427
export default ResultView;

frontend/src/components/tabHook.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useEffect, useState } from 'react';
2+
import { useLocation, useNavigate } from 'react-router-dom';
3+
4+
// Assisted by watsonx Code Assistant
5+
// Code generated by WCA@IBM in this programming language is not approved for use in IBM product development.
6+
7+
export const useTabHook = (validTabIndicies=[], defaultTab='summary', skipHash=false) => {
8+
9+
const location = useLocation();
10+
const navigate = useNavigate();
11+
const [activeTab, setActiveTab] = useState(defaultTab);
12+
13+
useEffect(() => {
14+
if (!skipHash) {
15+
const currentHash = location.hash.slice(1);
16+
setActiveTab(validTabIndicies.includes(currentHash) ? currentHash : defaultTab);
17+
}
18+
}, [defaultTab, location.hash, skipHash, validTabIndicies]);
19+
20+
useEffect(() => {
21+
// catch a bad tab index and use the default
22+
if (!skipHash && !validTabIndicies.includes(location.hash.slice(1))) {navigate(`${location.pathname}${location.search}#${defaultTab}`);}
23+
}, [defaultTab, location, navigate, skipHash, validTabIndicies]);
24+
25+
const onTabSelect = (_, tabIndex) => {
26+
if (!skipHash) {navigate(`${location.pathname}${location.search}#${tabIndex}`);};
27+
setActiveTab(tabIndex);
28+
};
29+
30+
return {activeTab, onTabSelect};
31+
};

frontend/src/result.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const Result = () => {
3737
throw new Error('Failed with HTTP code ' + response.status);
3838
}
3939
} catch (error) {
40-
console.log(error);
40+
console.error(error);
4141
}
4242
};
4343
fetchTestResult();
@@ -53,13 +53,12 @@ const Result = () => {
5353
</TextContent>
5454
</PageSection>
5555
<PageSection>
56-
{!testResult ? (
57-
<Skeleton />
58-
) : (
56+
{testResult ? (
5957
isResultValid ?
6058
<ResultView testResult={testResult} /> :
6159
<EmptyObject headingText="Result not found" returnLink="/results" returnLinkText="Return to results list"/>
62-
)}
60+
) :
61+
<Skeleton />}
6362
</PageSection>
6463
</React.Fragment>
6564
);

frontend/src/run-list.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const runToRow = (run, filterFunc) => {
7777
}
7878
return {
7979
'cells': [
80-
{title: <React.Fragment><Link to={`${run.id}`}>{run.id}</Link> {badges}</React.Fragment>},
80+
{title: <React.Fragment><Link to={`${run.id}#summary`}>{run.id}</Link> {badges}</React.Fragment>},
8181
{title: round(run.duration) + 's'},
8282
{title: <RunSummary summary={run.summary} />},
8383
{title: created.toLocaleString()},
@@ -442,7 +442,7 @@ const RunList = () => {
442442
onApplyFilter={applyFilter}
443443
onRemoveFilter={removeFilter}
444444
onClearFilters={clearFilters}
445-
onApplyReport={() => navigate('/project/' + params.project_id + '/reports?' + buildParams(filters).join('&'))}
445+
onApplyReport={() => navigate(`/project/${params.project_id}/reports?${buildParams(filters).join('&')}`)}
446446
onSetPage={setPage}
447447
onSetPageSize={setPageSize}
448448
hideFilters={['project_id']}

0 commit comments

Comments
 (0)