Skip to content

Commit

Permalink
update dataset tags to allow addition of new tags
Browse files Browse the repository at this point in the history
Signed-off-by: sharpd <davidsharp7@gmail.com>
  • Loading branch information
davidsharp7 committed Feb 29, 2024
1 parent 585068c commit f4e2617
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 29 deletions.
213 changes: 192 additions & 21 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,59 @@ 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)

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,6 +136,14 @@ 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) => {
Expand All @@ -112,22 +168,81 @@ const DatasetTags: React.FC<IProps> = (props) => {

return (
<>
<Snackbar
open={snackbarOpen}
autoHideDuration={500}
style={{ zIndex: 9999 }}
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: '30px', width: '20px', marginLeft: '5px' }}
>
<MQTooltip placement='left' title={options[selectedIndex]}>
<Button
variant='outlined'
sx={{ height: '30px', 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
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
23 changes: 15 additions & 8 deletions web/src/store/reducers/tags.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
// 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 }
export type ITagsState = { tags: Tag[] }

export const initialState: ITagsState = {
isLoading: false,
init: false,
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
switch (type) {
case FETCH_TAGS:
return { ...state, isLoading: true }
return { ...state }
case FETCH_TAGS_SUCCESS:
return { ...state, isLoading: false, init: true, tags: payload.tags }
return { ...state, 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

0 comments on commit f4e2617

Please sign in to comment.