Skip to content

Commit 31dba43

Browse files
authored
Merge branch 'main' into web/add_paging_dataset_version_hist
2 parents d55cb79 + 2015c39 commit 31dba43

13 files changed

Lines changed: 265 additions & 32 deletions

File tree

api/src/main/java/marquez/api/JobResource.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,8 @@ public Response listRuns(
238238

239239
final List<Run> runs =
240240
runService.findAll(namespaceName.getValue(), jobName.getValue(), limit, offset);
241-
return Response.ok(new Runs(runs)).build();
241+
final int totalCount = jobService.countJobRuns(namespaceName.getValue(), jobName.getValue());
242+
return Response.ok(new Runs(runs, totalCount)).build();
242243
}
243244

244245
@Path("/jobs/runs/{id}")
@@ -329,5 +330,8 @@ public static class Runs {
329330
@NonNull
330331
@JsonProperty("runs")
331332
List<Run> value;
333+
334+
@JsonProperty("totalCount")
335+
int totalCount;
332336
}
333337
}

api/src/main/java/marquez/db/DatasetVersionDao.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,13 +278,19 @@ LEFT JOIN (
278278
WHERE dv.namespace_name = :namespaceName
279279
AND dv.dataset_name = :datasetName
280280
LIMIT :limit OFFSET :offset
281+
),
282+
dataset_symlinks_names as (
283+
SELECT DISTINCT dataset_uuid, name
284+
FROM dataset_symlinks
285+
WHERE NOT is_primary
281286
)
282287
SELECT
283288
type, name, physical_name, namespace_name, source_name, description, lifecycle_state,
284289
created_at, version, dataset_schema_version_uuid, fields, createdByRunUuid, schema_location,
285290
tags, dataset_version_uuid,
286291
JSONB_AGG(facets ORDER BY lineage_event_time ASC) AS facets
287292
FROM dataset_info
293+
WHERE name NOT IN (SELECT name FROM dataset_symlinks_names)
288294
GROUP BY type, name, physical_name, namespace_name, source_name, description, lifecycle_state,
289295
created_at, version, dataset_schema_version_uuid, fields, createdByRunUuid, schema_location,
290296
tags, dataset_version_uuid

