diff --git a/web/src/components/ResourceCard/ResourceCard.styled.ts b/web/src/components/ResourceCard/ResourceCard.styled.ts index 4b89270db9..9781a03ccb 100644 --- a/web/src/components/ResourceCard/ResourceCard.styled.ts +++ b/web/src/components/ResourceCard/ResourceCard.styled.ts @@ -1,8 +1,9 @@ import {MoreOutlined} from '@ant-design/icons'; import {Button, Space, Typography} from 'antd'; +import styled from 'styled-components'; import emptyStateIcon from 'assets/SpanAssertionsEmptyState.svg'; -import styled from 'styled-components'; +import Link from 'components/Link'; import {ResourceType} from 'types/Resource.type'; export const ActionButton = styled(MoreOutlined)` @@ -77,9 +78,7 @@ export const HeaderDetail = styled(Typography.Text)` margin-right: 8px; `; -export const Link = styled(Button).attrs({ - type: 'link', -})` +export const CustomLink = styled(Link)` color: ${({theme}) => theme.color.primary}; font-weight: 600; padding: 0; diff --git a/web/src/components/ResourceCard/ResourceCardRuns.tsx b/web/src/components/ResourceCard/ResourceCardRuns.tsx index 4caae4ac83..ab99ea58f6 100644 --- a/web/src/components/ResourceCard/ResourceCardRuns.tsx +++ b/web/src/components/ResourceCard/ResourceCardRuns.tsx @@ -11,7 +11,15 @@ interface IProps { resourcePath: string; } -const ResourceCardRuns = ({children, hasMoreRuns, hasRuns, isCollapsed, isLoading, onViewAll, resourcePath}: IProps) => { +const ResourceCardRuns = ({ + children, + hasMoreRuns, + hasRuns, + isCollapsed, + isLoading, + onViewAll, + resourcePath, +}: IProps) => { if (isCollapsed) return null; return ( @@ -28,9 +36,9 @@ const ResourceCardRuns = ({children, hasMoreRuns, hasRuns, isCollapsed, isLoadin {hasMoreRuns && ( - + View all runs - + )} diff --git a/web/src/components/RunDetailLayout/HeaderLeft.tsx b/web/src/components/RunDetailLayout/HeaderLeft.tsx index 6efac490b8..09677fd192 100644 --- a/web/src/components/RunDetailLayout/HeaderLeft.tsx +++ b/web/src/components/RunDetailLayout/HeaderLeft.tsx @@ -9,9 +9,10 @@ import * as S from './RunDetailLayout.styled'; interface IProps { name: string; triggerType: string; + origin: string; } -const HeaderLeft = ({name, triggerType}: IProps) => { +const HeaderLeft = ({name, triggerType, origin}: IProps) => { const {run: {createdAt, testSuiteId, testSuiteRunId, executionTime, trace, traceId, testVersion} = {}, run} = useTestRun(); const createdTimeAgo = Date.getTimeAgo(createdAt ?? ''); @@ -36,7 +37,7 @@ const HeaderLeft = ({name, triggerType}: IProps) => { return ( - navigate(-1)}> + navigate(origin)}> diff --git a/web/src/components/RunDetailLayout/RunDetailLayout.tsx b/web/src/components/RunDetailLayout/RunDetailLayout.tsx index 00a0d02918..b4e149a5bd 100644 --- a/web/src/components/RunDetailLayout/RunDetailLayout.tsx +++ b/web/src/components/RunDetailLayout/RunDetailLayout.tsx @@ -12,6 +12,8 @@ import {isRunStateSucceeded} from 'models/TestRun.model'; import {useNotification} from 'providers/Notification/Notification.provider'; import {useSettingsValues} from 'providers/SettingsValues/SettingsValues.provider'; import {useTestRun} from 'providers/TestRun/TestRun.provider'; +import {useAppSelector} from 'redux/hooks'; +import UserSelectors from 'selectors/User.selectors'; import TestRunAnalyticsService from 'services/Analytics/TestRunAnalytics.service'; import {ConfigMode} from 'types/DataStore.types'; import HeaderLeft from './HeaderLeft'; @@ -41,6 +43,7 @@ const RunDetailLayout = ({test: {id, name, trigger}, test}: IProps) => { const {dataStoreConfig} = useSettingsValues(); const [prevState, setPrevState] = useState(run.state); useDocumentTitle(`${name} - ${run.state}`); + const runOriginPath = useAppSelector(UserSelectors.selectRunOriginPath); useEffect(() => { const isNoTracingMode = dataStoreConfig.mode === ConfigMode.NO_TRACING_MODE; @@ -59,10 +62,10 @@ const RunDetailLayout = ({test: {id, name, trigger}, test}: IProps) => { const tabBarExtraContent = useMemo( () => ({ - left: , + left: , right: , }), - [id, name, trigger.type] + [id, name, trigger.type, runOriginPath] ); return ( diff --git a/web/src/components/TestSuiteHeader/TestSuiteHeader.tsx b/web/src/components/TestSuiteHeader/TestSuiteHeader.tsx index a146eca67b..5c21a60ca1 100644 --- a/web/src/components/TestSuiteHeader/TestSuiteHeader.tsx +++ b/web/src/components/TestSuiteHeader/TestSuiteHeader.tsx @@ -7,6 +7,8 @@ import {TestState as TestStateEnum} from 'constants/TestRun.constants'; import {useDashboard} from 'providers/Dashboard/Dashboard.provider'; import {useTestSuite} from 'providers/TestSuite/TestSuite.provider'; import {useTestSuiteRun} from 'providers/TestSuiteRun/TestSuite.provider'; +import {useAppSelector} from 'redux/hooks'; +import UserSelectors from 'selectors/User.selectors'; import * as S from './TestSuiteHeader.styled'; import VariableSetSelector from '../VariableSetSelector/VariableSetSelector'; @@ -34,11 +36,12 @@ const TestSuiteHeader = () => { const {id: testSuiteId, name, version, description} = testSuite; const {state, id: runId, allStepsRequiredGatesPassed} = run; const lastPath = getLastPath(pathname); + const runOriginPath = useAppSelector(UserSelectors.selectRunOriginPath); return ( - navigate('/')} data-cy="testsuite-header-back-button"> + navigate(runOriginPath)} data-cy="testsuite-header-back-button">
diff --git a/web/src/hooks/useRouterSync.ts b/web/src/hooks/useRouterSync.ts index c90433d65f..18b962e50b 100644 --- a/web/src/hooks/useRouterSync.ts +++ b/web/src/hooks/useRouterSync.ts @@ -8,6 +8,10 @@ const useRouterSync = () => { useEffect(() => { return RouterMiddleware.startListening({testId: params.testId, runId: params.runId}); }, [params.runId, params.testId]); + + useEffect(() => { + return RouterMiddleware.startListeningForLocationChange(); + }, []); }; export default useRouterSync; diff --git a/web/src/redux/Router.middleware.ts b/web/src/redux/Router.middleware.ts index a9d55037be..b6ea46df62 100644 --- a/web/src/redux/Router.middleware.ts +++ b/web/src/redux/Router.middleware.ts @@ -1,23 +1,30 @@ import {createListenerMiddleware} from '@reduxjs/toolkit'; +import type {TypedStartListening} from '@reduxjs/toolkit'; import {LOCATION_CHANGE} from 'redux-first-history'; import {parse} from 'query-string'; import RouterActions from './actions/Router.actions'; -import {RootState} from './store'; +import {runOriginPathAdded} from './slices/User.slice'; +import {AppDispatch, RootState} from './store'; +type AppStartListening = TypedStartListening; const listener = createListenerMiddleware(); +const startAppListening = listener.startListening as AppStartListening; const sideEffectActionList = [RouterActions.updateSelectedAssertion, RouterActions.updateSelectedSpan]; +const runUrlRegex = /^\/(test|testsuite)\/([^\/]+)\/run\/([^\/]+)(.*)$/; + const RouterMiddleware = () => ({ middleware: listener.middleware, + startListening(params = {}) { - return listener.startListening({ + return startAppListening({ predicate: ({type = ''}) => type === LOCATION_CHANGE || (type.endsWith('/fulfilled') && !type.startsWith('router/')), effect(_, {dispatch, getState}) { const { router: {location}, - } = getState() as RootState; + } = getState(); const search = parse(location?.search || ''); @@ -27,6 +34,31 @@ const RouterMiddleware = () => ({ }, }); }, + + startListeningForLocationChange() { + return startAppListening({ + predicate: action => { + const pathname = action?.payload?.location?.pathname ?? ''; + return action?.type === LOCATION_CHANGE && pathname.match(runUrlRegex); + }, + effect: async (_, {dispatch, getOriginalState, getState}) => { + const { + router: {location: prevLocation}, + } = getOriginalState(); + + const { + router: {location: currLocation}, + } = getState(); + + const prevPathname = prevLocation?.pathname ?? ''; + + if (!prevPathname.match(runUrlRegex)) { + const defaultPath = currLocation?.pathname?.includes('testsuite') ? '/testsuites' : '/'; + dispatch(runOriginPathAdded(prevLocation?.pathname ?? defaultPath)); + } + }, + }); + }, }); export default RouterMiddleware(); diff --git a/web/src/redux/slices/User.slice.ts b/web/src/redux/slices/User.slice.ts index 2084e0925a..8f36cd8d88 100644 --- a/web/src/redux/slices/User.slice.ts +++ b/web/src/redux/slices/User.slice.ts @@ -1,9 +1,10 @@ -import {createAction, createSlice} from '@reduxjs/toolkit'; +import {PayloadAction, createAction, createSlice} from '@reduxjs/toolkit'; import UserPreferencesService from 'services/UserPreferences.service'; import {IUserState, TUserPreferenceKey, TUserPreferenceValue} from 'types/User.types'; export const initialState: IUserState = { preferences: UserPreferencesService.get(), + runOriginPath: '/', }; interface ISetUserPreferencesProps { @@ -20,7 +21,11 @@ export const setUserPreference = createAction('user/setUserPreference', ({key, v const userSlice = createSlice({ name: 'user', initialState, - reducers: {}, + reducers: { + runOriginPathAdded(state, {payload}: PayloadAction) { + state.runOriginPath = payload; + }, + }, extraReducers: builder => { builder.addCase(setUserPreference, (state, {payload: {preferences}}) => { state.preferences = preferences; @@ -28,5 +33,5 @@ const userSlice = createSlice({ }, }); -// export const {} = testDefinitionSlice.actions; +export const {runOriginPathAdded} = userSlice.actions; export default userSlice.reducer; diff --git a/web/src/selectors/User.selectors.ts b/web/src/selectors/User.selectors.ts index 204af363f0..277b635e7f 100644 --- a/web/src/selectors/User.selectors.ts +++ b/web/src/selectors/User.selectors.ts @@ -13,6 +13,8 @@ const UserSelectors = () => ({ return user.preferences[key]; } ), + + selectRunOriginPath: (state: RootState) => state.user.runOriginPath, }); export default UserSelectors(); diff --git a/web/src/types/User.types.ts b/web/src/types/User.types.ts index 1672b5b1b8..684ec379d9 100644 --- a/web/src/types/User.types.ts +++ b/web/src/types/User.types.ts @@ -12,4 +12,5 @@ export type TUserPreferenceValue