Skip to content

Commit

Permalink
feat(front): pull to refresh build list (#371)
Browse files Browse the repository at this point in the history
  • Loading branch information
ekelen authored Jul 12, 2020
1 parent f39ea9d commit bc78139
Show file tree
Hide file tree
Showing 12 changed files with 227 additions and 99 deletions.
5 changes: 5 additions & 0 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"react-dom": "16.13.1",
"react-feather": "2.0.3",
"react-hook-form": "5.2.0",
"react-js-pull-to-refresh": "^1.2.2",
"react-router-dom": "^5.1.2",
"tabler-react": "^1.30.1"
},
Expand Down
25 changes: 22 additions & 3 deletions web/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,39 @@
import React, { useContext, useEffect } from 'react'
import { hot } from 'react-hot-loader'
import {
BrowserRouter as Router, Redirect, Route, Switch,
BrowserRouter as Router,
Redirect,
Route,
Switch,
} from 'react-router-dom'
import 'tabler-react/dist/Tabler.css'
import './assets/main.scss'
import { GlobalStore } from './store/GlobalStore'
import { GlobalStore, GlobalContext } from './store/GlobalStore'
import { ThemeContext, ThemeStore } from './store/ThemeStore'
import Error404 from './ui/pages/Error404/Error404'
import Home from './ui/pages/Home/Home'
import { getMobileOperatingSystem } from './util/browser'

