Skip to content

Commit

Permalink
Add paging on jobs panel (#2852)
Browse files Browse the repository at this point in the history
* Initial commit of run pagination for jobs

Signed-off-by: sharpd <davidsharp7@gmail.com>

* added linted code

Signed-off-by: sharpd <davidsharp7@gmail.com>

* add pageInit to prevent multiple initial fetches

Signed-off-by: sharpd <davidsharp7@gmail.com>

* fix react error and add additional formatting for runs.

Signed-off-by: sharpd <davidsharp7@gmail.com>

* move paging to JobDetailPage to simplify

Signed-off-by: sharpd <davidsharp7@gmail.com>

* fix runs messaging

Signed-off-by: sharpd <davidsharp7@gmail.com>

* add new fetchLatestRuns and state

Signed-off-by: sharpd <davidsharp7@gmail.com>

* aligning action types

Signed-off-by: sharpd <davidsharp7@gmail.com>

---------

Signed-off-by: sharpd <davidsharp7@gmail.com>
  • Loading branch information
davidsharp7 authored Jul 23, 2024
1 parent f0b2195 commit 2015c39
Show file tree
Hide file tree
Showing 11 changed files with 204 additions and 32 deletions.
6 changes: 5 additions & 1 deletion api/src/main/java/marquez/api/JobResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,8 @@ public Response listRuns(

final List<Run> runs =
runService.findAll(namespaceName.getValue(), jobName.getValue(), limit, offset);
return Response.ok(new Runs(runs)).build();
final int totalCount = jobService.countJobRuns(namespaceName.getValue(), jobName.getValue());
return Response.ok(new Runs(runs, totalCount)).build();
}

@Path("/jobs/runs/{id}")
Expand Down Expand Up @@ -329,5 +330,8 @@ public static class Runs {
@NonNull
@JsonProperty("runs")
List<Run> value;

@JsonProperty("totalCount")
int totalCount;
}
}
14 changes: 14 additions & 0 deletions api/src/main/java/marquez/db/JobDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,20 @@ job_tags as (
@SqlQuery("SELECT count(*) FROM jobs_view AS j WHERE symlink_target_uuid IS NULL")
int count();

@SqlQuery(
"""
select
count(*)
from
runs
where
namespace_name = :namespaceName
and
job_name = :job
;
""")
int countJobRuns(String namespaceName, String job);

@SqlQuery(
"SELECT count(*) FROM jobs_view AS j WHERE j.namespace_name = :namespaceName\n"
+ "AND symlink_target_uuid IS NULL")
Expand Down
1 change: 1 addition & 0 deletions api/src/test/java/marquez/db/JobDaoTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public void testCountFor() {
assertThat(jobDao.count()).isEqualTo(4);

assertThat(jobDao.countFor(namespace.getName())).isEqualTo(3);
assertThat(jobDao.countJobRuns(namespace.getName(), "targetJob")).isEqualTo(0);
}

@Test
Expand Down
32 changes: 17 additions & 15 deletions web/src/components/jobs/JobDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
deleteJob,
dialogToggle,
fetchJobTags,
fetchRuns,
fetchLatestRuns,
resetJobs,
resetRuns,
setTabIndex,
Expand All @@ -44,7 +44,7 @@ import Runs from './Runs'
import SpeedRounded from '@mui/icons-material/SpeedRounded'

interface DispatchProps {
fetchRuns: typeof fetchRuns
fetchLatestRuns: typeof fetchLatestRuns
resetRuns: typeof resetRuns
resetJobs: typeof resetJobs
deleteJob: typeof deleteJob
Expand All @@ -56,41 +56,42 @@ interface DispatchProps {
type IProps = {
job: LineageJob
jobs: IState['jobs']
runs: Run[]
runsLoading: boolean
display: IState['display']
tabIndex: IState['lineage']['tabIndex']
jobTags: string[]
latestRuns: Run[]
isLatestRunsLoading: boolean
} & DispatchProps

const JobDetailPage: FunctionComponent<IProps> = (props) => {
const theme = createTheme(useTheme())
const {
job,
jobs,
fetchRuns,
fetchLatestRuns,
resetRuns,
deleteJob,
dialogToggle,
runs,
latestRuns,
display,
runsLoading,
tabIndex,
setTabIndex,
jobTags,
fetchJobTags,
isLatestRunsLoading,
} = props
const navigate = useNavigate()
const [_, setSearchParams] = useSearchParams()

const handleChange = (event: ChangeEvent, newValue: number) => {
setTabIndex(newValue)
}

const i18next = require('i18next')

useEffect(() => {
fetchRuns(job.name, job.namespace)
fetchJobTags(job.namespace, job.name)
fetchLatestRuns(job.name, job.namespace)
}, [job.name])

useEffect(() => {
Expand All @@ -102,12 +103,13 @@ const JobDetailPage: FunctionComponent<IProps> = (props) => {
// unmounting
useEffect(() => {
return () => {
resetRuns()
resetJobs()
resetRuns()
setTabIndex(0)
}
}, [])

if (runsLoading || jobs.isLoading) {
if (jobs.isLoading || isLatestRunsLoading) {
return (
<Box display={'flex'} justifyContent={'center'} mt={2}>
<CircularProgress color='primary' />
Expand Down Expand Up @@ -244,7 +246,7 @@ const JobDetailPage: FunctionComponent<IProps> = (props) => {
<MqInfo
icon={<DirectionsRun color={'disabled'} />}
label={'Running Status'.toUpperCase()}
value={<MqStatus label={job.latestRun?.state} color={jobRunsStatus(runs)} />}
value={<MqStatus label={job.latestRun?.state} color={jobRunsStatus(latestRuns)} />}
/>
</Grid>
</Grid>
Expand All @@ -271,14 +273,14 @@ const JobDetailPage: FunctionComponent<IProps> = (props) => {
)
)
) : null}
{tabIndex === 1 && <Runs runs={runs} />}
{tabIndex === 1 && <Runs jobName={job.name} jobNamespace={job.namespace} />}
</Box>
)
}

const mapStateToProps = (state: IState) => ({
runs: state.runs.result,
runsLoading: state.runs.isLoading,
latestRuns: state.runs.latestRuns,
isLatestRunsLoading: state.runs.isLatestRunsLoading,
display: state.display,
jobs: state.jobs,
tabIndex: state.lineage.tabIndex,
Expand All @@ -288,7 +290,7 @@ const mapStateToProps = (state: IState) => ({
const mapDispatchToProps = (dispatch: Redux.Dispatch) =>
bindActionCreators(
{
fetchRuns: fetchRuns,
fetchLatestRuns: fetchLatestRuns,
resetRuns: resetRuns,
resetJobs: resetJobs,
deleteJob: deleteJob,
Expand Down
83 changes: 76 additions & 7 deletions web/src/components/jobs/Runs.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,94 @@
// Copyright 2018-2023 contributors to the Marquez project
// SPDX-License-Identifier: Apache-2.0

import * as Redux from 'redux'
import { ArrowBackIosRounded } from '@mui/icons-material'
import {
Box,
Chip,
CircularProgress,
IconButton,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
} from '@mui/material'
import { IState } from '../../store/reducers'
import { Run } from '../../types/api'
import { alpha, createTheme } from '@mui/material/styles'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { fetchRuns } from '../../store/actionCreators'
import { formatUpdatedAt } from '../../helpers'
import { runStateColor } from '../../helpers/nodes'
import { stopWatchDuration } from '../../helpers/time'
import { useTheme } from '@emotion/react'
import MqCode from '../core/code/MqCode'
import MqCopy from '../core/copy/MqCopy'
import MqEmpty from '../core/empty/MqEmpty'
import MqPaging from '../paging/MqPaging'
import MqStatus from '../core/status/MqStatus'
import MqText from '../core/text/MqText'
import React, { FunctionComponent, SetStateAction } from 'react'
import RunInfo from './RunInfo'

interface DispatchProps {
fetchRuns: typeof fetchRuns
}

interface RunsProps {
runs: Run[]
facets?: object
totalCount: number
runsLoading: boolean
jobName: string
jobNamespace: string
}

interface RunsState {
page: number
}

const Runs: FunctionComponent<RunsProps> = (props) => {
const { runs, facets } = props
const PAGE_SIZE = 10

const Runs: FunctionComponent<RunsProps & DispatchProps> = (props) => {
const { runs, facets, totalCount, runsLoading, fetchRuns, jobName, jobNamespace } = props
const i18next = require('i18next')
if (runs.length === 0) {
return <MqEmpty title={i18next.t('jobs.empty_title')} body={i18next.t('jobs.empty_body')} />
}

const [state, setState] = React.useState<RunsState>({
page: 0,
})

const [infoView, setInfoView] = React.useState<Run | null>(null)
const handleClick = (newValue: SetStateAction<Run | null>) => {
setInfoView(newValue)
}

const handleClickPage = (direction: 'prev' | 'next') => {
const directionPage = direction === 'next' ? state.page + 1 : state.page - 1
window.scrollTo(0, 0)
setState({ ...state, page: directionPage })
}

React.useEffect(() => {
fetchRuns(jobName, jobNamespace, PAGE_SIZE, state.page * PAGE_SIZE)
}, [state.page])

const theme = createTheme(useTheme())

if (runs.length === 0) {
return <MqEmpty title={i18next.t('jobs.empty_title')} body={i18next.t('jobs.empty_body')} />
}

if (runsLoading) {
return (
<Box display={'flex'} justifyContent={'center'} mt={2}>
<CircularProgress color='primary' />
</Box>
)
}

if (infoView) {
return (
<>
Expand Down Expand Up @@ -148,12 +192,23 @@ const Runs: FunctionComponent<RunsProps> = (props) => {
<TableCell align='left'>{formatUpdatedAt(run.createdAt)}</TableCell>
<TableCell align='left'>{formatUpdatedAt(run.startedAt)}</TableCell>
<TableCell align='left'>N/A</TableCell>
<TableCell align='left'>{stopWatchDuration(run.durationMs)}</TableCell>
<TableCell align='left'>
{run.state === 'RUNNING' || run.state === 'NEW'
? 'N/A'
: stopWatchDuration(run.durationMs)}{' '}
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
<MqPaging
pageSize={PAGE_SIZE}
currentPage={state.page}
totalCount={totalCount}
incrementPage={() => handleClickPage('next')}
decrementPage={() => handleClickPage('prev')}
/>
{facets && (
<Box mt={2}>
<Box mb={1}>
Expand All @@ -166,4 +221,18 @@ const Runs: FunctionComponent<RunsProps> = (props) => {
)
}

export default Runs
const mapStateToProps = (state: IState) => ({
runs: state.runs.result,
totalCount: state.runs.totalCount,
runsLoading: state.runs.isLoading,
})

const mapDispatchToProps = (dispatch: Redux.Dispatch) =>
bindActionCreators(
{
fetchRuns: fetchRuns,
},
dispatch
)

export default connect(mapStateToProps, mapDispatchToProps)(Runs)
2 changes: 2 additions & 0 deletions web/src/store/actionCreators/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const DIALOG_TOGGLE = 'DIALOG_TOGGLE'
export const FETCH_RUNS = 'FETCH_RUNS'
export const FETCH_RUNS_SUCCESS = 'FETCH_RUNS_SUCCESS'
export const RESET_RUNS = 'RESET_RUNS'
export const FETCH_LATEST_RUNS = 'FETCH_LATEST_RUNS'
export const FETCH_LATEST_RUNS_SUCCESS = 'FETCH_LATEST_RUNS_SUCCESS'

export const FETCH_NAMESPACES_SUCCESS = 'FETCH_NAMESPACES_SUCCESS'
export const SELECT_NAMESPACE = 'SELECT_NAMESPACE'
Expand Down
22 changes: 20 additions & 2 deletions web/src/store/actionCreators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,19 +296,37 @@ export const deleteJobSuccess = (jobName: string) => ({
},
})

export const fetchRuns = (jobName: string, namespace: string) => ({
export const fetchRuns = (jobName: string, namespace: string, limit: number, offset: number) => ({
type: actionTypes.FETCH_RUNS,
payload: {
jobName,
namespace,
limit,
offset,
},
})

export const fetchRunsSuccess = (jobName: string, jobRuns: Run[]) => ({
export const fetchRunsSuccess = (jobName: string, jobRuns: Run[], totalCount: number) => ({
type: actionTypes.FETCH_RUNS_SUCCESS,
payload: {
jobName,
runs: jobRuns,
totalCount,
},
})

export const fetchLatestRuns = (jobName: string, namespace: string) => ({
type: actionTypes.FETCH_LATEST_RUNS,
payload: {
jobName,
namespace,
},
})

export const fetchLatestRunsSuccess = (jobRuns: Run[]) => ({
type: actionTypes.FETCH_LATEST_RUNS_SUCCESS,
payload: {
runs: jobRuns,
},
})

Expand Down
Loading

0 comments on commit 2015c39

Please sign in to comment.