api/src/main/java/marquez/db/JobDao.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,20 @@ job_tags as (
252252
@SqlQuery("SELECT count(*) FROM jobs_view AS j WHERE symlink_target_uuid IS NULL")
253253
int count();
254254

255+
@SqlQuery(
256+
"""
257+
select
258+
count(*)
259+
from
260+
runs
261+
where
262+
namespace_name = :namespaceName
263+
and
264+
job_name = :job
265+
;
266+
""")
267+
int countJobRuns(String namespaceName, String job);
268+
255269
@SqlQuery(
256270
"SELECT count(*) FROM jobs_view AS j WHERE j.namespace_name = :namespaceName\n"
257271
+ "AND symlink_target_uuid IS NULL")

api/src/test/java/marquez/DatasetIntegrationTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import static marquez.db.ColumnLineageTestUtils.getDatasetA;
99
import static marquez.db.ColumnLineageTestUtils.getDatasetB;
10+
import static marquez.db.LineageTestUtils.PRODUCER_URL;
11+
import static marquez.db.LineageTestUtils.SCHEMA_URL;
1012
import static org.assertj.core.api.Assertions.assertThat;
1113

1214
import com.fasterxml.jackson.core.type.TypeReference;
@@ -574,4 +576,57 @@ public void testApp_doesNotShowDeletedDatasetAfterUndeleteNamespace() throws IOE
574576
jobs = client.listJobs(namespaceName);
575577
assertThat(jobs).hasSize(1);
576578
}
579+
580+
@Test
581+
public void testApp_getTableVersionsWithSymlinks() {
582+
client.createDataset(NAMESPACE_NAME, DB_TABLE_NAME, DB_TABLE_META);
583+
584+
ImmutableMap<String, Object> outputFacets =
585+
ImmutableMap.of("outputFacetKey", "outputFacetValue");
586+
ImmutableMap<String, Object> inputFacets = ImmutableMap.of("inputFacetKey", "inputFacetValue");
587+
588+
final LineageEvent.DatasetFacets datasetFacets =
589+
LineageTestUtils.newDatasetFacet(
590+
outputFacets,
591+
LineageEvent.SchemaField.builder()
592+
.name("firstname")
593+
.type("string")
594+
.description("the first name")
595+
.build());
596+
datasetFacets
597+
.getDocumentation()
598+
.setDescription(DB_TABLE_META.getDescription().orElse("the dataset documentation"));
599+
datasetFacets.setSymlinks(
600+
new LineageEvent.DatasetSymlinkFacet(
601+
PRODUCER_URL,
602+
SCHEMA_URL,
603+
Collections.singletonList(
604+
new LineageEvent.SymlinkIdentifier("symlinkNamespace", "symlinkName", "type"))));
605+
final LineageEvent lineageEvent =
606+
LineageEvent.builder()
607+
.producer("testApp_getTableVersionsWithSymlinks")
608+
.eventType("COMPLETE")
609+
.run(
610+
new LineageEvent.Run(
611+
UUID.randomUUID().toString(), LineageEvent.RunFacet.builder().build()))
612+
.job(LineageEvent.Job.builder().namespace(NAMESPACE_NAME).name(JOB_NAME).build())
613+
.eventTime(ZonedDateTime.now())
614+
.inputs(
615+
Collections.singletonList(
616+
LineageEvent.Dataset.builder()
617+
.namespace(NAMESPACE_NAME)
618+
.name(DB_TABLE_NAME)
619+
.facets(datasetFacets)
620+
.build()))
621+
.outputs(Collections.emptyList())
622+
.build();
623+
final CompletableFuture<Integer> resp = sendEvent(lineageEvent);
624+
assertThat(resp.join()).isEqualTo(201);
625+
List<DatasetVersion> versions = client.listDatasetVersions(NAMESPACE_NAME, DB_TABLE_NAME);
626+
627+
versions.forEach(
628+
datasetVersion -> {
629+
assertThat(datasetVersion.getName()).isNotEqualTo("symlinkName");
630+
});
631+
}
577632
}

api/src/test/java/marquez/db/JobDaoTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ public void testCountFor() {
136136
assertThat(jobDao.count()).isEqualTo(4);
137137

138138
assertThat(jobDao.countFor(namespace.getName())).isEqualTo(3);
139+
assertThat(jobDao.countJobRuns(namespace.getName(), "targetJob")).isEqualTo(0);
139140
}
140141

141142
@Test

web/src/components/jobs/JobDetailPage.tsx

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
deleteJob,
2121
dialogToggle,
2222
fetchJobTags,
23-
fetchRuns,
23+
fetchLatestRuns,
2424
resetJobs,
2525
resetRuns,
2626
setTabIndex,
@@ -44,7 +44,7 @@ import Runs from './Runs'
4444
import SpeedRounded from '@mui/icons-material/SpeedRounded'
4545

