Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update dataset tags to allow editing/addition of tags #2759

Merged
merged 3 commits into from
Mar 5, 2024
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
215 changes: 193 additions & 22 deletions web/src/components/datasets/DatasetTags.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
// Copyright 2018-2024 contributors to the Marquez project
// SPDX-License-Identifier: Apache-2.0
import * as Redux from 'redux'
import { Autocomplete, TextField } from '@mui/material'
import { Box, createTheme } from '@mui/material'
import { IState } from '../../store/reducers'
import { Tag } from '../../types/api'
import {
addDatasetFieldTag,
addDatasetTag,
addTags,
deleteDatasetFieldTag,
deleteDatasetTag,
fetchTags,
} from '../../store/actionCreators'
import { bindActionCreators } from 'redux'
import { connect, useSelector } from 'react-redux'
import { useTheme } from '@emotion/react'
import AddIcon from '@mui/icons-material/Add'
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'
import Button from '@mui/material/Button'
import ButtonGroup from '@mui/material/ButtonGroup'
import Chip from '@mui/material/Chip'
import ClickAwayListener from '@mui/material/ClickAwayListener'
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 EditNoteIcon from '@mui/icons-material/EditNote'
import FormControl from '@mui/material/FormControl'
import IconButton from '@mui/material/IconButton'
import Grow from '@mui/material/Grow'
import MQText from '../core/text/MqText'
import MQTooltip from '../core/tooltip/MQTooltip'
import MenuItem from '@mui/material/MenuItem'
import React, { useEffect, useState } from 'react'
import MenuList from '@mui/material/MenuList'
import Paper from '@mui/material/Paper'
import Popper from '@mui/material/Popper'
import React, { useRef, useState } from 'react'
import Select from '@mui/material/Select'
import Snackbar from '@mui/material/Snackbar'

