Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion web/src/__tests__/reducers/jobs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('jobs reducer', () => {
jobs: jobs as Job[]
}
} as IJobsAction
expect(jobsReducer(initialState, action)).toStrictEqual({ isLoading: false, result: jobs, totalCount: 13, init: true, deletedJobName: '' })
expect(jobsReducer(initialState, action)).toStrictEqual({ isLoading: false, result: jobs, totalCount: 13, init: true, deletedJobName: '', jobTags:[] })
})
})

Expand Down
12 changes: 11 additions & 1 deletion web/src/components/jobs/JobDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { connect } from 'react-redux'
import {
deleteJob,
dialogToggle,
fetchJobTags,
fetchRuns,
resetJobs,
resetRuns,
Expand All @@ -34,6 +35,7 @@ import { useTheme } from '@emotion/react'
import CloseIcon from '@mui/icons-material/Close'
import Dialog from '../Dialog'
import IconButton from '@mui/material/IconButton'
import JobTags from './JobTags'
import MqEmpty from '../core/empty/MqEmpty'
import MqStatus from '../core/status/MqStatus'
import MqText from '../core/text/MqText'
Expand All @@ -48,6 +50,7 @@ interface DispatchProps {
deleteJob: typeof deleteJob
dialogToggle: typeof dialogToggle
setTabIndex: typeof setTabIndex
fetchJobTags: typeof fetchJobTags
}

type IProps = {
Expand All @@ -57,6 +60,7 @@ type IProps = {
runsLoading: boolean
display: IState['display']
tabIndex: IState['lineage']['tabIndex']
jobTags: string[]
} & DispatchProps

const JobDetailPage: FunctionComponent<IProps> = (props) => {
Expand All @@ -73,6 +77,8 @@ const JobDetailPage: FunctionComponent<IProps> = (props) => {
runsLoading,
tabIndex,
setTabIndex,
jobTags,
fetchJobTags,
} = props
const navigate = useNavigate()
const [_, setSearchParams] = useSearchParams()
Expand All @@ -84,6 +90,7 @@ const JobDetailPage: FunctionComponent<IProps> = (props) => {

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

useEffect(() => {
Expand All @@ -100,7 +107,7 @@ const JobDetailPage: FunctionComponent<IProps> = (props) => {
}
}, [])

if (runsLoading) {
if (runsLoading || jobs.isLoading) {
return (
<Box display={'flex'} justifyContent={'center'} mt={2}>
<CircularProgress color='primary' />
Expand Down Expand Up @@ -242,6 +249,7 @@ const JobDetailPage: FunctionComponent<IProps> = (props) => {
</Grid>
</Grid>
<Divider sx={{ my: 1 }} />
<JobTags jobTags={jobTags} jobName={job.name} namespace={job.namespace} />
<Box
mb={2}
display={'flex'}
Expand Down Expand Up @@ -274,6 +282,7 @@ const mapStateToProps = (state: IState) => ({
display: state.display,
jobs: state.jobs,
tabIndex: state.lineage.tabIndex,
jobTags: state.jobs.jobTags,
})

const mapDispatchToProps = (dispatch: Redux.Dispatch) =>
Expand All @@ -285,6 +294,7 @@ const mapDispatchToProps = (dispatch: Redux.Dispatch) =>
deleteJob: deleteJob,
dialogToggle: dialogToggle,
setTabIndex: setTabIndex,
fetchJobTags: fetchJobTags,
},
dispatch
)
Expand Down
290 changes: 290 additions & 0 deletions web/src/components/jobs/JobTags.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
// Copyright 2018-2024 contributors to the Marquez project
// SPDX-License-Identifier: Apache-2.0
import * as Redux from 'redux'
import {
Autocomplete,
AutocompleteChangeDetails,
AutocompleteChangeReason,
Checkbox,
TextField,
} from '@mui/material'
import { Box, createTheme } from '@mui/material'
import { IState } from '../../store/reducers'
import { Tag } from '../../types/api'
import { addJobTag, addTags, deleteJobTag } from '../../store/actionCreators'
import { bindActionCreators } from 'redux'
import { connect, useSelector } from 'react-redux'
import { useTheme } from '@emotion/react'
import Button from '@mui/material/Button'
import CheckBoxIcon from '@mui/icons-material/CheckBox'
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'
import Chip from '@mui/material/Chip'
import Dialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions'
import DialogContent from '@mui/material/DialogContent'
import LocalOfferIcon from '@mui/icons-material/LocalOffer'
import MQText from '../core/text/MqText'
import MQTooltip from '../core/tooltip/MQTooltip'
import React, { ChangeEvent, useState } from 'react'
import Snackbar from '@mui/material/Snackbar'

interface JobTagsProps {
namespace: string
jobName: string
jobTags: string[]
}

interface DispatchProps {
deleteJobTag: typeof deleteJobTag
addJobTag: typeof addJobTag
addTags: typeof addTags
}

type IProps = JobTagsProps & DispatchProps

const JobTags: React.FC<IProps> = (props) => {
const { namespace, jobName, jobTags, deleteJobTag, addJobTag, addTags } = props

const [listTag, setListTag] = useState('')
const [openTagDesc, setOpenTagDesc] = useState(false)
const [tagDescription, setTagDescription] = useState('')
const [selectedTags, setSelectedTags] = useState<string[]>(jobTags)

const handleButtonClick = () => {
setOpenTagDesc(true)
}

const [snackbarOpen, setSnackbarOpen] = useState(false)
const theme = createTheme(useTheme())

const handleTagDescClose = () => {
setOpenTagDesc(false)
setListTag('')
setTagDescription('')
}

const handleTagDescChange = (_event: ChangeEvent<HTMLInputElement>, value: string) => {
const selectedTagData = tagData.find((tag) => tag.name === value)
setListTag(value)
setTagDescription(selectedTagData ? selectedTagData.description : '')
}

const handleDescriptionChange = (event: ChangeEvent<HTMLInputElement>) => {
setTagDescription(event.target.value)
}

const tagData = useSelector((state: IState) =>
state.tags.tags.sort((a, b) => a.name.localeCompare(b.name))
)

const handleTagChange = (
_event: React.SyntheticEvent,
_value: string[],
_reason: AutocompleteChangeReason,
details?: AutocompleteChangeDetails<string> | undefined
) => {
if (details && _reason === 'removeOption') {
const newTag = details.option
const newSelectedTags = selectedTags.filter((tag) => newTag !== tag)
setSelectedTags(newSelectedTags)
deleteJobTag(namespace, jobName, newTag)
} else if (details && !selectedTags.includes(details.option)) {
const newTag = details.option
const newSelectedTags = [...selectedTags, newTag]
setSelectedTags(newSelectedTags)
addJobTag(namespace, jobName, newTag)
}
}

const handleDelete = (deletedTag: string) => {
const newSelectedTags = selectedTags.filter((tag) => deletedTag !== tag)

setSelectedTags(newSelectedTags)

deleteJobTag(namespace, jobName, deletedTag)
}

const addTag = () => {
addTags(listTag, tagDescription)
setSnackbarOpen(true)
setOpenTagDesc(false)
setListTag('')
setTagDescription('')
}

const formatTags = (tags: string[], tag_desc: Tag[]) => {
return tags.map((tag, index) => {
const tagDescription = tag_desc.find((tagItem) => tagItem.name === tag)
const tooltipTitle = tagDescription?.description || 'No Tag Description'
return (
<MQTooltip title={tooltipTitle} key={tag}>
<Chip
color={'primary'}
variant='outlined'
label={tag}
size='small'
onDelete={() => handleDelete(tag)}
style={{
display: 'row',
marginLeft: index === 0 ? theme.spacing(0) : theme.spacing(1),
}}
/>
</MQTooltip>
)
})
}

return (
<>
<Snackbar
open={snackbarOpen}
autoHideDuration={1000}
style={{ zIndex: theme.zIndex.snackbar }}
onClose={() => setSnackbarOpen(false)}
message={'Tag updated.'}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
/>
<Box display={'flex'} alignItems='center' justifyContent='center' width={'100%'}>
<MQTooltip title='Edit a Tag' key='edit-tag'>
<Button
variant='outlined'
onClick={handleButtonClick}
color='primary'
sx={{ marginRight: '8px' }}
style={{ paddingTop: '6.75px', paddingBottom: '6.75px' }}
startIcon={<LocalOfferIcon />}
>
Edit Tag
</Button>
</MQTooltip>
<Autocomplete
multiple
disableCloseOnSelect
id='dataset-tags'
sx={{ flex: 1, width: 'auto' }}
limitTags={6}
autoHighlight
disableClearable
disablePortal
options={tagData.map((option) => option.name)}
value={selectedTags}
onChange={handleTagChange}
renderTags={(value: string[]) => formatTags(value, tagData)}
renderOption={(props, option, { selected }) => (
<li {...props}>
<Checkbox
icon={<CheckBoxOutlineBlankIcon fontSize='small' />}
checkedIcon={<CheckBoxIcon fontSize='small' />}
style={{ marginRight: 4 }}
checked={selected}
/>
<div>
<MQText bold>{option}</MQText>
<MQText subdued overflowHidden>
{tagData.find((tagItem) => tagItem.name === option)?.description || ''}
</MQText>
</div>
</li>
)}
renderInput={(params) => (
<TextField
variant={'outlined'}
{...params}
placeholder={selectedTags.length > 0 ? '' : 'Search Tags'}
InputProps={{
...params.InputProps,
}}
InputLabelProps={{
shrink: true,
}}
size='small'
/>
)}
/>
</Box>
<Dialog
PaperProps={{
sx: { backgroundColor: theme.palette.background.default, backgroundImage: 'none' },
}}
open={openTagDesc}
fullWidth
maxWidth='sm'
onKeyDown={(event) => {
if (event.key === 'Escape') {
handleTagDescClose()
}
}}
>
<DialogContent>
<MQText label sx={{ fontSize: '1.25rem' }} bottomMargin>
Select a Tag to change
</MQText>
<MQText label sx={{ fontSize: '0.85rem' }}>Tag</MQText>
<Autocomplete
options={tagData.map((option) => option.name)}
autoSelect
freeSolo
fullWidth
autoFocus
forcePopupIcon
onChange={handleTagDescChange}
renderInput={(params) => (
<TextField
{...params}
placeholder={'Search for a Tag...or enter a new one.'}
autoFocus
margin='dense'
id='tag'
variant='outlined'
InputLabelProps={{
...params.InputProps,
shrink: false,
}}
/>
)}
/>
<MQText label sx={{ fontSize: '0.85rem' }} bottomMargin>
Description
</MQText>
<TextField
multiline
id='tag-description'
name='tag-description'
fullWidth
variant='outlined'
placeholder={''}
onChange={handleDescriptionChange}
rows={6}
value={tagDescription}
InputProps={{
style: { padding: '12px 16px' },
}}
InputLabelProps={{
shrink: false,
}}
/>
</DialogContent>
<DialogActions>
<Button color='primary' onClick={addTag} disabled={listTag === ''}>
Submit
</Button>
<Button color='primary' onClick={handleTagDescClose}>
Cancel
</Button>
</DialogActions>
</Dialog>
</>
)
}

const mapDispatchToProps = (dispatch: Redux.Dispatch) =>
bindActionCreators(
{
deleteJobTag: deleteJobTag,
addJobTag: addJobTag,
addTags: addTags,
},
dispatch
)

export default connect(null, mapDispatchToProps)(JobTags)
6 changes: 6 additions & 0 deletions web/src/store/actionCreators/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ export const FETCH_JOBS_SUCCESS = 'FETCH_JOBS_SUCCESS'
export const RESET_JOBS = 'RESET_JOBS'
export const DELETE_JOB = 'DELETE_JOB'
export const DELETE_JOB_SUCCESS = 'DELETE_JOB_SUCCESS'
export const FETCH_JOB_TAGS = 'FETCH_JOB_TAGS'
export const FETCH_JOB_TAGS_SUCCESS = 'FETCH_JOB_TAGS_SUCCESS'
export const ADD_JOB_TAG = 'ADD_JOB_TAG'
export const ADD_JOB_TAG_SUCCESS = 'ADD_JOB_TAG_SUCCESS'
export const DELETE_JOB_TAG = 'DELETE_JOB_TAG'
export const DELETE_JOB_TAG_SUCCESS = 'DELETE_JOB_TAG_SUCCESS'

// datasets
export const FETCH_DATASETS = 'FETCH_DATASETS'
Expand Down
Loading