Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 DialogTitle from '@mui/material/DialogTitle'
import LocalOfferIcon from '@mui/icons-material/LocalOffer'
import MQText from '../core/text/MqText'
import MQTooltip from '../core/tooltip/MQTooltip'
import React, { 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('No Description')
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('No Description')
}

const handleTagDescChange = (_event: any, value: string) => {
const selectedTagData = tagData.find((tag) => tag.name === value)
setListTag(value)
setTagDescription(selectedTagData ? selectedTagData.description : 'No Description')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"No description" is kind of a description in and of itself, is this what you want out of this, would you prefer if it was just empty?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's my data hat talking - can leave blank.

}

const handleDescriptionChange = (event: any) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can have an explicit type here, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes good spot.

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('No Description')
}

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 ||
'No Tag 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()
}
}}
>
<DialogTitle>Select a Tag to change</DialogTitle>
<DialogContent>
<MQText subheading>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 subheading bottomMargin>
Description
</MQText>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking we may want to use the label prop on these, maybe it would work a bit better with the design system. I could be wrong.

<TextField
multiline
id='tag-description'
name='tag-description'
fullWidth
variant='outlined'
placeholder={'No Description'}
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