diff --git a/aim/web/ui/src/components/AttachedTagsList/AttachedTagsList.scss b/aim/web/ui/src/components/AttachedTagsList/AttachedTagsList.scss index 8813f81b7c..af3fad63c0 100644 --- a/aim/web/ui/src/components/AttachedTagsList/AttachedTagsList.scss +++ b/aim/web/ui/src/components/AttachedTagsList/AttachedTagsList.scss @@ -6,6 +6,7 @@ align-items: baseline; font-size: $text-sm; line-height: 15px; + position: relative; margin-top: $space-xs; &__title { @@ -24,16 +25,20 @@ &__tags { display: flex; flex-wrap: wrap; - overflow: auto; + overflow: hidden; + &__tagWrapper { + max-width: 100%; + } .Badge { margin-bottom: $space-xs; margin-right: $space-xs; - max-width: unset; + max-width: 100%; } } &__ControlPopover__anchor { + max-width: 100%; .icon-edit { width: 1.5rem; height: 1.5rem; @@ -55,6 +60,12 @@ margin-right: $space-xs; } } + + &__ControlPopover__editPopoverButton { + position: absolute; + top: toRem(-26px); + right: 0; + } } .InlineAttachedTagsList { @@ -62,6 +73,11 @@ align-items: center; max-width: 100%; width: 100%; + .AttachedTagsList__ControlPopover__anchor { + max-width: 100%; + width: 100%; + cursor: text; + } .AttachedTagsList__tags { align-items: center; flex-wrap: unset; diff --git a/aim/web/ui/src/components/AttachedTagsList/AttachedTagsList.tsx b/aim/web/ui/src/components/AttachedTagsList/AttachedTagsList.tsx index 89864d999a..54f92d0dc5 100644 --- a/aim/web/ui/src/components/AttachedTagsList/AttachedTagsList.tsx +++ b/aim/web/ui/src/components/AttachedTagsList/AttachedTagsList.tsx @@ -24,13 +24,15 @@ function AttachedTagsList({ addTagButtonSize = 'xSmall', onTagsChange, onRunsTagsChange, - hasAttachedTagsPopup = false, + inlineAttachedTagsList = false, }: IAttachedTagsListProps) { const [attachedTags, setAttachedTags] = React.useState( tags ?? initialTags ?? [], ); + const [selectTagsPopoverKey, setSelectTagPopoverKey] = React.useState( + `${Date.now()}`, + ); const getRunInfoRef = React.useRef(null); - const deleteRunsTagRef = React.useRef(null); const getRunInfo = React.useCallback((runHash: string): void => { getRunInfoRef.current = runsService?.getRunInfo(runHash); @@ -39,29 +41,6 @@ function AttachedTagsList({ }); }, []); - const deleteRunsTag = React.useCallback( - (run_id: string, tag: ITagInfo): void => { - deleteRunsTagRef.current = runsService?.deleteRunsTag(run_id, tag.id); - deleteRunsTagRef.current.call(); - }, - [], - ); - - const onAttachedTagDelete = React.useCallback( - (label: string): void => { - const tag = attachedTags.find((tag) => tag.name === label); - if (tag) { - const resultTags: ITagInfo[] = attachedTags.filter( - (t) => tag.id !== t.id, - ); - setAttachedTags(resultTags); - deleteRunsTag(runHash, tag); - onRunsTagsChange && onRunsTagsChange(runHash, resultTags); - } - }, - [onRunsTagsChange, attachedTags, deleteRunsTag, runHash], - ); - React.useEffect(() => { if (runHash) { if (!initialTags && !tags) { @@ -87,82 +66,43 @@ function AttachedTagsList({ const renderTagsBadges = React.useCallback(() => { if (!_.isEmpty(attachedTags)) { - if (hasAttachedTagsPopup) { - return ( -
- ( -
- {attachedTags.map((tag: ITagInfo) => ( - - ))} -
- )} - component={ -
-
- {attachedTags.map((tag: ITagInfo) => { - return ( -
- -
- ); - })} -
-
- } - /> -
- ); - } return (
{attachedTags.map((tag: ITagInfo) => ( - + +
+ +
+
))}
); } return ( -
No attached tags
+
+ {inlineAttachedTagsList ? 'Click to edit tags' : 'No attached tags'} +
+ ); + }, [attachedTags]); + + const renderAddTagsButton = React.useCallback(() => { + return ( + ); - }, [attachedTags, onAttachedTagDelete, hasAttachedTagsPopup]); + }, [attachedTags, addTagButtonSize]); return ( @@ -176,12 +116,12 @@ function AttachedTagsList({ )} - {renderTagsBadges()} + {!inlineAttachedTagsList && renderTagsBadges()} ( - -
- {!_.isEmpty(attachedTags) ? ( - - ) : ( - - )} -
-
+ {inlineAttachedTagsList && renderTagsBadges()} + {!inlineAttachedTagsList && renderAddTagsButton()} + )} component={ } /> diff --git a/aim/web/ui/src/components/SelectTag/SelectTag.scss b/aim/web/ui/src/components/SelectTag/SelectTag.scss index 6bbdbc606c..541250194d 100644 --- a/aim/web/ui/src/components/SelectTag/SelectTag.scss +++ b/aim/web/ui/src/components/SelectTag/SelectTag.scss @@ -3,30 +3,83 @@ .SelectTag { max-width: 20rem; width: 20rem; - + max-height: 240px; + display: flex; + flex-direction: column; + &__searchBarContainer { + padding: $space-xs; + } &__tags { - padding: $space-xs $space-unit; overflow: auto; - display: flex; - flex-wrap: wrap; - max-height: 50vh; - .Badge { - margin: 0; - max-width: unset; - cursor: pointer; - } - + max-height: 200px; + flex: 1 1; .icon-check { padding: 0; } - &__badge { + &__item { display: flex; - margin-bottom: $space-xs; - margin-right: $space-xs; + width: 100%; + max-width: 100%; + border-bottom: $border-main; + padding: $space-xs toRem(25px) $space-xs toRem(30px); + position: relative; + cursor: pointer; + &:hover { + background-color: $cuddle-20; + } + &:last-child { + border-bottom: $border-transparent; + } + &__checkedIcon { + position: absolute; + left: toRem(10px); + top: $space-xs; + } + &__deleteButton { + position: absolute; + right: toRem(10px); + top: $space-xxs; + } + &__content { + display: flex; + max-width: 100%; + flex-direction: column; + width: 100%; + padding-right: $space-xxxs; + + &__nameWrapper { + display: flex; + align-items: flex-start; + &__name { + word-break: break-all; + } + &__colorBadge { + width: toRem(14px); + min-width: toRem(14px); + height: toRem(14px); + display: block; + border-radius: $border-radius-circle; + margin-right: $space-xxxs; + } + } + + &__description { + margin-top: $space-xxxxs; + word-break: break-all; + } + } } } + &__noTags { + padding: 1rem 0; + display: flex; + justify-content: center; + flex: 1; + align-items: center; + } + &__createTag__container { padding: $space-xs $space-unit; text-align: center; diff --git a/aim/web/ui/src/components/SelectTag/SelectTag.tsx b/aim/web/ui/src/components/SelectTag/SelectTag.tsx index 5be8f90170..b679d6e828 100644 --- a/aim/web/ui/src/components/SelectTag/SelectTag.tsx +++ b/aim/web/ui/src/components/SelectTag/SelectTag.tsx @@ -1,10 +1,12 @@ import React from 'react'; +import _ from 'lodash-es'; import { Link as RouteLink } from 'react-router-dom'; import { Divider, Link } from '@material-ui/core'; -import { Badge, Button } from 'components/kit'; +import { Text, Button, Icon } from 'components/kit'; import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary'; +import SearchInput from 'components/kit/DataList/SearchBar/SearchInput'; import { PathEnum } from 'config/enums/routesEnum'; @@ -12,7 +14,7 @@ import tagsService from 'services/api/tags/tagsService'; import runsService from 'services/api/runs/runsService'; import { ISelectTagProps } from 'types/components/SelectTag/SelectTag'; -import { ITagInfo } from 'types/pages/tags/Tags'; +import { ITagInfo, ITagInfoWithSelectedProperty } from 'types/pages/tags/Tags'; import './SelectTag.scss'; @@ -21,10 +23,60 @@ function SelectTag({ attachedTags, setAttachedTags, onRunsTagsChange, + updatePopover, }: ISelectTagProps): JSX.Element { const [tags, setTags] = React.useState([]); + const [searchValue, setSearchValue] = React.useState(''); + const [sortedTags, setSortedTags] = React.useState< + ITagInfoWithSelectedProperty[] + >([]); const getTagsRef = React.useRef(null); const attachTagToRunRef = React.useRef(null); + const deleteRunsTagRef = React.useRef(null); + + const addSelectedPropertyToTags = function ( + tags: ITagInfo[], + ): ITagInfoWithSelectedProperty[] { + return tags.map((tag: ITagInfo) => { + const selected = !!attachedTags.find( + (attachedTag) => attachedTag.id === tag.id, + ); + return { ...tag, selected }; + }); + }; + + const deleteRunsTag = React.useCallback( + (run_id: string, tag: ITagInfo): void => { + deleteRunsTagRef.current = runsService?.deleteRunsTag(run_id, tag.id); + deleteRunsTagRef.current.call(); + }, + [], + ); + + const onAttachedTagDelete = React.useCallback( + (e): void => { + const tag_id = e.currentTarget?.id; + const tag = attachedTags.find((tag) => tag.id === tag_id); + if (tag) { + const resultTags: ITagInfo[] = attachedTags.filter( + (t) => tag.id !== t.id, + ); + setAttachedTags(resultTags); + deleteRunsTag(runHash, tag); + onRunsTagsChange && onRunsTagsChange(runHash, resultTags); + setSortedTags((sT) => + sT.map((tag: ITagInfoWithSelectedProperty) => { + if (tag.id === tag_id) { + return { ...tag, selected: false }; + } + return tag; + }), + ); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [onRunsTagsChange, setAttachedTags, attachedTags, deleteRunsTag, runHash], + ); const attachTagToRun = React.useCallback((tag: ITagInfo, run_id: string) => { attachTagToRunRef.current = runsService?.attachRunsTag( @@ -43,9 +95,18 @@ function SelectTag({ setAttachedTags((prevState) => [...prevState, tag]); attachTagToRun(tag, runHash); onRunsTagsChange && onRunsTagsChange(runHash, [...attachedTags, tag]); + setSortedTags((sT) => + sT.map((tag: ITagInfoWithSelectedProperty) => { + if (tag.id === tag_id) { + return { ...tag, selected: true }; + } + return tag; + }), + ); } } }, + // eslint-disable-next-line react-hooks/exhaustive-deps [ attachedTags, attachTagToRun, @@ -59,45 +120,109 @@ function SelectTag({ React.useEffect(() => { if (runHash) { getTagsRef.current = tagsService?.getTags(); - getTagsRef.current.call().then((tags: any) => { - setTags(tags || []); + getTagsRef.current?.call().then((tags: ITagInfo[]) => { + setSortedTags( + tags + ? _.orderBy( + addSelectedPropertyToTags(tags), + ['selected', 'name'], + ['desc', 'asc'], + ) + : [], + ); + setTags(tags ?? []); + if (updatePopover) { + updatePopover(`${Date.now()}`); + } }); } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [runHash]); + + const filteredTagsList = React.useMemo(() => { + if (searchValue) { + return sortedTags.filter((tag) => + tag.name.toLowerCase().includes(searchValue.toLowerCase()), + ); + } + return sortedTags; + }, [searchValue, sortedTags]); + + React.useEffect(() => { return () => { getTagsRef.current?.abort(); + attachTagToRunRef.current?.abort(); + deleteRunsTagRef.current?.abort(); }; - }, [runHash]); + }, []); return (
- {tags?.length > 0 ? ( -
- {tags.map((tag: ITagInfo) => { - const tagAttached = attachedTags.find( - (attachedTag) => attachedTag.id === tag.id, - ); +
+ setSearchValue('')} + onInputChange={(value) => setSearchValue(value)} + isDisabled={_.isEmpty(sortedTags)} + isValidInput={true} + /> +
+ + {filteredTagsList?.length > 0 ? ( +
+ {filteredTagsList.map((tag: ITagInfoWithSelectedProperty) => { return (
+ tag.selected ? onAttachedTagDelete(e) : onAttachedTagAdd(e) + } > - + {tag.selected && ( + + )} +
+
+ + + {tag.name} + +
+ {tag.description ? ( + + {tag.description} + + ) : null} +
); })}
) : ( - <> +
+ + No Tags Found + +
)}
@@ -108,7 +233,7 @@ function SelectTag({ color='primary' className='SelectTag__createTag' > - Create New Tag + Edit Tags
diff --git a/aim/web/ui/src/components/kit/DataList/SearchBar/SearchBar.scss b/aim/web/ui/src/components/kit/DataList/SearchBar/SearchBar.scss index f3050bb4bf..9a63d11da9 100644 --- a/aim/web/ui/src/components/kit/DataList/SearchBar/SearchBar.scss +++ b/aim/web/ui/src/components/kit/DataList/SearchBar/SearchBar.scss @@ -6,19 +6,7 @@ margin-bottom: toRem(14px); align-items: center; max-width: 100%; - .SearchInput { - .EndAdornment { - .Icon__container { - font-size: $text-md; - &.icon-close { - font-size: $text-xs; - } - } - } - .MuiFormLabel-root { - top: $space-xxxxs; - } - } + .MatchIcons { display: flex; align-items: baseline; diff --git a/aim/web/ui/src/components/kit/DataList/SearchBar/SearchInput/EndAdornment.tsx b/aim/web/ui/src/components/kit/DataList/SearchBar/SearchInput/EndAdornment.tsx index d19778721a..1ecc9a7b20 100644 --- a/aim/web/ui/src/components/kit/DataList/SearchBar/SearchInput/EndAdornment.tsx +++ b/aim/web/ui/src/components/kit/DataList/SearchBar/SearchInput/EndAdornment.tsx @@ -15,13 +15,13 @@ function EndAdornment({ {showSearchIcon ? ( - + ) : ( <>
diff --git a/aim/web/ui/src/pages/Runs/components/RunsTableGrid/RunsTableGrid.tsx b/aim/web/ui/src/pages/Runs/components/RunsTableGrid/RunsTableGrid.tsx index 2b073ed456..1a8be90557 100644 --- a/aim/web/ui/src/pages/Runs/components/RunsTableGrid/RunsTableGrid.tsx +++ b/aim/web/ui/src/pages/Runs/components/RunsTableGrid/RunsTableGrid.tsx @@ -9,6 +9,7 @@ import COLORS from 'config/colors/colors'; import { TABLE_DEFAULT_CONFIG } from 'config/table/tableConfigs'; import { ITableColumn } from 'types/pages/metrics/components/TableColumns/TableColumns'; +import { ITagInfo } from 'types/pages/tags/Tags'; import { formatSystemMetricName } from 'utils/formatSystemMetricName'; import alphabeticalSortComparator from 'utils/alphabeticalSortComparator'; @@ -177,8 +178,19 @@ function getRunsTableColumns( return columns; } +const TagsColumn = (props: { + runHash: string; + tags: ITagInfo[]; + onRunsTagsChange: (runHash: string, tags: ITagInfo[]) => void; + headerRenderer: () => React.ReactNode; + addTagButtonSize: 'xxSmall' | 'xSmall'; +}) => { + return ; +}; + function runsTableRowRenderer( rowData: any, + onRunsTagsChange: (runHash: string, tags: ITagInfo[]) => void, groupHeaderRow = false, columns: string[] = [], ) { @@ -207,15 +219,14 @@ function runsTableRowRenderer( ), }, tags: { - content: ( - <>} - addTagButtonSize='xxSmall' - hasAttachedTagsPopup - /> - ), + component: TagsColumn, + props: { + runHash: rowData.hash, + tags: rowData.tags, + onRunsTagsChange, + headerRenderer: () => <>, + addTagButtonSize: 'xxSmall', + }, }, actions: { content: null, diff --git a/aim/web/ui/src/services/models/explorer/createAppModel.ts b/aim/web/ui/src/services/models/explorer/createAppModel.ts index a53bb822d5..77c6a3c755 100644 --- a/aim/web/ui/src/services/models/explorer/createAppModel.ts +++ b/aim/web/ui/src/services/models/explorer/createAppModel.ts @@ -2233,6 +2233,10 @@ function createAppModel(appConfig: IAppInitialConfig) { }); } + function onModelRunsTagsChange(runHash: string, tags: ITagInfo[]): void { + onRunsTagsChange({ runHash, tags, model, updateModelData }); + } + function getRunsData( shouldUrlUpdate?: boolean, shouldResetSelectedRows?: boolean, @@ -2774,11 +2778,15 @@ function createAppModel(appConfig: IAppInitialConfig) { }); if (metricsCollection.config !== null) { rows[groupKey!].items.push( - isRawData ? rowValues : runsTableRowRenderer(rowValues), + isRawData + ? rowValues + : runsTableRowRenderer(rowValues, onModelRunsTagsChange), ); } else { rows.push( - isRawData ? rowValues : runsTableRowRenderer(rowValues), + isRawData + ? rowValues + : runsTableRowRenderer(rowValues, onModelRunsTagsChange), ); } }); @@ -2799,6 +2807,7 @@ function createAppModel(appConfig: IAppInitialConfig) { if (metricsCollection.config !== null && !isRawData) { rows[groupKey!].data = runsTableRowRenderer( rows[groupKey!].data, + onModelRunsTagsChange, true, Object.keys(columnsValues), ); @@ -3138,6 +3147,7 @@ function createAppModel(appConfig: IAppInitialConfig) { onExportTableData, onNotificationDelete: onModelNotificationDelete, setDefaultAppConfigData: setModelDefaultAppConfigData, + onRunsTagsChange: onModelRunsTagsChange, changeLiveUpdateConfig, archiveRuns, deleteRuns, diff --git a/aim/web/ui/src/types/components/AttachedTagsList/AttachedTagsList.d.ts b/aim/web/ui/src/types/components/AttachedTagsList/AttachedTagsList.d.ts index 0665d93679..bf91c4960c 100644 --- a/aim/web/ui/src/types/components/AttachedTagsList/AttachedTagsList.d.ts +++ b/aim/web/ui/src/types/components/AttachedTagsList/AttachedTagsList.d.ts @@ -6,8 +6,8 @@ export interface IAttachedTagsListProps { runHash: string; initialTags?: ITagInfo[]; tags?: ITagInfo[]; - addTagButtonSize?: 'xSmall' | 'xxSmall'; - hasAttachedTagsPopup?: boolean; + addTagButtonSize?: 'small' | 'xSmall' | 'xxSmall'; + inlineAttachedTagsList?: boolean; headerRenderer?: (tagsLength: number) => React.ReactNode; onTagsChange?: (tags: ITagInfo[]) => void; onRunsTagsChange?: (runHash: string, tags: ITagInfo[]) => void; diff --git a/aim/web/ui/src/types/components/SelectTag/SelectTag.d.ts b/aim/web/ui/src/types/components/SelectTag/SelectTag.d.ts index a0cd334691..712999613e 100644 --- a/aim/web/ui/src/types/components/SelectTag/SelectTag.d.ts +++ b/aim/web/ui/src/types/components/SelectTag/SelectTag.d.ts @@ -7,4 +7,5 @@ export interface ISelectTagProps { attachedTags: ITagInfo[]; setAttachedTags: Dispatch>; onRunsTagsChange?: (runHash: string, tags: ITagInfo[]) => void; + updatePopover?: (key: string) => void; } diff --git a/aim/web/ui/src/types/pages/tags/Tags.d.ts b/aim/web/ui/src/types/pages/tags/Tags.d.ts index 4524893aad..07d12b38bf 100644 --- a/aim/web/ui/src/types/pages/tags/Tags.d.ts +++ b/aim/web/ui/src/types/pages/tags/Tags.d.ts @@ -44,10 +44,13 @@ export interface ITagInfo { archived: boolean; color: string; id: string; - comment: string; + description: string; name: string; run_count: number; } +export interface ITagInfoWithSelectedProperty extends ITagInfo { + selected: boolean; +} export interface ITagDetailProps { id: string; onSoftDeleteModalToggle: () => void;