interface DatasetTagsProps {
namespace: string
Expand All @@ -41,7 +50,7 @@ interface DispatchProps {
addDatasetTag: typeof addDatasetTag
deleteDatasetFieldTag: typeof deleteDatasetFieldTag
addDatasetFieldTag: typeof addDatasetFieldTag
fetchTags: typeof fetchTags
addTags: typeof addTags
}

type IProps = DatasetTagsProps & DispatchProps
Expand All @@ -55,20 +64,60 @@ const DatasetTags: React.FC<IProps> = (props) => {
addDatasetTag,
deleteDatasetFieldTag,
addDatasetFieldTag,
fetchTags,
datasetField,
addTags,
} = props

const [isDialogOpen, setDialogOpen] = useState(false)
const [listTag, setListTag] = useState('')

const openDialog = () => setDialogOpen(true)
const closeDialog = () => setDialogOpen(false)
const i18next = require('i18next')
const options = ['Add a Tag', 'Edit a Tag Description']
const [openDropDown, setOpenDropDown] = useState(false)
const [openTagDesc, setOpenTagDesc] = useState(false)
const anchorRef = useRef<HTMLDivElement>(null)
const [selectedIndex, setSelectedIndex] = useState(0)
const [tagDescription, setTagDescription] = useState('No Description')
const handleButtonClick = () => {
options[selectedIndex] === 'Add a Tag' ? setDialogOpen(true) : setOpenTagDesc(true)
}
const [snackbarOpen, setSnackbarOpen] = useState(false)
const theme = createTheme(useTheme())

useEffect(() => {
fetchTags()
}, [])
const handleMenuItemClick = (
_event: React.MouseEvent<HTMLLIElement, MouseEvent>,
index: number
) => {
setSelectedIndex(index)
setOpenDropDown(false)
}

const handleDropDownToggle = () => {
setOpenDropDown((prevprevOpenDropDown) => !prevprevOpenDropDown)
}

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

const handleDropDownClose = (event: Event) => {
if (anchorRef.current && anchorRef.current.contains(event.target as HTMLElement)) {
return
}
setOpenDropDown(false)
}

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

const handleDescriptionChange = (event: any) => {
setTagDescription(event.target.value)
}

const tagData = useSelector((state: IState) => state.tags.tags)

Expand All @@ -88,8 +137,15 @@ const DatasetTags: React.FC<IProps> = (props) => {
: deleteDatasetTag(namespace, datasetName, deletedTag)
}

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

const formatTags = (tags: string[], tag_desc: Tag[]) => {
const theme = createTheme(useTheme())
return tags.map((tag) => {
const tagDescription = tag_desc.find((tagItem) => tagItem.name === tag)
const tooltipTitle = tagDescription?.description || 'No Tag Description'
Expand All @@ -112,22 +168,81 @@ const DatasetTags: React.FC<IProps> = (props) => {

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'}>
<MQText subheading>{i18next.t('dataset_tags.tags')}</MQText>
{formatTags(datasetTags, tagData)}
<MQTooltip placement='top' title={i18next.t('dataset_tags.tooltip')} key='tag-tooltip'>
<IconButton
onClick={openDialog}
<ButtonGroup
variant='contained'
ref={anchorRef}
aria-label='tags-nested-menu'
sx={{ height: '32px', width: '20px', marginLeft: '8px' }}
>
<MQTooltip placement='left' title={options[selectedIndex]}>
<Button
variant='outlined'
sx={{ height: '32px', width: '20px' }}
onClick={handleButtonClick}
>
{selectedIndex === 0 ? <AddIcon /> : <EditNoteIcon />}
</Button>
</MQTooltip>
<Button
variant='outlined'
size='small'
color='primary'
sx={{ m: 1 }}
aria-label='add'
aria-controls={openDropDown ? 'split-button-menu' : undefined}
aria-expanded={openDropDown ? 'true' : undefined}
aria-label='tags-menu'
aria-haspopup='menu'
onClick={handleDropDownToggle}
>
<AddIcon fontSize='small' color='primary' />
</IconButton>
</MQTooltip>
<ArrowDropDownIcon />
</Button>
</ButtonGroup>
</Box>

<Popper
sx={{
zIndex: 1,
}}
open={openDropDown}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper>
<ClickAwayListener onClickAway={handleDropDownClose}>
<MenuList id='split-button-menu' autoFocusItem>
{options.map((option, index) => (
<MenuItem
key={option}
selected={index === selectedIndex}
disabled={index === 1 && !!datasetField}
onClick={(event) => handleMenuItemClick(event, index)}
>
{option}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
<Dialog open={isDialogOpen} onClose={closeDialog} fullWidth maxWidth='sm'>
<DialogTitle>{i18next.t('dataset_tags.dialogtitle')}</DialogTitle>
<DialogContent>
Expand Down Expand Up @@ -168,18 +283,74 @@ const DatasetTags: React.FC<IProps> = (props) => {
</Button>
</DialogActions>
</Dialog>
<Dialog open={openTagDesc} fullWidth maxWidth='sm'>
<DialogTitle>Select a Tag to change</DialogTitle>
<DialogContent>
<MQText subheading>Tag</MQText>
<Autocomplete
options={tagData.map((option) => option.name)}
freeSolo
autoSelect
onChange={handleTagDescChange}
renderInput={(params) => (
<TextField
{...params}
placeholder={'Search for a Tag...or enter a new one.'}
autoFocus
margin='dense'
id='tag'
fullWidth
variant='outlined'
InputLabelProps={{
...params.InputProps,
shrink: false,
}}
/>
)}
/>
<MQText subheading bottomMargin>
Description
</MQText>
<TextField
autoFocus
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(
{
fetchTags: fetchTags,
deleteDatasetTag: deleteDatasetTag,
addDatasetTag: addDatasetTag,
deleteDatasetFieldTag: deleteDatasetFieldTag,
addDatasetFieldTag: addDatasetFieldTag,
addTags: addTags,
},
dispatch
)
Expand Down
3 changes: 3 additions & 0 deletions web/src/helpers/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ export const theme = createTheme({
main: '#454f5b',
},
},
zIndex: {
snackbar: 9999,
},
})

export const THEME_EXTRA = {
Expand Down
2 changes: 2 additions & 0 deletions web/src/store/actionCreators/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export const RESET_FACETS = 'RESET_FACETS'
// tags
export const FETCH_TAGS = 'FETCH_TAGS'
export const FETCH_TAGS_SUCCESS = 'FETCH_TAGS_SUCCESS'
export const ADD_TAGS = 'ADD_TAGS'
export const ADD_TAGS_SUCCESS = 'ADD_TAGS_SUCCESS'

// column lineage
export const FETCH_COLUMN_LINEAGE = 'FETCH_COLUMN_LINEAGE'
Expand Down
12 changes: 12 additions & 0 deletions web/src/store/actionCreators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,18 @@ export const fetchTagsSuccess = (tags: Tag[]) => ({
},
})

export const addTags = (tag: string, description: string) => ({
type: actionTypes.ADD_TAGS,
payload: {
tag,
description,
},
})

export const addTagsSuccess = () => ({
type: actionTypes.ADD_TAGS_SUCCESS,
})

export const applicationError = (message: string) => ({
type: actionTypes.APPLICATION_ERROR,
payload: {
Expand Down
15 changes: 12 additions & 3 deletions web/src/store/reducers/tags.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
// Copyright 2018-2023 contributors to the Marquez project
// SPDX-License-Identifier: Apache-2.0
import { FETCH_TAGS, FETCH_TAGS_SUCCESS } from '../actionCreators/actionTypes'
import {
ADD_TAGS,
ADD_TAGS_SUCCESS,
FETCH_TAGS,
FETCH_TAGS_SUCCESS,
} from '../actionCreators/actionTypes'
import { Tag } from '../../types/api'
import { fetchTagsSuccess } from '../actionCreators'
import { addTagsSuccess, fetchTagsSuccess } from '../actionCreators'

export type ITagsState = { isLoading: boolean; tags: Tag[]; init: boolean }

Expand All @@ -12,7 +17,7 @@ export const initialState: ITagsState = {
tags: [],
}

type ITagsAction = ReturnType<typeof fetchTagsSuccess>
type ITagsAction = ReturnType<typeof fetchTagsSuccess> & ReturnType<typeof addTagsSuccess>

export default (state: ITagsState = initialState, action: ITagsAction): ITagsState => {
const { type, payload } = action
Expand All @@ -21,6 +26,10 @@ export default (state: ITagsState = initialState, action: ITagsAction): ITagsSta
return { ...state, isLoading: true }
case FETCH_TAGS_SUCCESS:
return { ...state, isLoading: false, init: true, tags: payload.tags }
case ADD_TAGS:
return { ...state }
case ADD_TAGS_SUCCESS:
return { ...state }
default:
return state
}
Expand Down
1 change: 1 addition & 0 deletions web/src/store/requests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const genericErrorMessageConstructor = (functionName: string, error: APIE
export interface IParams {
method: HttpMethod
body?: string
headers?: Record<string, string>
}

export const parseResponse = async (response: Response, functionName: string) => {
Expand Down
Loading
Loading