const AppRouter = () => {
const { theme: { bg: { page: pageBgColor } } } = useContext(ThemeContext)
const {
theme: {
bg: { page: pageBgColor },
},
} = useContext(ThemeContext)
const {
state: { userAgent },
updateState,
} = useContext(GlobalContext)
useEffect(() => {
document.body.style.backgroundColor = pageBgColor
}, [pageBgColor])
useEffect(() => {
if (!userAgent) {
updateState({
userAgent: getMobileOperatingSystem(),
})
}
}, [userAgent, updateState])

return (
<Router>
Expand Down
37 changes: 21 additions & 16 deletions web/src/api/apiResponseTransforms.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,18 @@ export const flagBuildsFirstOfDay = (sortedTopLevelBuilds) => Array.isArray(sort
const { created_at: laterBuildCreatedAt = null } = acc[i - 1] || {}
acc[i] = {
...build,
buildIsFirstOfDay:
!!getIsNextDay(buildCreatedAt, laterBuildCreatedAt),
buildIsFirstOfDay: !!getIsNextDay(buildCreatedAt, laterBuildCreatedAt),
}
return acc
}, [])


/**
* Adds entry {allBuildsForMr: Array<int>} to each build,
* each int corresponds to builds indices in state.builds
* with the same merge request ID
*/
export const groupByMr = (acc = {}, build, i) => {
const {
has_mergerequest: hasMergeRequest,
has_mergerequest_id: buildMrId,
} = build || {}
const { has_mergerequest: hasMergeRequest, has_mergerequest_id: buildMrId } = build || {}

if (!hasMergeRequest || !buildMrId) {
return {
Expand Down Expand Up @@ -82,12 +77,15 @@ export const groupBuildsByMr = (builds) => values(builds.reduce(groupByMr, {}))
* @return {Array<Number>}
*/
export const getLatestMasterBuildsForProjects = (sortedTopLevelBuilds) => {
const uniqueProjects = uniq(sortedTopLevelBuilds.map((b) => b.has_project_id))
const uniqueProjects = uniq(
sortedTopLevelBuilds.map((b) => b.has_project_id),
)
const latestMasterBuildPerProject = uniqueProjects
.map((p) => sortedTopLevelBuilds
.findIndex((build) => build.has_project_id
&& build.has_project_id === p
&& getStrEquNormalized(build.branch, BRANCH.MASTER)))
.map((p) => sortedTopLevelBuilds.findIndex(
(build) => build.has_project_id
&& build.has_project_id === p
&& getStrEquNormalized(build.branch, BRANCH.MASTER),
))
.filter((index) => index > -1)
return latestMasterBuildPerProject
}
Expand All @@ -98,18 +96,25 @@ export const getLatestMasterBuildsForProjects = (sortedTopLevelBuilds) => {
* @return {Object<{humanMessage: string, status: number, statusText: string}>}
*/
export const validateError = ({ error }) => {
const { message: axiosMessage } = error.toJSON()
const { message: axiosMessage } = error.toJSON
? error.toJSON()
: { error: error.toString() }
const {
response: { data: customTopLevelMessage = '', status, statusText } = {
data: '',
status: 0,
statusText: '',
},
} = error || {}
const { response = {} } = error || {}
const {
response: { data: { message: customNestedMessage } = { data: { message: '' } } },
} = error || {}
const humanMessage = getSafeStr(customTopLevelMessage) || getSafeStr(customNestedMessage) || axiosMessage
data: { message: customNestedMessage = '' } = {
data: { message: '' },
},
} = response || {}
const humanMessage = getSafeStr(customTopLevelMessage)
|| getSafeStr(customNestedMessage)
|| axiosMessage
return {
humanMessage,
status,
Expand Down
17 changes: 14 additions & 3 deletions web/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ import { mockBuildListRequest } from './mock'
import { buildListRequest } from './requests'
import { validateError } from './apiResponseTransforms'

export const getBuildList = ({ apiKey = '', queryObject }) => (process.env.YOLO_UI_TEST === 'true'
export const getBuildList = ({ apiKey = '', queryObject }) => process.env.YOLO_UI_TEST === 'true'
? mockBuildListRequest()
: buildListRequest({ apiKey, queryObject }))
: buildListRequest({ apiKey, queryObject })

export const requestBuilds = ({ updateState = () => { }, locationSearch = '', apiKey = '' }) => {
export const requestBuilds = ({
updateState = () => {},
locationSearch = '',
apiKey = '',
}) => {
if (!locationSearch) {
return null
}
Expand Down Expand Up @@ -43,4 +47,11 @@ export const requestBuilds = ({ updateState = () => { }, locationSearch = '', ap
})
},
)
.finally(() => {
updateState({
autoRefreshOn: false,
isLoaded: true,
authIsPending: false,
})
})
}
58 changes: 37 additions & 21 deletions web/src/hooks/queryHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ import { useHistory, useLocation } from 'react-router-dom'
import { requestBuilds } from '../api'
import { actions } from '../constants'
import { GlobalContext } from '../store/GlobalStore'
import { getFallbackQueryString, getFiltersFromUrlQuery } from '../store/globalStoreHelpers'
import {
getFallbackQueryString,
getFiltersFromUrlQuery,
} from '../store/globalStoreHelpers'

export const useRedirectHome = () => {
const history = useHistory()
const redirectHome = useCallback(() => history.push({
path: '/',
}), [history])
const redirectHome = useCallback(
() => history.push({
path: '/',
}),
[history],
)
return { redirectHome }
}

Expand All @@ -19,27 +25,38 @@ export const useRedirectHome = () => {
*/
export const useRedirectOnEmptyQuery = () => {
const { search: locationSearch } = useLocation()
const { state: { userAgent }, updateState } = useContext(GlobalContext)
const {
state: { userAgent },
updateState,
} = useContext(GlobalContext)
const history = useHistory()
useEffect(() => {
if (!locationSearch) {
const fallbackQueryString = getFallbackQueryString({ userAgent, updateState })
history.push({
pathname: '/',
search: fallbackQueryString,
})
}
},
// TODO: Bad; refactor
// eslint-disable-next-line react-hooks/exhaustive-deps
[locationSearch])
useEffect(
() => {
if (!locationSearch) {
const fallbackQueryString = getFallbackQueryString({
userAgent,
updateState,
})
history.push({
pathname: '/',
search: fallbackQueryString,
})
}
},
// TODO: Bad; refactor
// eslint-disable-next-line react-hooks/exhaustive-deps
[locationSearch],
)
}

/**
* Call API if query in URL bar changes
* Call API if query in URL bar changes and state.needsRefresh is true
*/
export const useRequestOnQueryChange = () => {
const { state: { needsRefresh, apiKey }, updateState } = useContext(GlobalContext)
const {
state: { needsRefresh, apiKey },
updateState,
} = useContext(GlobalContext)
const { search: locationSearch } = useLocation()

useEffect(() => {
Expand All @@ -62,8 +79,7 @@ export const useSetFiltersOnQueryChange = () => {
const { artifact_kinds, build_driver, build_state } = getFiltersFromUrlQuery({ locationSearch }) || {}
dispatch({
type: actions.UPDATE_UI_FILTERS,
payload:
{ artifact_kinds, build_driver, build_state },
payload: { artifact_kinds, build_driver, build_state },
})
}
if (locationSearch) updateFilters()
Expand Down
22 changes: 0 additions & 22 deletions web/src/ui/components/BuildListContainer.js

This file was deleted.

2 changes: 1 addition & 1 deletion web/src/ui/components/Header/Header.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
padding-right: 0.75rem;
overflow-x: hidden;
flex-shrink: 0;
width: 100%;
min-width: 80vw;
display: flex;
align-items: center;
justify-content: flex-start;
Expand Down
52 changes: 52 additions & 0 deletions web/src/ui/components/PullToRefresh.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react'
import { PullToRefresh } from 'react-js-pull-to-refresh'

const RefreshContent = () => (
<div
style={{
backgroundColor: 'transparent',
textAlign: 'center',
height: 75,
width: '100vw',
}}
/>
)

const ReleaseContent = () => (
<div
style={{
height: 75,
width: '100vw',
}}
/>
)

const PullDownContent = () => (
<div
style={{
textAlign: 'center',
height: 75,
width: '100vw',
}}
/>
)

export const PullToRefreshWrapper = ({
onRefresh,
children,
isAuthed,
isMobile,
}) => (
<PullToRefresh
pullDownContent={<PullDownContent />}
releaseContent={<ReleaseContent />}
refreshContent={<RefreshContent />}
pullDownThreshold={50}
onRefresh={onRefresh}
triggerHeight={isAuthed && isMobile ? 200 : 0}
// startInvisible
backgroundColor="invisible"
>
{children}
</PullToRefresh>
)
15 changes: 13 additions & 2 deletions web/src/ui/components/ShowFiltersButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { Sliders } from 'react-feather'
import { ThemeContext } from '../../store/ThemeStore'

const ShowFiltersButton = ({ clickAction, showingFiltersModal }) => {
const { theme: { bg: { btnPrimary } } } = useContext(ThemeContext)
const {
theme: {
bg: { btnPrimary },
},
} = useContext(ThemeContext)

const showFiltersButtonStyle = {
position: 'fixed',
Expand All @@ -18,10 +22,17 @@ const ShowFiltersButton = ({ clickAction, showingFiltersModal }) => {
transform: 'rotate(90deg)',
cursor: 'pointer',
display: showingFiltersModal ? 'none' : 'flex',
zIndex: 2000,
}

return (
<div style={showFiltersButtonStyle} onClick={clickAction} onKeyDown={clickAction} tabIndex={0} role="button">
<div
style={showFiltersButtonStyle}
onClick={clickAction}
onKeyDown={clickAction}
tabIndex={0}
role="button"
>
<Sliders color="white" size={20} />
</div>
)
Expand Down
Loading

0 comments on commit bc78139

Please sign in to comment.