From f4e261717a9f5c72ba6b8143eee3e3e330a76fb7 Mon Sep 17 00:00:00 2001 From: sharpd Date: Fri, 1 Mar 2024 10:17:39 +1300 Subject: [PATCH] update dataset tags to allow addition of new tags Signed-off-by: sharpd --- web/src/components/datasets/DatasetTags.tsx | 213 ++++++++++++++++++-- web/src/store/actionCreators/actionTypes.ts | 2 + web/src/store/actionCreators/index.ts | 12 ++ web/src/store/reducers/tags.ts | 23 ++- web/src/store/requests/index.ts | 1 + web/src/store/requests/tags.ts | 18 ++ web/src/store/sagas/index.ts | 17 ++ 7 files changed, 257 insertions(+), 29 deletions(-) diff --git a/web/src/components/datasets/DatasetTags.tsx b/web/src/components/datasets/DatasetTags.tsx index 27ee8b2c9d..5ebd404666 100644 --- a/web/src/components/datasets/DatasetTags.tsx +++ b/web/src/components/datasets/DatasetTags.tsx @@ -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 @@ -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 @@ -55,20 +64,59 @@ const DatasetTags: React.FC = (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(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, + 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) @@ -88,6 +136,14 @@ const DatasetTags: React.FC = (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) => { @@ -112,22 +168,81 @@ const DatasetTags: React.FC = (props) => { return ( <> + setSnackbarOpen(false)} + message={'Tag updated.'} + anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }} + /> {i18next.t('dataset_tags.tags')} {formatTags(datasetTags, tagData)} - - + + + + + - + + {({ TransitionProps, placement }) => ( + + + + + {options.map((option, index) => ( + handleMenuItemClick(event, index)} + > + {option} + + ))} + + + + + )} + {i18next.t('dataset_tags.dialogtitle')} @@ -168,6 +283,62 @@ const DatasetTags: React.FC = (props) => { + + Select a Tag to change + + Tag + option.name)} + freeSolo + autoSelect + onChange={handleTagDescChange} + renderInput={(params) => ( + + )} + /> + + Description + + + + + + + + ) } @@ -175,11 +346,11 @@ const DatasetTags: React.FC = (props) => { const mapDispatchToProps = (dispatch: Redux.Dispatch) => bindActionCreators( { - fetchTags: fetchTags, deleteDatasetTag: deleteDatasetTag, addDatasetTag: addDatasetTag, deleteDatasetFieldTag: deleteDatasetFieldTag, addDatasetFieldTag: addDatasetFieldTag, + addTags: addTags, }, dispatch ) diff --git a/web/src/store/actionCreators/actionTypes.ts b/web/src/store/actionCreators/actionTypes.ts index ea5dd20c04..524b455513 100644 --- a/web/src/store/actionCreators/actionTypes.ts +++ b/web/src/store/actionCreators/actionTypes.ts @@ -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' diff --git a/web/src/store/actionCreators/index.ts b/web/src/store/actionCreators/index.ts index 9e73eae55b..360f00e09e 100644 --- a/web/src/store/actionCreators/index.ts +++ b/web/src/store/actionCreators/index.ts @@ -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: { diff --git a/web/src/store/reducers/tags.ts b/web/src/store/reducers/tags.ts index d4bf54583d..4e2070eda6 100644 --- a/web/src/store/reducers/tags.ts +++ b/web/src/store/reducers/tags.ts @@ -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 +type ITagsAction = ReturnType & ReturnType 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 } diff --git a/web/src/store/requests/index.ts b/web/src/store/requests/index.ts index db7c49d941..9caf958873 100644 --- a/web/src/store/requests/index.ts +++ b/web/src/store/requests/index.ts @@ -11,6 +11,7 @@ export const genericErrorMessageConstructor = (functionName: string, error: APIE export interface IParams { method: HttpMethod body?: string + headers?: Record } export const parseResponse = async (response: Response, functionName: string) => { diff --git a/web/src/store/requests/tags.ts b/web/src/store/requests/tags.ts index a4345e9e59..31ef759e23 100644 --- a/web/src/store/requests/tags.ts +++ b/web/src/store/requests/tags.ts @@ -8,3 +8,21 @@ export const getTags = async () => { const url = `${API_URL}/tags` return genericFetchWrapper(url, { method: 'GET' }, 'fetchTags') } + +export const addTags = async (tag: string, description: string) => { + const url = `${API_URL}/tags/${tag}` + const payload = { + description: description, + } + return genericFetchWrapper( + url, + { + method: 'PUT', + body: JSON.stringify(payload), + headers: { + 'Content-Type': 'application/json', + }, + }, + 'addTags' + ) +} diff --git a/web/src/store/sagas/index.ts b/web/src/store/sagas/index.ts index 96770e1a15..0e36113c73 100644 --- a/web/src/store/sagas/index.ts +++ b/web/src/store/sagas/index.ts @@ -5,6 +5,7 @@ import * as Effects from 'redux-saga/effects' import { ADD_DATASET_FIELD_TAG, ADD_DATASET_TAG, + ADD_TAGS, DELETE_DATASET, DELETE_DATASET_FIELD_TAG, DELETE_DATASET_TAG, @@ -43,6 +44,7 @@ import { Search } from '../../types/api' import { addDatasetFieldTag, addDatasetTag, + addTags, deleteDataset, deleteDatasetFieldTag, deleteDatasetTag, @@ -61,6 +63,7 @@ import { import { addDatasetFieldTagSuccess, addDatasetTagSuccess, + addTagsSuccess, applicationError, deleteDatasetFieldTagSuccess, deleteDatasetSuccess, @@ -316,6 +319,19 @@ export function* addDatasetFieldTagSaga() { } } +export function* addTagsSaga() { + while (true) { + try { + const { payload } = yield take(ADD_TAGS) + yield call(addTags, payload.tag, payload.description) + yield put(addTagsSuccess()) + yield call(fetchTags) + } catch (e) { + yield put(applicationError('Something went wrong while adding a tag.')) + } + } +} + export function* fetchDatasetVersionsSaga() { while (true) { try { @@ -376,6 +392,7 @@ export default function* rootSaga(): Generator { addDatasetTagSaga(), deleteDatasetFieldTagSaga(), addDatasetFieldTagSaga(), + addTagsSaga(), ] yield all([...sagasThatAreKickedOffImmediately, ...sagasThatWatchForAction])