From 5bb09acdf3a70adbc636bcf1beae0792109c9377 Mon Sep 17 00:00:00 2001 From: sharpd Date: Thu, 7 Mar 2024 16:43:06 +1300 Subject: [PATCH 01/15] add key down escape for dialogs Signed-off-by: sharpd --- web/src/components/datasets/DatasetTags.tsx | 23 +++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/web/src/components/datasets/DatasetTags.tsx b/web/src/components/datasets/DatasetTags.tsx index 245214fa3c..9cef895dc3 100644 --- a/web/src/components/datasets/DatasetTags.tsx +++ b/web/src/components/datasets/DatasetTags.tsx @@ -243,7 +243,17 @@ const DatasetTags: React.FC = (props) => { )} - + { + if (event.key === 'Escape') { + closeDialog() + } + }} + > {i18next.t('dataset_tags.dialogtitle')} @@ -283,7 +293,16 @@ const DatasetTags: React.FC = (props) => { - + { + if (event.key === 'Escape') { + handleTagDescClose() + } + }} + > Select a Tag to change Tag From f543b32281a2d52874e7324cceea2afe66e8de52 Mon Sep 17 00:00:00 2001 From: sharpd Date: Thu, 7 Mar 2024 18:18:04 +1300 Subject: [PATCH 02/15] add truthy condition of refreshTags Signed-off-by: sharpd --- web/src/components/datasets/DatasetDetailPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/datasets/DatasetDetailPage.tsx b/web/src/components/datasets/DatasetDetailPage.tsx index ac07f0778f..fcd1922188 100644 --- a/web/src/components/datasets/DatasetDetailPage.tsx +++ b/web/src/components/datasets/DatasetDetailPage.tsx @@ -91,7 +91,7 @@ const DatasetDetailPage: FunctionComponent = (props) => { useEffect(() => { fetchDatasetVersions(lineageDataset.namespace, lineageDataset.name) - }, [lineageDataset.name, datasets.refreshTags]) + }, [lineageDataset.name, datasets.refreshTags === true]) // if the dataset is deleted then redirect to datasets end point useEffect(() => { From 864974edb585dae85bc3c4ffd25b39830abd8966 Mon Sep 17 00:00:00 2001 From: sharpd Date: Fri, 8 Mar 2024 08:16:19 +1300 Subject: [PATCH 03/15] fix freshTags state Signed-off-by: sharpd --- .../components/datasets/DatasetDetailPage.tsx | 2 +- web/src/store/actionCreators/index.ts | 18 +++++++++++++---- web/src/store/reducers/datasets.ts | 20 +++++++++---------- web/src/store/sagas/index.ts | 16 +++++++-------- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/web/src/components/datasets/DatasetDetailPage.tsx b/web/src/components/datasets/DatasetDetailPage.tsx index fcd1922188..ac07f0778f 100644 --- a/web/src/components/datasets/DatasetDetailPage.tsx +++ b/web/src/components/datasets/DatasetDetailPage.tsx @@ -91,7 +91,7 @@ const DatasetDetailPage: FunctionComponent = (props) => { useEffect(() => { fetchDatasetVersions(lineageDataset.namespace, lineageDataset.name) - }, [lineageDataset.name, datasets.refreshTags === true]) + }, [lineageDataset.name, datasets.refreshTags]) // if the dataset is deleted then redirect to datasets end point useEffect(() => { diff --git a/web/src/store/actionCreators/index.ts b/web/src/store/actionCreators/index.ts index 360f00e09e..e76a62fd4d 100644 --- a/web/src/store/actionCreators/index.ts +++ b/web/src/store/actionCreators/index.ts @@ -118,10 +118,12 @@ export const deleteDatasetTag = (namespace: string, datasetName: string, tag: st }, }) -export const deleteDatasetTagSuccess = (datasetName: string) => ({ +export const deleteDatasetTagSuccess = (namespace:string, datasetName: string, tag:string) => ({ type: actionTypes.DELETE_DATASET_TAG_SUCCESS, payload: { datasetName, + namespace, + tag }, }) @@ -140,10 +142,13 @@ export const deleteDatasetFieldTag = ( }, }) -export const deleteDatasetFieldTagSuccess = (datasetName: string) => ({ +export const deleteDatasetFieldTagSuccess = (namespace:string, datasetName: string, field:string, tag:string) => ({ type: actionTypes.DELETE_DATASET_FIELD_TAG_SUCCESS, payload: { datasetName, + namespace, + tag, + field }, }) @@ -156,10 +161,12 @@ export const addDatasetTag = (namespace: string, datasetName: string, tag: strin }, }) -export const addDatasetTagSuccess = (datasetName: string) => ({ +export const addDatasetTagSuccess = (namespace: string, datasetName: string, tag: string) => ({ type: actionTypes.ADD_DATASET_TAG_SUCCESS, payload: { datasetName, + namespace, + tag }, }) @@ -178,10 +185,13 @@ export const addDatasetFieldTag = ( }, }) -export const addDatasetFieldTagSuccess = (datasetName: string) => ({ +export const addDatasetFieldTagSuccess = (namespace: string, datasetName: string, field:string, tag: string) => ({ type: actionTypes.ADD_DATASET_FIELD_TAG_SUCCESS, payload: { datasetName, + namespace, + field, + tag }, }) diff --git a/web/src/store/reducers/datasets.ts b/web/src/store/reducers/datasets.ts index 1952674430..a072439251 100644 --- a/web/src/store/reducers/datasets.ts +++ b/web/src/store/reducers/datasets.ts @@ -32,7 +32,7 @@ export type IDatasetsState = { totalCount: number init: boolean deletedDatasetName: string - refreshTags: boolean + refreshTags: string } export const initialState: IDatasetsState = { @@ -41,7 +41,7 @@ export const initialState: IDatasetsState = { result: [], totalCount: 0, deletedDatasetName: '', - refreshTags: false, + refreshTags: '', } export type IDatasetsAction = ReturnType & @@ -72,21 +72,21 @@ export default (state: IDatasetsState = initialState, action: IDatasetsAction): case DELETE_DATASET_SUCCESS: return { ...state, deletedDatasetName: payload.datasetName } case DELETE_DATASET_TAG: - return { ...state, refreshTags: false } + return { ...state } case DELETE_DATASET_TAG_SUCCESS: - return { ...state, refreshTags: true } + return { ...state, refreshTags: `${payload.namespace}#${payload.datasetName}#${payload.tag}#d` } case DELETE_DATASET_FIELD_TAG: - return { ...state, refreshTags: false } + return { ...state} case DELETE_DATASET_FIELD_TAG_SUCCESS: - return { ...state, refreshTags: true } + return { ...state, refreshTags: `${payload.namespace}#${payload.datasetName}#${payload.field}#${payload.tag}#d`} case ADD_DATASET_TAG: - return { ...state, refreshTags: false } + return { ...state } case ADD_DATASET_TAG_SUCCESS: - return { ...state, refreshTags: true } + return { ...state, refreshTags: `${payload.namespace}#${payload.datasetName}#${payload.tag}`} case ADD_DATASET_FIELD_TAG: - return { ...state, refreshTags: false } + return { ...state } case ADD_DATASET_FIELD_TAG_SUCCESS: - return { ...state, refreshTags: true } + return { ...state, refreshTags: `${payload.namespace}#${payload.datasetName}#${payload.field}#${payload.tag}`} default: return state } diff --git a/web/src/store/sagas/index.ts b/web/src/store/sagas/index.ts index 0e36113c73..638feb9076 100644 --- a/web/src/store/sagas/index.ts +++ b/web/src/store/sagas/index.ts @@ -253,13 +253,13 @@ export function* deleteDatasetTagSaga() { while (true) { try { const { payload } = yield take(DELETE_DATASET_TAG) - const dataset: Dataset = yield call( + yield call( deleteDatasetTag, payload.namespace, payload.datasetName, payload.tag ) - yield put(deleteDatasetTagSuccess(dataset.name)) + yield put(deleteDatasetTagSuccess(payload.namespace, payload.datasetName, payload.tag)) } catch (e) { yield put(applicationError('Something went wrong while removing tag from dataset')) } @@ -270,14 +270,14 @@ export function* deleteDatasetFieldTagSaga() { while (true) { try { const { payload } = yield take(DELETE_DATASET_FIELD_TAG) - const dataset: Dataset = yield call( + yield call( deleteDatasetFieldTag, payload.namespace, payload.datasetName, payload.tag, payload.field ) - yield put(deleteDatasetFieldTagSuccess(dataset.name)) + yield put(deleteDatasetFieldTagSuccess(payload.namespace, payload.datasetName, payload.field, payload.tag)) } catch (e) { yield put(applicationError('Something went wrong while removing tag from dataset field')) } @@ -288,13 +288,13 @@ export function* addDatasetTagSaga() { while (true) { try { const { payload } = yield take(ADD_DATASET_TAG) - const dataset: Dataset = yield call( + yield call( addDatasetTag, payload.namespace, payload.datasetName, payload.tag ) - yield put(addDatasetTagSuccess(dataset.name)) + yield put(addDatasetTagSuccess(payload.namespace, payload.datasetName, payload.tag)) } catch (e) { yield put(applicationError('Something went wrong while adding tag to dataset')) } @@ -305,14 +305,14 @@ export function* addDatasetFieldTagSaga() { while (true) { try { const { payload } = yield take(ADD_DATASET_FIELD_TAG) - const dataset: Dataset = yield call( + yield call( addDatasetFieldTag, payload.namespace, payload.datasetName, payload.tag, payload.field ) - yield put(addDatasetFieldTagSuccess(dataset.name)) + yield put(addDatasetFieldTagSuccess(payload.namespace, payload.datasetName , payload.field, payload.tag)) } catch (e) { yield put(applicationError('Something went wrong while adding tag to dataset field.')) } From 2b6f9712fc9e48efcbc64f844cd33c04e18ec4c9 Mon Sep 17 00:00:00 2001 From: sharpd Date: Fri, 8 Mar 2024 10:09:08 +1300 Subject: [PATCH 04/15] fix failing test Signed-off-by: sharpd --- web/src/__tests__/reducers/datasets.test.ts | 2 +- web/src/components/datasets/DatasetTags.tsx | 1 + web/src/store/actionCreators/index.ts | 24 ++++++++++----- web/src/store/reducers/datasets.ts | 19 +++++++++--- web/src/store/sagas/index.ts | 34 ++++++++++++--------- 5 files changed, 52 insertions(+), 28 deletions(-) diff --git a/web/src/__tests__/reducers/datasets.test.ts b/web/src/__tests__/reducers/datasets.test.ts index 3724410ef8..483cccc45b 100644 --- a/web/src/__tests__/reducers/datasets.test.ts +++ b/web/src/__tests__/reducers/datasets.test.ts @@ -21,7 +21,7 @@ describe('datasets reducer', () => { result: datasets, totalCount: 16, deletedDatasetName: '', - refreshTags: false + refreshTags: '' }) }) diff --git a/web/src/components/datasets/DatasetTags.tsx b/web/src/components/datasets/DatasetTags.tsx index 9cef895dc3..8b5859b984 100644 --- a/web/src/components/datasets/DatasetTags.tsx +++ b/web/src/components/datasets/DatasetTags.tsx @@ -129,6 +129,7 @@ const DatasetTags: React.FC = (props) => { datasetField ? addDatasetFieldTag(namespace, datasetName, listTag, datasetField) : addDatasetTag(namespace, datasetName, listTag) + setDialogOpen(false) } const handleDelete = (deletedTag: string) => { diff --git a/web/src/store/actionCreators/index.ts b/web/src/store/actionCreators/index.ts index e76a62fd4d..c8c7fb8728 100644 --- a/web/src/store/actionCreators/index.ts +++ b/web/src/store/actionCreators/index.ts @@ -118,12 +118,12 @@ export const deleteDatasetTag = (namespace: string, datasetName: string, tag: st }, }) -export const deleteDatasetTagSuccess = (namespace:string, datasetName: string, tag:string) => ({ +export const deleteDatasetTagSuccess = (namespace: string, datasetName: string, tag: string) => ({ type: actionTypes.DELETE_DATASET_TAG_SUCCESS, payload: { datasetName, namespace, - tag + tag, }, }) @@ -142,13 +142,18 @@ export const deleteDatasetFieldTag = ( }, }) -export const deleteDatasetFieldTagSuccess = (namespace:string, datasetName: string, field:string, tag:string) => ({ +export const deleteDatasetFieldTagSuccess = ( + namespace: string, + datasetName: string, + field: string, + tag: string +) => ({ type: actionTypes.DELETE_DATASET_FIELD_TAG_SUCCESS, payload: { datasetName, namespace, tag, - field + field, }, }) @@ -166,7 +171,7 @@ export const addDatasetTagSuccess = (namespace: string, datasetName: string, tag payload: { datasetName, namespace, - tag + tag, }, }) @@ -185,13 +190,18 @@ export const addDatasetFieldTag = ( }, }) -export const addDatasetFieldTagSuccess = (namespace: string, datasetName: string, field:string, tag: string) => ({ +export const addDatasetFieldTagSuccess = ( + namespace: string, + datasetName: string, + field: string, + tag: string +) => ({ type: actionTypes.ADD_DATASET_FIELD_TAG_SUCCESS, payload: { datasetName, namespace, field, - tag + tag, }, }) diff --git a/web/src/store/reducers/datasets.ts b/web/src/store/reducers/datasets.ts index a072439251..a9868afb0b 100644 --- a/web/src/store/reducers/datasets.ts +++ b/web/src/store/reducers/datasets.ts @@ -74,19 +74,28 @@ export default (state: IDatasetsState = initialState, action: IDatasetsAction): case DELETE_DATASET_TAG: return { ...state } case DELETE_DATASET_TAG_SUCCESS: - return { ...state, refreshTags: `${payload.namespace}#${payload.datasetName}#${payload.tag}#d` } + return { + ...state, + refreshTags: `${payload.namespace}#${payload.datasetName}#${payload.tag}#d`, + } case DELETE_DATASET_FIELD_TAG: - return { ...state} + return { ...state } case DELETE_DATASET_FIELD_TAG_SUCCESS: - return { ...state, refreshTags: `${payload.namespace}#${payload.datasetName}#${payload.field}#${payload.tag}#d`} + return { + ...state, + refreshTags: `${payload.namespace}#${payload.datasetName}#${payload.field}#${payload.tag}#d`, + } case ADD_DATASET_TAG: return { ...state } case ADD_DATASET_TAG_SUCCESS: - return { ...state, refreshTags: `${payload.namespace}#${payload.datasetName}#${payload.tag}`} + return { ...state, refreshTags: `${payload.namespace}#${payload.datasetName}#${payload.tag}` } case ADD_DATASET_FIELD_TAG: return { ...state } case ADD_DATASET_FIELD_TAG_SUCCESS: - return { ...state, refreshTags: `${payload.namespace}#${payload.datasetName}#${payload.field}#${payload.tag}`} + return { + ...state, + refreshTags: `${payload.namespace}#${payload.datasetName}#${payload.field}#${payload.tag}`, + } default: return state } diff --git a/web/src/store/sagas/index.ts b/web/src/store/sagas/index.ts index 638feb9076..b233da757b 100644 --- a/web/src/store/sagas/index.ts +++ b/web/src/store/sagas/index.ts @@ -253,12 +253,7 @@ export function* deleteDatasetTagSaga() { while (true) { try { const { payload } = yield take(DELETE_DATASET_TAG) - yield call( - deleteDatasetTag, - payload.namespace, - payload.datasetName, - payload.tag - ) + yield call(deleteDatasetTag, payload.namespace, payload.datasetName, payload.tag) yield put(deleteDatasetTagSuccess(payload.namespace, payload.datasetName, payload.tag)) } catch (e) { yield put(applicationError('Something went wrong while removing tag from dataset')) @@ -270,14 +265,21 @@ export function* deleteDatasetFieldTagSaga() { while (true) { try { const { payload } = yield take(DELETE_DATASET_FIELD_TAG) - yield call( + yield call( deleteDatasetFieldTag, payload.namespace, payload.datasetName, payload.tag, payload.field ) - yield put(deleteDatasetFieldTagSuccess(payload.namespace, payload.datasetName, payload.field, payload.tag)) + yield put( + deleteDatasetFieldTagSuccess( + payload.namespace, + payload.datasetName, + payload.field, + payload.tag + ) + ) } catch (e) { yield put(applicationError('Something went wrong while removing tag from dataset field')) } @@ -288,12 +290,7 @@ export function* addDatasetTagSaga() { while (true) { try { const { payload } = yield take(ADD_DATASET_TAG) - yield call( - addDatasetTag, - payload.namespace, - payload.datasetName, - payload.tag - ) + yield call(addDatasetTag, payload.namespace, payload.datasetName, payload.tag) yield put(addDatasetTagSuccess(payload.namespace, payload.datasetName, payload.tag)) } catch (e) { yield put(applicationError('Something went wrong while adding tag to dataset')) @@ -312,7 +309,14 @@ export function* addDatasetFieldTagSaga() { payload.tag, payload.field ) - yield put(addDatasetFieldTagSuccess(payload.namespace, payload.datasetName , payload.field, payload.tag)) + yield put( + addDatasetFieldTagSuccess( + payload.namespace, + payload.datasetName, + payload.field, + payload.tag + ) + ) } catch (e) { yield put(applicationError('Something went wrong while adding tag to dataset field.')) } From 9aef43fc9194c75a781254b6856b9daf9cbf1c9b Mon Sep 17 00:00:00 2001 From: sharpd Date: Mon, 11 Mar 2024 17:03:26 +1300 Subject: [PATCH 05/15] add tag sort order and update conflict insert sql Signed-off-by: sharpd --- api/src/main/java/marquez/db/DatasetFieldDao.java | 2 +- web/src/components/datasets/DatasetTags.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/marquez/db/DatasetFieldDao.java b/api/src/main/java/marquez/db/DatasetFieldDao.java index 7b66b55adf..86abf8de60 100644 --- a/api/src/main/java/marquez/db/DatasetFieldDao.java +++ b/api/src/main/java/marquez/db/DatasetFieldDao.java @@ -129,7 +129,7 @@ void deleteDatasetVersionFieldTag( @SqlUpdate( "INSERT INTO dataset_fields_tag_mapping (dataset_field_uuid, tag_uuid, tagged_at) " - + "VALUES (:rowUuid, :tagUuid, :taggedAt)") + + "VALUES (:rowUuid, :tagUuid, :taggedAt) ON CONFLICT DO NOTHING") void updateTags(UUID rowUuid, UUID tagUuid, Instant taggedAt); @SqlBatch( diff --git a/web/src/components/datasets/DatasetTags.tsx b/web/src/components/datasets/DatasetTags.tsx index 8b5859b984..ac173e2dd6 100644 --- a/web/src/components/datasets/DatasetTags.tsx +++ b/web/src/components/datasets/DatasetTags.tsx @@ -119,7 +119,7 @@ const DatasetTags: React.FC = (props) => { setTagDescription(event.target.value) } - const tagData = useSelector((state: IState) => state.tags.tags) + const tagData = useSelector((state: IState) => state.tags.tags.sort((a,b) => a.name.localeCompare(b.name))) const handleTagListChange = (event: any) => { setListTag(event.target.value) From 82036871f21f18c7fc5153e74f6df0c5b5d2ea0e Mon Sep 17 00:00:00 2001 From: sharpd Date: Fri, 15 Mar 2024 14:17:40 +1300 Subject: [PATCH 06/15] add tag integration ON CONFLICT test Signed-off-by: sharpd --- .../api/TagResourceIntegrationTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/api/src/test/java/marquez/api/TagResourceIntegrationTest.java b/api/src/test/java/marquez/api/TagResourceIntegrationTest.java index 4304b55d33..60b905c33a 100644 --- a/api/src/test/java/marquez/api/TagResourceIntegrationTest.java +++ b/api/src/test/java/marquez/api/TagResourceIntegrationTest.java @@ -100,4 +100,38 @@ public void testApp_testDatasetTagFieldDelete() { // assert that only SENSITIVE remains assertThat(taggedDatasetFieldDelete.getFields().get(0).getTags()).containsExactly("SENSITIVE"); } + + @Test + public void testApp_testDatasetTagFieldConflict() { + // Create Namespace + createNamespace(NAMESPACE_NAME); + // create a source + createSource(DB_TABLE_SOURCE_NAME); + // Create Dataset + MARQUEZ_CLIENT.createDataset(NAMESPACE_NAME, DB_TABLE_NAME, DB_TABLE_META); + + // tag dataset field + Dataset taggedDatasetField = + MARQUEZ_CLIENT.tagFieldWith( + NAMESPACE_NAME, + DB_TABLE_NAME, + DB_TABLE_META.getFields().get(0).getName(), + "TESTFIELDTAG"); + // assert the tag TESTFIELDTAG has been added to field at position 0 + assertThat(taggedDatasetField.getFields().get(0).getTags()).contains("TESTFIELDTAG"); + // assert a total of two tags exist on the field + assertThat(taggedDatasetField.getFields().get(0).getTags()).hasSize(2); + + // tag dataset field again to test ON CONFLICT DO NOTHING + Dataset taggedDatasetField2 = + MARQUEZ_CLIENT.tagFieldWith( + NAMESPACE_NAME, + DB_TABLE_NAME, + DB_TABLE_META.getFields().get(0).getName(), + "TESTFIELDTAG"); + // assert the tag TESTFIELDTAG has been added to field at position 0 + assertThat(taggedDatasetField2.getFields().get(0).getTags()).contains("TESTFIELDTAG"); + // assert a total of two tags exist on the field + assertThat(taggedDatasetField2.getFields().get(0).getTags()).hasSize(2); + } } From abd7b31650543fd4e538f4638211a44f325ed273 Mon Sep 17 00:00:00 2001 From: sharpd Date: Tue, 2 Apr 2024 19:37:29 +1300 Subject: [PATCH 07/15] draft changes to tagging Signed-off-by: sharpd --- web/src/components/datasets/DatasetInfo.tsx | 71 ++---- web/src/components/datasets/DatasetTags.tsx | 242 +++++++------------- web/src/store/reducers/datasets.ts | 2 - 3 files changed, 95 insertions(+), 220 deletions(-) diff --git a/web/src/components/datasets/DatasetInfo.tsx b/web/src/components/datasets/DatasetInfo.tsx index a218aa96ac..f32f1b676c 100644 --- a/web/src/components/datasets/DatasetInfo.tsx +++ b/web/src/components/datasets/DatasetInfo.tsx @@ -4,16 +4,13 @@ import * as Redux from 'redux' import { Box, Table, TableBody, TableCell, TableHead, TableRow } from '@mui/material' import { Field, Run } from '../../types/api' import { IState } from '../../store/reducers' - import { connect, useSelector } from 'react-redux' import { fetchJobFacets, resetFacets } from '../../store/actionCreators' -import Collapse from '@mui/material/Collapse' import DatasetTags from './DatasetTags' -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' import MqEmpty from '../core/empty/MqEmpty' import MqJsonView from '../core/json-view/MqJsonView' import MqText from '../core/text/MqText' -import React, { FunctionComponent, useEffect, useState } from 'react' +import React, { FunctionComponent, useEffect } from 'react' export interface DispatchProps { fetchJobFacets: typeof fetchJobFacets @@ -47,11 +44,6 @@ const DatasetInfo: FunctionComponent = (props) => { ) const dsName = useSelector((state: IState) => state.datasetVersions.result.versions[0].name) - const loadCollapsedState = () => { - const storedState = localStorage.getItem(`dsi_${dsNamespace}_${dsName}`) - return storedState ? JSON.parse(storedState) : [] - } - useEffect(() => { run && fetchJobFacets(run.id) }, [run]) @@ -62,27 +54,6 @@ const DatasetInfo: FunctionComponent = (props) => { }, [] ) - const [expandedRows, setExpandedRows] = useState(loadCollapsedState) - - const toggleRow = (index: number) => { - setExpandedRows((prevExpandedRows) => { - const newExpandedRows = prevExpandedRows.includes(index) - ? prevExpandedRows.filter((rowIndex) => rowIndex !== index) - : [...prevExpandedRows, index] - - localStorage.setItem(`dsi_${dsNamespace}_${dsName}`, JSON.stringify(newExpandedRows)) - - return newExpandedRows - }) - } - - useEffect(() => { - for (const key in localStorage) { - if (key !== `dsi_${dsNamespace}_${dsName}`) { - localStorage.removeItem(key) - } - } - }, [dsNamespace, dsName]) return ( @@ -112,44 +83,30 @@ const DatasetInfo: FunctionComponent = (props) => { {i18next.t('dataset_info_columns.description')} - + + + {i18next.t('dataset_tags.tags')} + + - {datasetFields.map((field, index) => { + {datasetFields.map((field) => { return ( - toggleRow(index)} - className='expandable-row' - > + {field.name} {field.type} {field.description || 'no description'} - - + - - - - - - - - - ) })} diff --git a/web/src/components/datasets/DatasetTags.tsx b/web/src/components/datasets/DatasetTags.tsx index ac173e2dd6..2457c17f03 100644 --- a/web/src/components/datasets/DatasetTags.tsx +++ b/web/src/components/datasets/DatasetTags.tsx @@ -1,7 +1,12 @@ // 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 { + Autocomplete, + AutocompleteChangeDetails, + AutocompleteChangeReason, + TextField, +} from '@mui/material' import { Box, createTheme } from '@mui/material' import { IState } from '../../store/reducers' import { Tag } from '../../types/api' @@ -15,27 +20,16 @@ import { 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 Grow from '@mui/material/Grow' +import LocalOfferIcon from '@mui/icons-material/LocalOffer' import MQText from '../core/text/MqText' import MQTooltip from '../core/tooltip/MQTooltip' -import MenuItem from '@mui/material/MenuItem' -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 React, { useState } from 'react' import Snackbar from '@mui/material/Snackbar' interface DatasetTagsProps { @@ -68,47 +62,23 @@ const DatasetTags: React.FC = (props) => { addTags, } = props - const [isDialogOpen, setDialogOpen] = useState(false) const [listTag, setListTag] = useState('') - 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 [selectedTags, setSelectedTags] = useState(datasetTags) + const handleButtonClick = () => { - options[selectedIndex] === 'Add a Tag' ? setDialogOpen(true) : setOpenTagDesc(true) + setOpenTagDesc(true) } const [snackbarOpen, setSnackbarOpen] = useState(false) const theme = createTheme(useTheme()) - 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) @@ -119,20 +89,31 @@ const DatasetTags: React.FC = (props) => { setTagDescription(event.target.value) } - const tagData = useSelector((state: IState) => state.tags.tags.sort((a,b) => a.name.localeCompare(b.name))) - - const handleTagListChange = (event: any) => { - setListTag(event.target.value) - } + const tagData = useSelector((state: IState) => + state.tags.tags.sort((a, b) => a.name.localeCompare(b.name)) + ) - const handleTagChange = () => { - datasetField - ? addDatasetFieldTag(namespace, datasetName, listTag, datasetField) - : addDatasetTag(namespace, datasetName, listTag) - setDialogOpen(false) + const handleTagChange = ( + _event: React.SyntheticEvent, + _value: string[], + reason: AutocompleteChangeReason, + details?: AutocompleteChangeDetails | undefined + ) => { + if (reason === 'selectOption' && details) { + datasetField + ? addDatasetFieldTag(namespace, datasetName, details.option, datasetField) + : addDatasetTag(namespace, datasetName, details.option) + } } const handleDelete = (deletedTag: string) => { + const index = selectedTags.indexOf(deletedTag) + if (index !== -1) { + const newSelectedTags = [...selectedTags] + newSelectedTags.splice(index, 1) + setSelectedTags(newSelectedTags) + } + datasetField ? deleteDatasetFieldTag(namespace, datasetName, deletedTag, datasetField) : deleteDatasetTag(namespace, datasetName, deletedTag) @@ -154,6 +135,7 @@ const DatasetTags: React.FC = (props) => { handleDelete(tag)} @@ -167,6 +149,8 @@ const DatasetTags: React.FC = (props) => { }) } + const renderTags = (value: string[]) => formatTags(value, tagData) + return ( <> = (props) => { message={'Tag updated.'} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }} /> - - {i18next.t('dataset_tags.tags')} - {formatTags(datasetTags, tagData)} - - + + {!datasetField && ( + - - - - - {({ TransitionProps, placement }) => ( - - - - - {options.map((option, index) => ( - handleMenuItemClick(event, index)} - > - {option} - - ))} - - - - )} - - { - if (event.key === 'Escape') { - closeDialog() - } - }} - > - {i18next.t('dataset_tags.dialogtitle')} - - - - - - - - - - + InputLabelProps={{ + shrink: true, + }} + size='small' + /> + )} + /> + = (props) => { Tag option.name)} - freeSolo autoSelect onChange={handleTagDescChange} renderInput={(params) => ( diff --git a/web/src/store/reducers/datasets.ts b/web/src/store/reducers/datasets.ts index a9868afb0b..137bdb996f 100644 --- a/web/src/store/reducers/datasets.ts +++ b/web/src/store/reducers/datasets.ts @@ -76,14 +76,12 @@ export default (state: IDatasetsState = initialState, action: IDatasetsAction): case DELETE_DATASET_TAG_SUCCESS: return { ...state, - refreshTags: `${payload.namespace}#${payload.datasetName}#${payload.tag}#d`, } case DELETE_DATASET_FIELD_TAG: return { ...state } case DELETE_DATASET_FIELD_TAG_SUCCESS: return { ...state, - refreshTags: `${payload.namespace}#${payload.datasetName}#${payload.field}#${payload.tag}#d`, } case ADD_DATASET_TAG: return { ...state } From 8f7b10adb37a07af2ed5775b7c4558a7e60d63c1 Mon Sep 17 00:00:00 2001 From: sharpd Date: Tue, 2 Apr 2024 19:49:45 +1300 Subject: [PATCH 08/15] add missing freesolo Signed-off-by: sharpd --- web/src/components/datasets/DatasetTags.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/components/datasets/DatasetTags.tsx b/web/src/components/datasets/DatasetTags.tsx index 2457c17f03..5b54f77f42 100644 --- a/web/src/components/datasets/DatasetTags.tsx +++ b/web/src/components/datasets/DatasetTags.tsx @@ -231,6 +231,7 @@ const DatasetTags: React.FC = (props) => { option.name)} autoSelect + freeSolo onChange={handleTagDescChange} renderInput={(params) => ( Date: Wed, 3 Apr 2024 15:00:53 +1300 Subject: [PATCH 09/15] add field tag toggle Signed-off-by: sharpd --- .../components/datasets/DatasetDetailPage.tsx | 16 ++++- web/src/components/datasets/DatasetInfo.tsx | 63 +++++++++++-------- web/src/components/datasets/DatasetTags.tsx | 8 +-- web/src/i18n/config.ts | 1 + 4 files changed, 55 insertions(+), 33 deletions(-) diff --git a/web/src/components/datasets/DatasetDetailPage.tsx b/web/src/components/datasets/DatasetDetailPage.tsx index ac07f0778f..dc538c790f 100644 --- a/web/src/components/datasets/DatasetDetailPage.tsx +++ b/web/src/components/datasets/DatasetDetailPage.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import * as Redux from 'redux' -import { Box, Button, Tab, Tabs, createTheme } from '@mui/material' +import { Box, Button, Switch, Tab, Tabs, createTheme } from '@mui/material' import { CircularProgress } from '@mui/material' import { DatasetVersion } from '../../types/api' import { IState } from '../../store/reducers' @@ -31,7 +31,7 @@ import IconButton from '@mui/material/IconButton' import Io from '../io/Io' import MqStatus from '../core/status/MqStatus' import MqText from '../core/text/MqText' -import React, { ChangeEvent, FunctionComponent, useEffect } from 'react' +import React, { ChangeEvent, FunctionComponent, useEffect, useState } from 'react' interface StateProps { lineageDataset: LineageDataset @@ -79,6 +79,7 @@ const DatasetDetailPage: FunctionComponent = (props) => { const i18next = require('i18next') const theme = createTheme(useTheme()) const [_, setSearchParams] = useSearchParams() + const [showTags, setShowTags] = useState(false) // unmounting useEffect( @@ -194,7 +195,7 @@ const DatasetDetailPage: FunctionComponent = (props) => { - + {facetsStatus && ( @@ -203,6 +204,14 @@ const DatasetDetailPage: FunctionComponent = (props) => { {name} + + {i18next.t('datasets.show_field_tags')} + setShowTags(!showTags)} + inputProps={{ 'aria-label': 'toggle show tags' }} + /> + {description} @@ -213,6 +222,7 @@ const DatasetDetailPage: FunctionComponent = (props) => { datasetFields={firstVersion.fields} facets={firstVersion.facets} run={firstVersion.createdByRun} + showTags={showTags} /> )} {tabIndex === 1 && } diff --git a/web/src/components/datasets/DatasetInfo.tsx b/web/src/components/datasets/DatasetInfo.tsx index f32f1b676c..b938066cc1 100644 --- a/web/src/components/datasets/DatasetInfo.tsx +++ b/web/src/components/datasets/DatasetInfo.tsx @@ -33,11 +33,12 @@ type DatasetInfoProps = { datasetFields: Field[] facets?: object run?: Run + showTags?: boolean } & JobFacetsProps & DispatchProps const DatasetInfo: FunctionComponent = (props) => { - const { datasetFields, facets, run, fetchJobFacets, resetFacets } = props + const { datasetFields, facets, run, fetchJobFacets, resetFacets, showTags } = props const i18next = require('i18next') const dsNamespace = useSelector( (state: IState) => state.datasetVersions.result.versions[0].namespace @@ -73,21 +74,27 @@ const DatasetInfo: FunctionComponent = (props) => { {i18next.t('dataset_info_columns.name')} - - - {i18next.t('dataset_info_columns.type')} - - - - - {i18next.t('dataset_info_columns.description')} - - - - - {i18next.t('dataset_tags.tags')} - - + {!showTags && ( + + + {i18next.t('dataset_info_columns.type')} + + + )} + {!showTags && ( + + + {i18next.t('dataset_info_columns.description')} + + + )} + {showTags && ( + + + {i18next.t('dataset_tags.tags')} + + + )} @@ -96,16 +103,20 @@ const DatasetInfo: FunctionComponent = (props) => { {field.name} - {field.type} - {field.description || 'no description'} - - - + {!showTags && {field.type}} + {!showTags && ( + {field.description || 'no description'} + )} + {showTags && ( + + + + )} ) diff --git a/web/src/components/datasets/DatasetTags.tsx b/web/src/components/datasets/DatasetTags.tsx index 5b54f77f42..9c940584ea 100644 --- a/web/src/components/datasets/DatasetTags.tsx +++ b/web/src/components/datasets/DatasetTags.tsx @@ -128,7 +128,7 @@ const DatasetTags: React.FC = (props) => { } const formatTags = (tags: string[], tag_desc: Tag[]) => { - return tags.map((tag) => { + return tags.map((tag, index) => { const tagDescription = tag_desc.find((tagItem) => tagItem.name === tag) const tooltipTitle = tagDescription?.description || 'No Tag Description' return ( @@ -141,7 +141,7 @@ const DatasetTags: React.FC = (props) => { onDelete={() => handleDelete(tag)} style={{ display: 'row', - marginLeft: theme.spacing(1), + marginLeft: index === 0 ? theme.spacing(0) : theme.spacing(1), }} /> @@ -179,8 +179,8 @@ const DatasetTags: React.FC = (props) => { option.name)} diff --git a/web/src/i18n/config.ts b/web/src/i18n/config.ts index ee6d29b4cf..60b66ada70 100644 --- a/web/src/i18n/config.ts +++ b/web/src/i18n/config.ts @@ -93,6 +93,7 @@ i18next column_lineage_tab: 'COLUMN LINEAGE', dialog_delete: 'DELETE', dialog_confirmation_title: 'Are you sure?', + show_field_tags: 'Show Field Tags', }, datasets_route: { empty_title: 'No datasets found', From 2b3798450338c024b754f9115ca7d2bb3a3e2710 Mon Sep 17 00:00:00 2001 From: sharpd Date: Fri, 5 Apr 2024 12:04:35 +1300 Subject: [PATCH 10/15] add multi select/fix fresh Signed-off-by: sharpd --- .../components/datasets/DatasetDetailPage.tsx | 8 +++++- web/src/components/datasets/DatasetTags.tsx | 28 ++++++++++++++----- web/src/store/reducers/datasets.ts | 8 +++--- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/web/src/components/datasets/DatasetDetailPage.tsx b/web/src/components/datasets/DatasetDetailPage.tsx index 8decedc166..58d63ecd3d 100644 --- a/web/src/components/datasets/DatasetDetailPage.tsx +++ b/web/src/components/datasets/DatasetDetailPage.tsx @@ -92,7 +92,13 @@ const DatasetDetailPage: FunctionComponent = (props) => { useEffect(() => { fetchDatasetVersions(lineageDataset.namespace, lineageDataset.name) - }, [lineageDataset.name, datasets.refreshTags]) + }, [lineageDataset.name]) + + useEffect(() => { + if (showTags === true) { + fetchDatasetVersions(lineageDataset.namespace, lineageDataset.name) + } + }, [showTags]) // if the dataset is deleted then redirect to datasets end point useEffect(() => { diff --git a/web/src/components/datasets/DatasetTags.tsx b/web/src/components/datasets/DatasetTags.tsx index 9c940584ea..c8d5e11015 100644 --- a/web/src/components/datasets/DatasetTags.tsx +++ b/web/src/components/datasets/DatasetTags.tsx @@ -5,6 +5,7 @@ import { Autocomplete, AutocompleteChangeDetails, AutocompleteChangeReason, + Checkbox, TextField, } from '@mui/material' import { Box, createTheme } from '@mui/material' @@ -21,6 +22,8 @@ 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' @@ -70,6 +73,7 @@ const DatasetTags: React.FC = (props) => { const handleButtonClick = () => { setOpenTagDesc(true) } + const [snackbarOpen, setSnackbarOpen] = useState(false) const theme = createTheme(useTheme()) @@ -99,15 +103,20 @@ const DatasetTags: React.FC = (props) => { reason: AutocompleteChangeReason, details?: AutocompleteChangeDetails | undefined ) => { - if (reason === 'selectOption' && details) { + if (details && reason === 'selectOption') { + const newTag = details.option + const newSelectedTags = [...selectedTags, newTag] + setSelectedTags(newSelectedTags) + datasetField - ? addDatasetFieldTag(namespace, datasetName, details.option, datasetField) - : addDatasetTag(namespace, datasetName, details.option) + ? addDatasetFieldTag(namespace, datasetName, newTag, datasetField) + : addDatasetTag(namespace, datasetName, newTag) } } const handleDelete = (deletedTag: string) => { const index = selectedTags.indexOf(deletedTag) + if (index !== -1) { const newSelectedTags = [...selectedTags] newSelectedTags.splice(index, 1) @@ -149,8 +158,6 @@ const DatasetTags: React.FC = (props) => { }) } - const renderTags = (value: string[]) => formatTags(value, tagData) - return ( <> = (props) => { )} = (props) => { options={tagData.map((option) => option.name)} value={selectedTags} onChange={handleTagChange} - renderTags={renderTags} - renderOption={(props, option) => ( + renderTags={(value: string[]) => formatTags(value, tagData)} + renderOption={(props, option, { selected }) => (
  • + } + checkedIcon={} + style={{ marginRight: 4 }} + checked={selected} + />
    {option} diff --git a/web/src/store/reducers/datasets.ts b/web/src/store/reducers/datasets.ts index 137bdb996f..ed75282131 100644 --- a/web/src/store/reducers/datasets.ts +++ b/web/src/store/reducers/datasets.ts @@ -32,7 +32,7 @@ export type IDatasetsState = { totalCount: number init: boolean deletedDatasetName: string - refreshTags: string + refreshTags: boolean } export const initialState: IDatasetsState = { @@ -41,7 +41,7 @@ export const initialState: IDatasetsState = { result: [], totalCount: 0, deletedDatasetName: '', - refreshTags: '', + refreshTags: false, } export type IDatasetsAction = ReturnType & @@ -86,13 +86,13 @@ export default (state: IDatasetsState = initialState, action: IDatasetsAction): case ADD_DATASET_TAG: return { ...state } case ADD_DATASET_TAG_SUCCESS: - return { ...state, refreshTags: `${payload.namespace}#${payload.datasetName}#${payload.tag}` } + return { ...state, refreshTags: true } case ADD_DATASET_FIELD_TAG: return { ...state } case ADD_DATASET_FIELD_TAG_SUCCESS: return { ...state, - refreshTags: `${payload.namespace}#${payload.datasetName}#${payload.field}#${payload.tag}`, + refreshTags: true, } default: return state From 938b50784f4fa5f87cb00e464ce9cea4ed44045e Mon Sep 17 00:00:00 2001 From: sharpd Date: Fri, 5 Apr 2024 14:28:23 +1300 Subject: [PATCH 11/15] fix failing test Signed-off-by: sharpd --- web/src/__tests__/reducers/datasets.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/__tests__/reducers/datasets.test.ts b/web/src/__tests__/reducers/datasets.test.ts index 483cccc45b..3724410ef8 100644 --- a/web/src/__tests__/reducers/datasets.test.ts +++ b/web/src/__tests__/reducers/datasets.test.ts @@ -21,7 +21,7 @@ describe('datasets reducer', () => { result: datasets, totalCount: 16, deletedDatasetName: '', - refreshTags: '' + refreshTags: false }) }) From aa7327df66caa06c20d50fb1b73a5e515a6a189f Mon Sep 17 00:00:00 2001 From: sharpd Date: Fri, 5 Apr 2024 16:29:11 +1300 Subject: [PATCH 12/15] fix DOM underline error Signed-off-by: sharpd --- web/src/components/datasets/DatasetTags.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/datasets/DatasetTags.tsx b/web/src/components/datasets/DatasetTags.tsx index c8d5e11015..931489e93e 100644 --- a/web/src/components/datasets/DatasetTags.tsx +++ b/web/src/components/datasets/DatasetTags.tsx @@ -219,7 +219,7 @@ const DatasetTags: React.FC = (props) => { placeholder={selectedTags.length > 0 ? '' : 'Add some Tags.'} InputProps={{ ...params.InputProps, - disableUnderline: !!datasetField, + ...(datasetField ? { disableUnderline: true } : {}), }} InputLabelProps={{ shrink: true, From 80e1ac5c4c45c08054be10f7aa9835bb2874e213 Mon Sep 17 00:00:00 2001 From: sharpd Date: Sun, 14 Apr 2024 06:57:04 +1200 Subject: [PATCH 13/15] remove selectOption for tag update Signed-off-by: sharpd --- web/src/components/datasets/DatasetTags.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/components/datasets/DatasetTags.tsx b/web/src/components/datasets/DatasetTags.tsx index 931489e93e..28fa90fe88 100644 --- a/web/src/components/datasets/DatasetTags.tsx +++ b/web/src/components/datasets/DatasetTags.tsx @@ -100,10 +100,11 @@ const DatasetTags: React.FC = (props) => { const handleTagChange = ( _event: React.SyntheticEvent, _value: string[], - reason: AutocompleteChangeReason, + _reason: AutocompleteChangeReason, details?: AutocompleteChangeDetails | undefined ) => { - if (details && reason === 'selectOption') { + + if (details) { const newTag = details.option const newSelectedTags = [...selectedTags, newTag] setSelectedTags(newSelectedTags) From 5d5c449ea7183a40d3887fc62893dbcc22bd948d Mon Sep 17 00:00:00 2001 From: sharpd Date: Tue, 16 Apr 2024 08:58:59 +1200 Subject: [PATCH 14/15] add auto highlight prop to aid user selection Signed-off-by: sharpd --- web/src/components/datasets/DatasetTags.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/components/datasets/DatasetTags.tsx b/web/src/components/datasets/DatasetTags.tsx index 28fa90fe88..62bcfecbed 100644 --- a/web/src/components/datasets/DatasetTags.tsx +++ b/web/src/components/datasets/DatasetTags.tsx @@ -190,6 +190,7 @@ const DatasetTags: React.FC = (props) => { id='dataset-tags' sx={{ width: 516, flex: 1 }} limitTags={!datasetField ? 5 : 4} + autoHighlight disableClearable disablePortal options={tagData.map((option) => option.name)} From ef2de7b43d22d27bd3e50f04554585932b818ca0 Mon Sep 17 00:00:00 2001 From: sharpd Date: Wed, 17 Apr 2024 11:02:28 +1200 Subject: [PATCH 15/15] change tag deletion updated based on feedback Signed-off-by: sharpd --- web/src/components/datasets/DatasetTags.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/web/src/components/datasets/DatasetTags.tsx b/web/src/components/datasets/DatasetTags.tsx index 62bcfecbed..0863451136 100644 --- a/web/src/components/datasets/DatasetTags.tsx +++ b/web/src/components/datasets/DatasetTags.tsx @@ -116,13 +116,9 @@ const DatasetTags: React.FC = (props) => { } const handleDelete = (deletedTag: string) => { - const index = selectedTags.indexOf(deletedTag) - - if (index !== -1) { - const newSelectedTags = [...selectedTags] - newSelectedTags.splice(index, 1) - setSelectedTags(newSelectedTags) - } + const newSelectedTags = selectedTags.filter((tag) => deletedTag !== tag) + + setSelectedTags(newSelectedTags) datasetField ? deleteDatasetFieldTag(namespace, datasetName, deletedTag, datasetField)