4646
interface DispatchProps {
47-
fetchRuns: typeof fetchRuns
47+
fetchLatestRuns: typeof fetchLatestRuns
4848
resetRuns: typeof resetRuns
4949
resetJobs: typeof resetJobs
5050
deleteJob: typeof deleteJob
@@ -56,41 +56,42 @@ interface DispatchProps {
5656
type IProps = {
5757
job: LineageJob
5858
jobs: IState['jobs']
59-
runs: Run[]
60-
runsLoading: boolean
6159
display: IState['display']
6260
tabIndex: IState['lineage']['tabIndex']
6361
jobTags: string[]
62+
latestRuns: Run[]
63+
isLatestRunsLoading: boolean
6464
} & DispatchProps
6565

6666
const JobDetailPage: FunctionComponent<IProps> = (props) => {
6767
const theme = createTheme(useTheme())
6868
const {
6969
job,
7070
jobs,
71-
fetchRuns,
71+
fetchLatestRuns,
7272
resetRuns,
7373
deleteJob,
7474
dialogToggle,
75-
runs,
75+
latestRuns,
7676
display,
77-
runsLoading,
7877
tabIndex,
7978
setTabIndex,
8079
jobTags,
8180
fetchJobTags,
81+
isLatestRunsLoading,
8282
} = props
8383
const navigate = useNavigate()
8484
const [_, setSearchParams] = useSearchParams()
8585

8686
const handleChange = (event: ChangeEvent, newValue: number) => {
8787
setTabIndex(newValue)
8888
}
89+
8990
const i18next = require('i18next')
9091

9192
useEffect(() => {
92-
fetchRuns(job.name, job.namespace)
9393
fetchJobTags(job.namespace, job.name)
94+
fetchLatestRuns(job.name, job.namespace)
9495
}, [job.name])
9596

9697
useEffect(() => {
@@ -102,12 +103,13 @@ const JobDetailPage: FunctionComponent<IProps> = (props) => {
102103
// unmounting
103104
useEffect(() => {
104105
return () => {
105-
resetRuns()
106106
resetJobs()
107+
resetRuns()
108+
setTabIndex(0)
107109
}
108110
}, [])
109111

110-
if (runsLoading || jobs.isLoading) {
112+
if (jobs.isLoading || isLatestRunsLoading) {
111113
return (
112114
<Box display={'flex'} justifyContent={'center'} mt={2}>
113115
<CircularProgress color='primary' />
@@ -244,7 +246,7 @@ const JobDetailPage: FunctionComponent<IProps> = (props) => {
244246
<MqInfo
245247
icon={<DirectionsRun color={'disabled'} />}
246248
label={'Running Status'.toUpperCase()}
247-
value={<MqStatus label={job.latestRun?.state} color={jobRunsStatus(runs)} />}
249+
value={<MqStatus label={job.latestRun?.state} color={jobRunsStatus(latestRuns)} />}
248250
/>
249251
</Grid>
250252
</Grid>
@@ -271,14 +273,14 @@ const JobDetailPage: FunctionComponent<IProps> = (props) => {
271273
)
272274
)
273275
) : null}
274-
{tabIndex === 1 && <Runs runs={runs} />}
276+
{tabIndex === 1 && <Runs jobName={job.name} jobNamespace={job.namespace} />}
275277
</Box>
276278
)
277279
}
278280

279281
const mapStateToProps = (state: IState) => ({
280-
runs: state.runs.result,
281-
runsLoading: state.runs.isLoading,
282+
latestRuns: state.runs.latestRuns,
283+
isLatestRunsLoading: state.runs.isLatestRunsLoading,
282284
display: state.display,
283285
jobs: state.jobs,
284286
tabIndex: state.lineage.tabIndex,
@@ -288,7 +290,7 @@ const mapStateToProps = (state: IState) => ({
288290
const mapDispatchToProps = (dispatch: Redux.Dispatch) =>
289291
bindActionCreators(
290292
{
291-
fetchRuns: fetchRuns,
293+
fetchLatestRuns: fetchLatestRuns,
292294
resetRuns: resetRuns,
293295
resetJobs: resetJobs,
294296
deleteJob: deleteJob,

web/src/components/jobs/Runs.tsx

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,94 @@
11
// Copyright 2018-2023 contributors to the Marquez project
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import * as Redux from 'redux'
45
import { ArrowBackIosRounded } from '@mui/icons-material'
56
import {
67
Box,
78
Chip,
9+
CircularProgress,
810
IconButton,
911
Table,
1012
TableBody,
1113
TableCell,
1214
TableHead,
1315
TableRow,
1416
} from '@mui/material'
17+
import { IState } from '../../store/reducers'
1518
import { Run } from '../../types/api'
1619
import { alpha, createTheme } from '@mui/material/styles'
20+
import { bindActionCreators } from 'redux'
21+
import { connect } from 'react-redux'
22+
import { fetchRuns } from '../../store/actionCreators'
1723
import { formatUpdatedAt } from '../../helpers'
1824
import { runStateColor } from '../../helpers/nodes'
1925
import { stopWatchDuration } from '../../helpers/time'
2026
import { useTheme } from '@emotion/react'
2127
import MqCode from '../core/code/MqCode'
2228
import MqCopy from '../core/copy/MqCopy'
2329
import MqEmpty from '../core/empty/MqEmpty'
30+
import MqPaging from '../paging/MqPaging'
2431
import MqStatus from '../core/status/MqStatus'
2532
import MqText from '../core/text/MqText'
2633
import React, { FunctionComponent, SetStateAction } from 'react'
2734
import RunInfo from './RunInfo'
2835

36+
interface DispatchProps {
37+
fetchRuns: typeof fetchRuns
38+
}
39+
2940
interface RunsProps {
3041
runs: Run[]
3142
facets?: object
43+
totalCount: number
44+
runsLoading: boolean
45+
jobName: string
46+
jobNamespace: string
47+
}
48+
49+
interface RunsState {
50+
page: number
3251
}
3352

34-
const Runs: FunctionComponent<RunsProps> = (props) => {
35-
const { runs, facets } = props
53+
const PAGE_SIZE = 10
54+
55+
const Runs: FunctionComponent<RunsProps & DispatchProps> = (props) => {
56+
const { runs, facets, totalCount, runsLoading, fetchRuns, jobName, jobNamespace } = props
3657
const i18next = require('i18next')
37-
if (runs.length === 0) {
38-
return <MqEmpty title={i18next.t('jobs.empty_title')} body={i18next.t('jobs.empty_body')} />
39-
}
58+
59+
const [state, setState] = React.useState<RunsState>({
60+
page: 0,
61+
})
4062

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

68+
const handleClickPage = (direction: 'prev' | 'next') => {
69+
const directionPage = direction === 'next' ? state.page + 1 : state.page - 1
70+
window.scrollTo(0, 0)
71+
setState({ ...state, page: directionPage })
72+
}
73+
74+
React.useEffect(() => {
75+
fetchRuns(jobName, jobNamespace, PAGE_SIZE, state.page * PAGE_SIZE)
76+
}, [state.page])
77+
4678
const theme = createTheme(useTheme())
4779

80+
if (runs.length === 0) {
81+
return <MqEmpty title={i18next.t('jobs.empty_title')} body={i18next.t('jobs.empty_body')} />
82+
}
83+
84+
if (runsLoading) {
85+
return (
86+
<Box display={'flex'} justifyContent={'center'} mt={2}>
87+
<CircularProgress color='primary' />
88+
</Box>
89+
)
90+
}
91+
4892
if (infoView) {
4993
return (
5094
<>
@@ -148,12 +192,23 @@ const Runs: FunctionComponent<RunsProps> = (props) => {
148192
<TableCell align='left'>{formatUpdatedAt(run.createdAt)}</TableCell>
149193
<TableCell align='left'>{formatUpdatedAt(run.startedAt)}</TableCell>
150194
<TableCell align='left'>N/A</TableCell>
151-
<TableCell align='left'>{stopWatchDuration(run.durationMs)}</TableCell>
195+
<TableCell align='left'>
196+
{run.state === 'RUNNING' || run.state === 'NEW'
197+
? 'N/A'
198+
: stopWatchDuration(run.durationMs)}{' '}
199+
</TableCell>
152200
</TableRow>
153201
)
154202
})}
155203
</TableBody>
156204
</Table>
205+
<MqPaging
206+
pageSize={PAGE_SIZE}
207+
currentPage={state.page}
208+
totalCount={totalCount}
209+
incrementPage={() => handleClickPage('next')}
210+
decrementPage={() => handleClickPage('prev')}
211+
/>
157212
{facets && (
158213
<Box mt={2}>
159214
<Box mb={1}>
@@ -166,4 +221,18 @@ const Runs: FunctionComponent<RunsProps> = (props) => {
166221
)
167222
}
168223

169-
export default Runs
224+
const mapStateToProps = (state: IState) => ({
225+
runs: state.runs.result,
226+
totalCount: state.runs.totalCount,
227+
runsLoading: state.runs.isLoading,
228+
})
229+
230+
const mapDispatchToProps = (dispatch: Redux.Dispatch) =>
231+
bindActionCreators(
232+
{
233+
fetchRuns: fetchRuns,
234+
},
235+
dispatch
236+
)
237+
238+
export default connect(mapStateToProps, mapDispatchToProps)(Runs)

0 commit comments

Comments
 (0)