From ec8a4e9f77bc8921b784e679b0c39dfcde4161a0 Mon Sep 17 00:00:00 2001 From: jvalvo Date: Thu, 25 May 2023 15:56:02 +0900 Subject: [PATCH 01/11] feat: Added datadoc tags functionality and search filtering --- querybook/config/elasticsearch.yaml | 2 + .../8f194c1b59a3_data_doc_tag_item.py | 38 ++++ querybook/server/datasources/tag.py | 41 +++++ .../lib/elasticsearch/search_datadoc.py | 4 +- querybook/server/logic/elasticsearch.py | 2 + querybook/server/logic/tag.py | 99 ++++++++++- querybook/server/models/datadoc.py | 1 + querybook/server/models/tag.py | 32 ++++ .../test_elasticsearch/test_elasticsearch.py | 7 + .../webapp/components/DataDoc/DataDoc.scss | 5 + .../webapp/components/DataDoc/DataDoc.tsx | 13 ++ .../DataDocTags/CreateDataDocTag.scss | 0 .../DataDocTags/CreateDataDocTag.tsx | 70 ++++++++ .../DataDocTags/DataDocTagConfigModal.tsx | 137 +++++++++++++++ .../DataDocTags/DataDocTagGroupSelect.tsx | 46 +++++ .../DataDocTags/DataDocTagSelect.scss | 13 ++ .../DataDocTags/DataDocTagSelect.tsx | 93 ++++++++++ .../components/DataDocTags/DataDocTags.scss | 8 + .../components/DataDocTags/DataDocTags.tsx | 162 ++++++++++++++++++ .../components/Search/SearchOverview.tsx | 20 +++ .../components/Search/SearchResultItem.tsx | 11 ++ querybook/webapp/const/search.ts | 2 + querybook/webapp/redux/tag/action.ts | 63 +++++++ querybook/webapp/redux/tag/reducer.ts | 30 ++++ querybook/webapp/redux/tag/selector.ts | 10 ++ querybook/webapp/redux/tag/types.ts | 39 ++++- querybook/webapp/resource/dataDoc.ts | 15 ++ 27 files changed, 959 insertions(+), 4 deletions(-) create mode 100644 querybook/migrations/versions/8f194c1b59a3_data_doc_tag_item.py create mode 100644 querybook/webapp/components/DataDocTags/CreateDataDocTag.scss create mode 100644 querybook/webapp/components/DataDocTags/CreateDataDocTag.tsx create mode 100644 querybook/webapp/components/DataDocTags/DataDocTagConfigModal.tsx create mode 100644 querybook/webapp/components/DataDocTags/DataDocTagGroupSelect.tsx create mode 100644 querybook/webapp/components/DataDocTags/DataDocTagSelect.scss create mode 100644 querybook/webapp/components/DataDocTags/DataDocTagSelect.tsx create mode 100644 querybook/webapp/components/DataDocTags/DataDocTags.scss create mode 100644 querybook/webapp/components/DataDocTags/DataDocTags.tsx diff --git a/querybook/config/elasticsearch.yaml b/querybook/config/elasticsearch.yaml index 5b8164867..3b006f33b 100644 --- a/querybook/config/elasticsearch.yaml +++ b/querybook/config/elasticsearch.yaml @@ -154,6 +154,8 @@ datadocs: type: boolean readable_user_ids: type: integer + tags: + type: keyword tables: index_name: search_tables_v1 type_name: tables # Keep this same in mappings diff --git a/querybook/migrations/versions/8f194c1b59a3_data_doc_tag_item.py b/querybook/migrations/versions/8f194c1b59a3_data_doc_tag_item.py new file mode 100644 index 000000000..1525290d4 --- /dev/null +++ b/querybook/migrations/versions/8f194c1b59a3_data_doc_tag_item.py @@ -0,0 +1,38 @@ +"""data_doc_tag_item + +Revision ID: 8f194c1b59a3 +Revises: 7f6cdb3621f7 +Create Date: 2023-06-01 20:40:07.871141 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '8f194c1b59a3' +down_revision = '7f6cdb3621f7' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('data_doc_tag_item', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('tag_name', sa.String(length=255), nullable=False), + sa.Column('datadoc_id', sa.Integer(), nullable=True), + sa.Column('uid', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['datadoc_id'], ['data_doc.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tag_name'], ['tag.name'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['user.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('data_doc_tag_item') + # ### end Alembic commands ### diff --git a/querybook/server/datasources/tag.py b/querybook/server/datasources/tag.py index 542e5366a..da6c29568 100644 --- a/querybook/server/datasources/tag.py +++ b/querybook/server/datasources/tag.py @@ -5,6 +5,7 @@ from app.auth.permission import ( verify_data_table_permission, verify_data_column_permission, + verify_data_doc_permission, ) from logic import tag as logic from models.tag import Tag @@ -19,6 +20,15 @@ def get_tags_by_table_id(table_id: int): return logic.get_tags_by_table_id(table_id=table_id) +@register( + "/datadoc//tag/", + methods=["GET"], +) +def get_tags_by_datadoc_id(datadoc_id: int): + verify_data_doc_permission(datadoc_id) + return logic.get_tags_by_datadoc_id(datadoc_id=datadoc_id) + + @register( "/column//tag/", methods=["GET"], @@ -63,6 +73,22 @@ def add_tag_to_table(table_id, tag): ) +@register( + "/datadoc//tag/", + methods=["POST"], +) +def add_tag_to_datadoc(datadoc_id, tag): + with DBSession() as session: + verify_data_doc_permission(datadoc_id, session=session) + return logic.add_tag_to_datadoc( + datadoc_id=datadoc_id, + tag_name=tag, + uid=current_user.id, + user_is_admin=current_user.is_admin, + session=session, + ) + + @register( "/table//tag/", methods=["DELETE"], @@ -76,3 +102,18 @@ def delete_tag_from_table(table_id: int, tag_name: str): user_is_admin=current_user.is_admin, session=session, ) + + +@register( + "/datadoc//tag/", + methods=["DELETE"], +) +def delete_tag_from_datadoc(datadoc_id: int, tag_name: str): + with DBSession() as session: + verify_data_doc_permission(datadoc_id, session=session) + return logic.delete_tag_from_datadoc( + datadoc_id=datadoc_id, + tag_name=tag_name, + user_is_admin=current_user.is_admin, + session=session, + ) diff --git a/querybook/server/lib/elasticsearch/search_datadoc.py b/querybook/server/lib/elasticsearch/search_datadoc.py index bb7475b76..41f0d0b95 100644 --- a/querybook/server/lib/elasticsearch/search_datadoc.py +++ b/querybook/server/lib/elasticsearch/search_datadoc.py @@ -44,7 +44,7 @@ def construct_datadoc_query( keywords, search_fields=_match_data_doc_fields(fields), ) - search_filter = match_filters(filters) + search_filter = match_filters(filters, and_filter_names=["tags"]) search_filter.setdefault("filter", {}).setdefault("bool", {}).setdefault( "must", [] ).append({"bool": {"should": _data_doc_access_terms(uid)}}) @@ -53,7 +53,7 @@ def construct_datadoc_query( "query": { "bool": combine_keyword_and_filter_query(keywords_query, search_filter) }, - "_source": ["id", "title", "owner_uid", "created_at"], + "_source": ["id", "title", "owner_uid", "created_at", "scheduled", "tags"], "size": limit, "from": offset, } diff --git a/querybook/server/logic/elasticsearch.py b/querybook/server/logic/elasticsearch.py index 98013d50e..323b82aed 100644 --- a/querybook/server/logic/elasticsearch.py +++ b/querybook/server/logic/elasticsearch.py @@ -438,6 +438,8 @@ def datadocs_to_es(datadoc, fields=None, session=None): "title": datadoc.title, "public": datadoc.public, "readable_user_ids": lambda: _get_datadoc_editors(datadoc, session=session), + "scheduled": datadoc.scheduled, + "tags": [tag.tag_name for tag in datadoc.tags], } return _get_dict_by_field(field_to_getter, fields=fields) diff --git a/querybook/server/logic/tag.py b/querybook/server/logic/tag.py index a9d96fc7f..9e0e085e2 100644 --- a/querybook/server/logic/tag.py +++ b/querybook/server/logic/tag.py @@ -4,7 +4,8 @@ from const.metastore import DataTag from lib.utils.color import find_nearest_palette_color from logic.metastore import update_es_tables_by_id -from models.tag import Tag, TagItem +from logic.datadoc import update_es_data_doc_by_id +from models.tag import Tag, TagItem, DataDocTagItem @with_session @@ -18,6 +19,17 @@ def get_tags_by_table_id(table_id, session=None): ) +@with_session +def get_tags_by_datadoc_id(datadoc_id, session=None): + return ( + session.query(Tag) + .join(DataDocTagItem) + .filter(DataDocTagItem.datadoc_id == datadoc_id) + .order_by(Tag.count.desc()) + .all() + ) + + @with_session def get_tags_by_column_id(column_id: int, session=None): return ( @@ -84,6 +96,27 @@ def add_tag_to_table(table_id, tag_name, uid, user_is_admin=False, session=None) return tag +@with_session +def add_tag_to_datadoc(datadoc_id, tag_name, uid, user_is_admin=False, session=None): + existing_tag_item = DataDocTagItem.get( + datadoc_id=datadoc_id, tag_name=tag_name, session=session + ) + + if existing_tag_item: + return + + tag = create_or_update_tag(tag_name=tag_name, commit=False, session=session) + if (tag.meta or {}).get("admin"): + assert user_is_admin, f"Tag {tag_name} can only be modified by admin" + + DataDocTagItem.create( + {"tag_name": tag_name, "datadoc_id": datadoc_id, "uid": uid}, session=session + ) + update_es_data_doc_by_id(datadoc_id) + + return tag + + @with_session def delete_tag_from_table( table_id, tag_name, user_is_admin=False, commit=True, session=None @@ -105,6 +138,27 @@ def delete_tag_from_table( session.flush() +@with_session +def delete_tag_from_datadoc( + datadoc_id, tag_name, user_is_admin=False, commit=True, session=None +): + tag_item = DataDocTagItem.get(datadoc_id=datadoc_id, tag_name=tag_name, session=session) + tag = tag_item.tag + + tag.count = tag_item.tag.count - 1 + tag.update_at = datetime.datetime.now() + if (tag.meta or {}).get("admin"): + assert user_is_admin, f"Tag {tag_name} can only be modified by admin" + + session.delete(tag_item) + + if commit: + session.commit() + update_es_data_doc_by_id(tag_item.datadoc_id) + else: + session.flush() + + @with_session def create_table_tags( table_id: int, @@ -148,6 +202,49 @@ def create_table_tags( session.flush() +@with_session +def create_datadoc_tags( + datadoc_id: int, + tags: list[DataTag] = [], + commit=True, + session=None, +): + """This function is used for loading datadoc tags from metastore.""" + # delete all tags from the table + session.query(DataDocTagItem).filter_by(datadoc_id=datadoc_id).delete() + + for tag in tags: + tag_color_name = ( + find_nearest_palette_color(tag.color)["name"] + if tag.color is not None + else None + ) + meta = { + "type": tag.type, + "tooltip": tag.description, + "color": tag_color_name, + "admin": True, + } + # filter out properties with none values + meta = {k: v for k, v in meta.items() if v is not None} + + # update or create a new tag if not exist + create_or_update_tag( + tag_name=tag.name, meta=meta, commit=commit, session=session + ) + + # add a new tag_item to associate with the datadoc + TagItem.create( + {"tag_name": tag.name, "datadoc_id": datadoc_id, "uid": None}, + session=session, + ) + + if commit: + session.commit() + else: + session.flush() + + @with_session def create_column_tags( column_id: int, diff --git a/querybook/server/models/datadoc.py b/querybook/server/models/datadoc.py index c9260a461..c041155d7 100644 --- a/querybook/server/models/datadoc.py +++ b/querybook/server/models/datadoc.py @@ -86,6 +86,7 @@ def to_dict(self, with_cells=False): "updated_at": self.updated_at, "meta": self.meta, "title": self.title, + "tags": self.tags, } if with_cells: diff --git a/querybook/server/models/tag.py b/querybook/server/models/tag.py index c7ecd40da..1ab727ae2 100644 --- a/querybook/server/models/tag.py +++ b/querybook/server/models/tag.py @@ -65,3 +65,35 @@ class TagItem(CRUDMixin, Base): backref=backref("tags", cascade="all, delete", passive_deletes=True), foreign_keys=[column_id], ) + + +class DataDocTagItem(CRUDMixin, Base): + __tablename__ = "data_doc_tag_item" + + id = sql.Column(sql.Integer, primary_key=True) + created_at = sql.Column(sql.DateTime, default=now) + + tag_name = sql.Column( + sql.String(length=name_length), + sql.ForeignKey("tag.name", ondelete="CASCADE"), + nullable=False, + ) + + datadoc_id = sql.Column( + sql.Integer, sql.ForeignKey("data_doc.id", ondelete="CASCADE"), nullable=True + ) + + uid = sql.Column( + sql.Integer, sql.ForeignKey("user.id", ondelete="SET NULL"), nullable=True + ) + + tag = relationship( + "Tag", + backref=backref("data_doc_tag_item", cascade="all, delete", passive_deletes=True), + foreign_keys=[tag_name], + ) + datadoc = relationship( + "DataDoc", + backref=backref("tags", cascade="all, delete", passive_deletes=True), + foreign_keys=[datadoc_id], + ) diff --git a/querybook/tests/test_lib/test_elasticsearch/test_elasticsearch.py b/querybook/tests/test_lib/test_elasticsearch/test_elasticsearch.py index e459e5e44..cb69880c4 100644 --- a/querybook/tests/test_lib/test_elasticsearch/test_elasticsearch.py +++ b/querybook/tests/test_lib/test_elasticsearch/test_elasticsearch.py @@ -245,6 +245,9 @@ class DataDocTestCase(TestCase): ENVIRONMENT_ID = 7 OWNER_UID = "bob" DATADOC_TITLE = "Test DataDoc" + SCHEDULED = True + + TAGS = [type('', (object,), {"tag_name": "1"})()] def _get_datadoc_cells_mock(self): return [ @@ -273,6 +276,8 @@ def _get_datadoc_mock(self): title=self.DATADOC_TITLE, public=False, cells=self._get_datadoc_cells_mock(), + scheduled=self.SCHEDULED, + tags=self.TAGS, ) return mock_doc @@ -304,6 +309,8 @@ def test_data_doc_to_es(self): "title": self.DATADOC_TITLE, "public": False, "readable_user_ids": ["alice", "charlie"], + "scheduled": self.SCHEDULED, + "tags": [tag.tag_name for tag in self.TAGS], } self.assertEqual(result, expected_result) diff --git a/querybook/webapp/components/DataDoc/DataDoc.scss b/querybook/webapp/components/DataDoc/DataDoc.scss index da6907597..492b8039c 100644 --- a/querybook/webapp/components/DataDoc/DataDoc.scss +++ b/querybook/webapp/components/DataDoc/DataDoc.scss @@ -190,3 +190,8 @@ z-index: 40; } } + +.data-doc-tag { + margin-left: 18px; + margin-bottom: 8px; +} diff --git a/querybook/webapp/components/DataDoc/DataDoc.tsx b/querybook/webapp/components/DataDoc/DataDoc.tsx index 34528e75e..13f56ffea 100644 --- a/querybook/webapp/components/DataDoc/DataDoc.tsx +++ b/querybook/webapp/components/DataDoc/DataDoc.tsx @@ -65,6 +65,8 @@ import { DataDocHeader } from './DataDocHeader'; import { DataDocLoading } from './DataDocLoading'; import './DataDoc.scss'; +import { AccentText } from "../../ui/StyledText/StyledText"; +import { DataDocTags } from "../DataDocTags/DataDocTags"; interface IOwnProps { docId: number; @@ -745,6 +747,17 @@ class DataDocComponent extends React.PureComponent { isSaving={isSaving} lastUpdated={lastUpdated} /> +
+ + Tags + + +
= ({ + datadocId, + tags, +}) => { + const dispatch: Dispatch = useDispatch(); + const [showSelect, setShowSelect] = React.useState(false); + + const existingTags = React.useMemo( + () => (tags || []).map((tag) => tag.name), + [tags] + ); + + const createTag = React.useCallback( + (tag: string) => dispatch(createDataDocTag(datadocId, tag)), + [datadocId, dispatch] + ); + + const handleCreateTag = React.useCallback( + (val: string) => { + createTag(val).finally(() => setShowSelect(false)); + }, + [createTag] + ); + + return ( +
+ {showSelect ? ( +
+ +
+ ) : ( + setShowSelect(true)} + tooltip="Add tag" + tooltipPos="down" + size={18} + invertCircle + /> + )} +
+ ); +}; diff --git a/querybook/webapp/components/DataDocTags/DataDocTagConfigModal.tsx b/querybook/webapp/components/DataDocTags/DataDocTagConfigModal.tsx new file mode 100644 index 000000000..058c895f5 --- /dev/null +++ b/querybook/webapp/components/DataDocTags/DataDocTagConfigModal.tsx @@ -0,0 +1,137 @@ +import { Formik } from 'formik'; +import React, { useCallback, useMemo } from 'react'; +import toast from 'react-hot-toast'; +import { useDispatch, useSelector } from 'react-redux'; + +import { ColorPalette } from 'const/chartColors'; +import { ITag, ITagMeta } from 'const/tag'; +import { Dispatch, IStoreState } from 'redux/store/types'; +import { updateTag } from 'redux/tag/action'; +import { AsyncButton } from 'ui/AsyncButton/AsyncButton'; +import { FormWrapper } from 'ui/Form/FormWrapper'; +import { SimpleField } from 'ui/FormikField/SimpleField'; +import { Icon } from 'ui/Icon/Icon'; +import AllLucideIcons from 'ui/Icon/LucideIcons'; +import { Modal } from 'ui/Modal/Modal'; + +export const DataDocTagConfigModal: React.FC<{ + tag: ITag; + onHide: () => void; +}> = ({ tag, onHide }) => { + const isAdmin = useSelector( + (state: IStoreState) => state.user.myUserInfo.isAdmin + ); + + const colorOptions = useMemo( + () => + ColorPalette.map((color) => ({ + value: color.name, + label: color.name, + color: color.color, + })), + [] + ); + + const iconOptions = useMemo( + () => + Object.keys(AllLucideIcons).map((iconName) => ({ + value: iconName, + label: ( + + + {iconName} + + ), + })), + [] + ); + + const initialValues: ITagMeta = useMemo( + () => ({ + rank: 0, + tooltip: undefined, + admin: false, + color: undefined, + icon: undefined, + ...tag.meta, + }), + [tag] + ); + + const dispatch = useDispatch(); + const handleSubmit = useCallback( + async (values: ITagMeta) => { + toast.promise( + dispatch( + updateTag({ + ...tag, + meta: values, + }) + ), + { + success: 'Tag updated!', + loading: 'Updating tag', + error: 'Failed to updated tag', + } + ); + onHide(); + }, + [dispatch, onHide, tag] + ); + + const form = ( + + {({ submitForm }) => ( +
+ + + + + + {isAdmin && ( + + )} + + +
+ +
+
+ )} +
+ ); + + return ( + + {form} + + ); +}; diff --git a/querybook/webapp/components/DataDocTags/DataDocTagGroupSelect.tsx b/querybook/webapp/components/DataDocTags/DataDocTagGroupSelect.tsx new file mode 100644 index 000000000..581b0cc97 --- /dev/null +++ b/querybook/webapp/components/DataDocTags/DataDocTagGroupSelect.tsx @@ -0,0 +1,46 @@ +import React, { useMemo } from 'react'; + +import { HoverIconTag } from 'ui/Tag/HoverIconTag'; + +import { DataDocTagSelect } from './DataDocTagSelect'; + +export const DataDocTagGroupSelect: React.FC<{ + tags?: string[]; + updateTags: (newTags: string[]) => void; +}> = ({ tags: propsTag, updateTags }) => { + const tags = useMemo(() => propsTag ?? [], [propsTag]); + + const handleTagSelect = React.useCallback( + (tag: string) => { + updateTags([...tags, tag]); + }, + [tags, updateTags] + ); + + const handleTagRemove = React.useCallback( + (tag: string) => { + updateTags(tags.filter((existingTag) => existingTag !== tag)); + }, + [tags, updateTags] + ); + + const tagsListDOM = tags.length ? ( +
+ {tags.map((tag) => ( + handleTagRemove(tag)} + /> + ))} +
+ ) : null; + + return ( +
+ {tagsListDOM} + +
+ ); +}; diff --git a/querybook/webapp/components/DataDocTags/DataDocTagSelect.scss b/querybook/webapp/components/DataDocTags/DataDocTagSelect.scss new file mode 100644 index 000000000..d7eaa3bd1 --- /dev/null +++ b/querybook/webapp/components/DataDocTags/DataDocTagSelect.scss @@ -0,0 +1,13 @@ +.DataDocTagSelect { + min-width: 180px; + margin: 0px; + padding: 0px; + > div { + width: 100%; + } + border: 1px solid transparent; + border-radius: var(--border-radius-sm); + &.invalid-string { + border-color: var(--color-false); + } +} diff --git a/querybook/webapp/components/DataDocTags/DataDocTagSelect.tsx b/querybook/webapp/components/DataDocTags/DataDocTagSelect.tsx new file mode 100644 index 000000000..e52db0f0a --- /dev/null +++ b/querybook/webapp/components/DataDocTags/DataDocTagSelect.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; + +import { useDebounce } from 'hooks/useDebounce'; +import { useResource } from 'hooks/useResource'; +import { + makeReactSelectStyle, + miniReactSelectStyles, +} from 'lib/utils/react-select'; +import { DataDocTagResource } from 'resource/dataDoc'; +import { SimpleReactSelect } from 'ui/SimpleReactSelect/SimpleReactSelect'; + +import './DataDocTagSelect.scss'; + +interface IProps { + onSelect: (val: string) => any; + existingTags?: string[]; + creatable?: boolean; +} + +const tagReactSelectStyle = makeReactSelectStyle(true, miniReactSelectStyles); + +function isTagValid(val: string, existingTags: string[]) { + const regex = /^[a-z0-9_ ]{1,255}$/i; + const match = val.match(regex); + return Boolean(match && !existingTags.includes(val)); +} + +export const DataDocTagSelect: React.FunctionComponent = ({ + onSelect, + existingTags = [], + creatable = false, +}) => { + const [tagString, setTagString] = React.useState(''); + const [isTyping, setIsTyping] = React.useState(false); + const debouncedTagString = useDebounce(tagString, 500); + + const { data: rawTagSuggestions } = useResource( + React.useCallback( + () => DataDocTagResource.search(debouncedTagString), + [debouncedTagString] + ) + ); + + const tagSuggestions = React.useMemo( + () => + (rawTagSuggestions || []).filter( + (str) => !existingTags.includes(str) + ), + [rawTagSuggestions, existingTags] + ); + + const isValid = React.useMemo( + () => + isTyping ? !tagString || isTagValid(tagString, existingTags) : true, + [existingTags, tagString, isTyping] + ); + + const handleSelect = React.useCallback( + (val: string) => { + const tagVal = val ?? tagString; + const valid = isTagValid(tagVal, existingTags); + if (valid) { + setTagString(''); + onSelect(tagVal); + } + }, + [tagString, onSelect, existingTags] + ); + + return ( +
+ handleSelect(val)} + selectProps={{ + onInputChange: (newValue) => setTagString(newValue), + placeholder: 'alphanumeric only', + styles: tagReactSelectStyle, + onFocus: () => setIsTyping(true), + onBlur: () => setIsTyping(false), + noOptionsMessage: () => null, + }} + clearAfterSelect + /> +
+ ); +}; diff --git a/querybook/webapp/components/DataDocTags/DataDocTags.scss b/querybook/webapp/components/DataDocTags/DataDocTags.scss new file mode 100644 index 000000000..e4cf37e51 --- /dev/null +++ b/querybook/webapp/components/DataDocTags/DataDocTags.scss @@ -0,0 +1,8 @@ +.DataDocTags { + flex-wrap: wrap; + + .DataDocTag { + margin: 4px 12px 4px 0px; + cursor: pointer; + } +} diff --git a/querybook/webapp/components/DataDocTags/DataDocTags.tsx b/querybook/webapp/components/DataDocTags/DataDocTags.tsx new file mode 100644 index 000000000..ea47ab433 --- /dev/null +++ b/querybook/webapp/components/DataDocTags/DataDocTags.tsx @@ -0,0 +1,162 @@ +import qs from 'qs'; +import * as React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { ITag } from 'const/tag'; +import { stopPropagationAndDefault } from 'lib/utils/noop'; +import { navigateWithinEnv } from 'lib/utils/query-string'; +import { useRankedTags } from 'lib/utils/tag'; +import { Dispatch, IStoreState } from 'redux/store/types'; +import { + deleteDataDocTag, + fetchDataDocTagsFromTableIfNeeded, +} from 'redux/tag/action'; +import { tagsInDataDocSelector } from 'redux/tag/selector'; +import { ContextMenu } from 'ui/ContextMenu/ContextMenu'; +import { Icon } from 'ui/Icon/Icon'; +import { Menu, MenuItem } from 'ui/Menu/Menu'; +import { HoverIconTag } from 'ui/Tag/HoverIconTag'; + +import { CreateDataDocTag } from './CreateDataDocTag'; +import { DataDocTagConfigModal } from './DataDocTagConfigModal'; + +import './DataDocTags.scss'; + +interface IProps { + datadocId: number; + readonly?: boolean; + mini?: boolean; + showType?: boolean; +} + +export const DataDocTags: React.FunctionComponent = ({ + datadocId, + readonly = false, + mini = false, + showType = true, +}) => { + const isUserAdmin = useSelector( + (state: IStoreState) => state.user.myUserInfo.isAdmin + ); + const dispatch: Dispatch = useDispatch(); + const loadTags = React.useCallback( + () => dispatch(fetchDataDocTagsFromTableIfNeeded(datadocId)), + [datadocId] + ); + const deleteTag = React.useCallback( + (tagName: string) => dispatch(deleteDataDocTag(datadocId, tagName)), + [datadocId] + ); + + const tags = useRankedTags( + useSelector((state: IStoreState) => tagsInDataDocSelector(state, datadocId)) + ); + + React.useEffect(() => { + loadTags(); + }, []); + + const listDOM = (tags || []).map((tag) => ( + + )); + + return ( +
+ {listDOM} + {readonly ? null : ( + + )} +
+ ); +}; + +export const DataDocTag: React.FC<{ + tag: ITag; + + isUserAdmin?: boolean; + readonly?: boolean; + deleteTag?: (tagName: string) => void; + mini?: boolean; + showType?: boolean; +}> = ({ tag, readonly, deleteTag, isUserAdmin, mini, showType = true }) => { + const tagMeta = tag.meta ?? {}; + const tagRef = React.useRef(); + const [showConfigModal, setShowConfigModal] = React.useState(false); + + const handleDeleteTag = React.useCallback( + (e: React.MouseEvent) => { + stopPropagationAndDefault(e); + deleteTag(tag.name); + }, + [deleteTag, tag.name] + ); + const handleTagClick = React.useCallback(() => { + navigateWithinEnv( + `/search/?${qs.stringify({ + searchType: 'DataDoc', + 'searchFilters[tags][0]': tag.name, + })}`, + { + isModal: true, + } + ); + }, [tag.name]); + + // user can update if it is not readonly and passes the admin check + const canUserUpdate = !(readonly || (tagMeta.admin && !isUserAdmin)); + const canUserDelete = !readonly && canUserUpdate; + + const renderContextMenu = () => ( + + setShowConfigModal(true)}> + + Configure Tag + + + + Remove Tag + + + ); + + return ( +
+ {canUserUpdate && ( + + )} + + {showConfigModal && ( + setShowConfigModal(false)} + /> + )} + +
+ ); +}; diff --git a/querybook/webapp/components/Search/SearchOverview.tsx b/querybook/webapp/components/Search/SearchOverview.tsx index 7f7296b6c..4dbc57081 100644 --- a/querybook/webapp/components/Search/SearchOverview.tsx +++ b/querybook/webapp/components/Search/SearchOverview.tsx @@ -34,6 +34,7 @@ import * as searchActions from 'redux/search/action'; import { RESULT_PER_PAGE, SearchOrder, SearchType } from 'redux/search/types'; import { IStoreState } from 'redux/store/types'; import { DataElementResource, TableTagResource } from 'resource/table'; +import { DataDocTagResource } from 'resource/dataDoc'; import { Button } from 'ui/Button/Button'; import { Checkbox } from 'ui/Checkbox/Checkbox'; import { Container } from 'ui/Container/Container'; @@ -349,6 +350,15 @@ export const SearchOverview: React.FC = ({ /> ); + const dataDocTagDOM = ( + + ); + const queryMetastore = searchType === SearchType.Table && queryMetastores.find((metastore) => metastore.id === metastoreId); @@ -692,6 +702,16 @@ export const SearchOverview: React.FC = ({ Authors {getAuthorFiltersDOM('owner_uid')}
+
+ + Tags + + {dataDocTagDOM} +
Created At {dateFilterDOM} diff --git a/querybook/webapp/components/Search/SearchResultItem.tsx b/querybook/webapp/components/Search/SearchResultItem.tsx index 8266c94ac..4b36cc77b 100644 --- a/querybook/webapp/components/Search/SearchResultItem.tsx +++ b/querybook/webapp/components/Search/SearchResultItem.tsx @@ -286,6 +286,16 @@ export const DataDocItem: React.FunctionComponent = ({ /> ); + const tagsListDOM = preview.tags?.length ? ( +
+ {preview.tags.map((tag) => ( + + {tag} + + ))} +
+ ) : null; + return (
= ({ searchString={searchString} />
+ {tagsListDOM} {descriptionDOM} diff --git a/querybook/webapp/const/search.ts b/querybook/webapp/const/search.ts index 59c4f7df1..cd53c211c 100644 --- a/querybook/webapp/const/search.ts +++ b/querybook/webapp/const/search.ts @@ -3,6 +3,8 @@ export interface IDataDocPreview { created_at: number; title: string; owner_uid: number; + scheduled: boolean; + tags: string[]; highlight?: { cells?: string[]; }; diff --git a/querybook/webapp/redux/tag/action.ts b/querybook/webapp/redux/tag/action.ts index 9ccd992ad..48fd938dd 100644 --- a/querybook/webapp/redux/tag/action.ts +++ b/querybook/webapp/redux/tag/action.ts @@ -1,8 +1,10 @@ import { ITag } from 'const/tag'; import { TableTagResource } from 'resource/table'; +import { DataDocTagResource } from 'resource/dataDoc'; import { ThunkResult } from './types'; +// Table Tags function fetchTableTagsFromTable( tableId: number ): ThunkResult> { @@ -76,3 +78,64 @@ export function updateTag(tag: ITag): ThunkResult> { return newTag; }; } + +// Datadoc Tags +function fetchDataDocTagsFromTable( + datadocId: number +): ThunkResult> { + return async (dispatch) => { + const { data } = await DataDocTagResource.get(datadocId); + dispatch({ + type: '@@tag/RECEIVE_TAGS_BY_DATADOC', + payload: { datadocId, tags: data }, + }); + return data; + }; +} + +export function fetchDataDocTagsFromTableIfNeeded( + datadocId: number +): ThunkResult> { + return (dispatch, getState) => { + const state = getState(); + const tags = state.tag.datadocIdToTagName[datadocId]; + if (!tags) { + return dispatch(fetchDataDocTagsFromTable(datadocId)); + } + }; +} + +export function createDataDocTag( + datadocId: number, + tag: string +): ThunkResult> { + return async (dispatch) => { + try { + const { data } = await DataDocTagResource.create(datadocId, tag); + dispatch({ + type: '@@tag/RECEIVE_TAG_BY_DATADOC', + payload: { datadocId, tag: data }, + }); + return data; + } catch (e) { + console.error(e); + } + }; +} + +export function deleteDataDocTag( + datadocId: number, + tagName: string +): ThunkResult> { + return async (dispatch) => { + try { + await DataDocTagResource.delete(datadocId, tagName); + dispatch({ + type: '@@tag/REMOVE_TAG_FROM_DATADOC', + payload: { datadocId, tagName }, + }); + } catch (e) { + console.error(e); + } + }; +} diff --git a/querybook/webapp/redux/tag/reducer.ts b/querybook/webapp/redux/tag/reducer.ts index d28b99602..d72b2d2dd 100644 --- a/querybook/webapp/redux/tag/reducer.ts +++ b/querybook/webapp/redux/tag/reducer.ts @@ -4,7 +4,9 @@ import { ITagState, TagAction } from './types'; const initialState: ITagState = { tableIdToTagName: {}, + datadocIdToTagName: {}, tagByName: {}, + datadocTagByName: {}, }; function tagReducer(state = initialState, action: TagAction) { @@ -19,17 +21,37 @@ function tagReducer(state = initialState, action: TagAction) { } return; } + case '@@tag/RECEIVE_TAGS_BY_DATADOC': { + const { datadocId, tags } = action.payload; + + draft.datadocIdToTagName[datadocId] = tags.map((t) => t.name); + for (const tag of tags) { + draft.datadocTagByName[tag.name] = tag; + } + return; + } case '@@tag/RECEIVE_TAG': { const { tag } = action.payload; draft.tagByName[tag.name] = tag; return; } + case '@@tag/RECEIVE_DATADOC_TAG': { + const { tag } = action.payload; + draft.datadocTagByName[tag.name] = tag; + return; + } case '@@tag/RECEIVE_TAG_BY_TABLE': { const { tableId, tag } = action.payload; draft.tableIdToTagName[tableId].push(tag.name); draft.tagByName[tag.name] = tag; return; } + case '@@tag/RECEIVE_TAG_BY_DATADOC': { + const { datadocId, tag } = action.payload; + draft.datadocIdToTagName[datadocId].push(tag.name); + draft.datadocTagByName[tag.name] = tag; + return; + } case '@@tag/REMOVE_TAG_FROM_TABLE': { const { tableId, tagName } = action.payload; @@ -38,6 +60,14 @@ function tagReducer(state = initialState, action: TagAction) { ].filter((tName) => tName !== tagName); return; } + case '@@tag/REMOVE_TAG_FROM_DATADOC': { + const { datadocId, tagName } = action.payload; + + draft.datadocIdToTagName[datadocId] = draft.datadocIdToTagName[ + datadocId + ].filter((tName) => tName !== tagName); + return; + } } }); } diff --git a/querybook/webapp/redux/tag/selector.ts b/querybook/webapp/redux/tag/selector.ts index 4e9a11923..8c4c00b1c 100644 --- a/querybook/webapp/redux/tag/selector.ts +++ b/querybook/webapp/redux/tag/selector.ts @@ -11,3 +11,13 @@ export const tagsInTableSelector = createSelector( tagByNameSelector, (tagNames, tagByName) => tagNames.map((name) => tagByName[name]) ); + +const datadocTagNameSelector = (state: IStoreState, datadocId: number) => + state.tag.datadocIdToTagName[datadocId] || []; +const datadocTagByNameSelector = (state: IStoreState) => state.tag.datadocTagByName; + +export const tagsInDataDocSelector = createSelector( + datadocTagNameSelector, + datadocTagByNameSelector, + (tagNames, tagByName) => tagNames.map((name) => tagByName[name]) +) diff --git a/querybook/webapp/redux/tag/types.ts b/querybook/webapp/redux/tag/types.ts index f6d5821da..1cd1f7a97 100644 --- a/querybook/webapp/redux/tag/types.ts +++ b/querybook/webapp/redux/tag/types.ts @@ -12,6 +12,14 @@ export interface IRecieveTagsByTable extends Action { }; } +export interface IReceiveTagsByDataDoc extends Action { + type: '@@tag/RECEIVE_TAGS_BY_DATADOC'; + payload: { + datadocId: number; + tags: ITag[]; + }; +} + export interface IRecieveTagByTable extends Action { type: '@@tag/RECEIVE_TAG_BY_TABLE'; payload: { @@ -20,6 +28,14 @@ export interface IRecieveTagByTable extends Action { }; } +export interface IReceiveTagByDataDoc extends Action { + type: '@@tag/RECEIVE_TAG_BY_DATADOC'; + payload: { + datadocId: number; + tag: ITag; + }; +} + export interface IRecieveTag extends Action { type: '@@tag/RECEIVE_TAG'; payload: { @@ -27,6 +43,13 @@ export interface IRecieveTag extends Action { }; } +export interface IReceiveDataDocTag extends Action { + type: '@@tag/RECEIVE_DATADOC_TAG'; + payload: { + tag: ITag; + }; +} + export interface IRemoveTagFromTable extends Action { type: '@@tag/REMOVE_TAG_FROM_TABLE'; payload: { @@ -35,15 +58,29 @@ export interface IRemoveTagFromTable extends Action { }; } +export interface IRemoveTagFromDataDoc extends Action { + type: '@@tag/REMOVE_TAG_FROM_DATADOC'; + payload: { + datadocId: number; + tagName: string; + }; +} + export type TagAction = | IRecieveTagsByTable + | IReceiveTagsByDataDoc | IRecieveTagByTable + | IReceiveTagByDataDoc | IRemoveTagFromTable - | IRecieveTag; + | IRemoveTagFromDataDoc + | IRecieveTag + | IReceiveDataDocTag; export type ThunkResult = ThunkAction; export interface ITagState { tableIdToTagName: Record; + datadocIdToTagName: Record; tagByName: Record; + datadocTagByName: Record; } diff --git a/querybook/webapp/resource/dataDoc.ts b/querybook/webapp/resource/dataDoc.ts index f6b2cc038..8a324619c 100644 --- a/querybook/webapp/resource/dataDoc.ts +++ b/querybook/webapp/resource/dataDoc.ts @@ -20,6 +20,7 @@ import { IScheduledDoc, ITransformedScheduledDocFilters, } from 'redux/scheduledDataDoc/types'; +import { ITag } from "../const/tag"; export const DataDocResource = { getAll: (filterMode: string, environmentId: number) => @@ -205,3 +206,17 @@ export const DataDocScheduleResource = { } ), }; + +export const DataDocTagResource = { + get: (datadocId: number) => ds.fetch(`/datadoc/${datadocId}/tag/`), + search: (keyword: string) => + ds.fetch(`/tag/keyword/`, { keyword }), + create: (datadocId: number, tag: string) => + ds.save(`/datadoc/${datadocId}/tag/`, { tag }), + delete: (datadocId: number, tagName: string) => + ds.delete(`/datadoc/${datadocId}/tag/`, { tag_name: tagName }), + update: (tag: ITag) => + ds.update(`/tag/${tag.id}/`, { + meta: tag.meta, + }), +}; From 775f89c96e6eb86d223560b00e47dbb8002b1262 Mon Sep 17 00:00:00 2001 From: jij1949 Date: Mon, 5 Jun 2023 08:23:40 -0700 Subject: [PATCH 02/11] Fix lint errors in logic/tag.py --- querybook/server/logic/tag.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/querybook/server/logic/tag.py b/querybook/server/logic/tag.py index 9e0e085e2..c3e60d7ad 100644 --- a/querybook/server/logic/tag.py +++ b/querybook/server/logic/tag.py @@ -142,7 +142,9 @@ def delete_tag_from_table( def delete_tag_from_datadoc( datadoc_id, tag_name, user_is_admin=False, commit=True, session=None ): - tag_item = DataDocTagItem.get(datadoc_id=datadoc_id, tag_name=tag_name, session=session) + tag_item = DataDocTagItem.get( + datadoc_id=datadoc_id, tag_name=tag_name, session=session + ) tag = tag_item.tag tag.count = tag_item.tag.count - 1 @@ -204,10 +206,10 @@ def create_table_tags( @with_session def create_datadoc_tags( - datadoc_id: int, - tags: list[DataTag] = [], - commit=True, - session=None, + datadoc_id: int, + tags: list[DataTag] = [], + commit=True, + session=None, ): """This function is used for loading datadoc tags from metastore.""" # delete all tags from the table From 7137a42e15d9f11e88e35f9996a8c215b380217f Mon Sep 17 00:00:00 2001 From: jij1949 Date: Mon, 5 Jun 2023 08:24:47 -0700 Subject: [PATCH 03/11] Fix lint errors in models/tag.py --- querybook/server/models/tag.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/querybook/server/models/tag.py b/querybook/server/models/tag.py index 1ab727ae2..6752028b6 100644 --- a/querybook/server/models/tag.py +++ b/querybook/server/models/tag.py @@ -89,7 +89,9 @@ class DataDocTagItem(CRUDMixin, Base): tag = relationship( "Tag", - backref=backref("data_doc_tag_item", cascade="all, delete", passive_deletes=True), + backref=backref( + "data_doc_tag_item", cascade="all, delete", passive_deletes=True + ), foreign_keys=[tag_name], ) datadoc = relationship( From 56bab992e7306f89b61fd8978110a1c0e34bf040 Mon Sep 17 00:00:00 2001 From: jij1949 Date: Mon, 5 Jun 2023 08:25:55 -0700 Subject: [PATCH 04/11] Fix lint errors in test_elasticsearch.py --- .../tests/test_lib/test_elasticsearch/test_elasticsearch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/querybook/tests/test_lib/test_elasticsearch/test_elasticsearch.py b/querybook/tests/test_lib/test_elasticsearch/test_elasticsearch.py index cb69880c4..d76a7b0d5 100644 --- a/querybook/tests/test_lib/test_elasticsearch/test_elasticsearch.py +++ b/querybook/tests/test_lib/test_elasticsearch/test_elasticsearch.py @@ -247,7 +247,7 @@ class DataDocTestCase(TestCase): DATADOC_TITLE = "Test DataDoc" SCHEDULED = True - TAGS = [type('', (object,), {"tag_name": "1"})()] + TAGS = [type("", (object,), {"tag_name": "1"})()] def _get_datadoc_cells_mock(self): return [ From b40e302e1c3e17518f6f56609e3e1bb2ff2fa8a2 Mon Sep 17 00:00:00 2001 From: jij1949 Date: Mon, 5 Jun 2023 08:28:01 -0700 Subject: [PATCH 05/11] Fix lint errors in DataDoc.tsx --- querybook/webapp/components/DataDoc/DataDoc.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/querybook/webapp/components/DataDoc/DataDoc.tsx b/querybook/webapp/components/DataDoc/DataDoc.tsx index 13f56ffea..9e93224b7 100644 --- a/querybook/webapp/components/DataDoc/DataDoc.tsx +++ b/querybook/webapp/components/DataDoc/DataDoc.tsx @@ -65,8 +65,8 @@ import { DataDocHeader } from './DataDocHeader'; import { DataDocLoading } from './DataDocLoading'; import './DataDoc.scss'; -import { AccentText } from "../../ui/StyledText/StyledText"; -import { DataDocTags } from "../DataDocTags/DataDocTags"; +import { AccentText } from '../../ui/StyledText/StyledText'; +import { DataDocTags } from '../DataDocTags/DataDocTags'; interface IOwnProps { docId: number; @@ -756,7 +756,7 @@ class DataDocComponent extends React.PureComponent { > Tags - +
Date: Mon, 5 Jun 2023 08:29:17 -0700 Subject: [PATCH 06/11] Fix lint errors in DataDocTags.tsx --- querybook/webapp/components/DataDocTags/DataDocTags.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/querybook/webapp/components/DataDocTags/DataDocTags.tsx b/querybook/webapp/components/DataDocTags/DataDocTags.tsx index ea47ab433..9e3bc53c1 100644 --- a/querybook/webapp/components/DataDocTags/DataDocTags.tsx +++ b/querybook/webapp/components/DataDocTags/DataDocTags.tsx @@ -49,7 +49,9 @@ export const DataDocTags: React.FunctionComponent = ({ ); const tags = useRankedTags( - useSelector((state: IStoreState) => tagsInDataDocSelector(state, datadocId)) + useSelector((state: IStoreState) => + tagsInDataDocSelector(state, datadocId) + ) ); React.useEffect(() => { From 3f6432b30cf2a7fcd0c63ec7b2ab78ffcb218533 Mon Sep 17 00:00:00 2001 From: jij1949 Date: Mon, 5 Jun 2023 08:30:45 -0700 Subject: [PATCH 07/11] Fix lint errors in tag/selector.ts --- querybook/webapp/redux/tag/selector.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/querybook/webapp/redux/tag/selector.ts b/querybook/webapp/redux/tag/selector.ts index 8c4c00b1c..3748a2642 100644 --- a/querybook/webapp/redux/tag/selector.ts +++ b/querybook/webapp/redux/tag/selector.ts @@ -14,10 +14,11 @@ export const tagsInTableSelector = createSelector( const datadocTagNameSelector = (state: IStoreState, datadocId: number) => state.tag.datadocIdToTagName[datadocId] || []; -const datadocTagByNameSelector = (state: IStoreState) => state.tag.datadocTagByName; +const datadocTagByNameSelector = (state: IStoreState) => + state.tag.datadocTagByName; export const tagsInDataDocSelector = createSelector( datadocTagNameSelector, datadocTagByNameSelector, (tagNames, tagByName) => tagNames.map((name) => tagByName[name]) -) +); From e3f399c5c12d8d7104eb6f46caf309cf764d7a09 Mon Sep 17 00:00:00 2001 From: jij1949 Date: Mon, 5 Jun 2023 08:31:20 -0700 Subject: [PATCH 08/11] Fix lint errors in dataDoc.ts --- querybook/webapp/resource/dataDoc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/querybook/webapp/resource/dataDoc.ts b/querybook/webapp/resource/dataDoc.ts index 8a324619c..4c854e4d9 100644 --- a/querybook/webapp/resource/dataDoc.ts +++ b/querybook/webapp/resource/dataDoc.ts @@ -20,7 +20,7 @@ import { IScheduledDoc, ITransformedScheduledDocFilters, } from 'redux/scheduledDataDoc/types'; -import { ITag } from "../const/tag"; +import { ITag } from '../const/tag'; export const DataDocResource = { getAll: (filterMode: string, environmentId: number) => From 13e74cfa29dc317482c9635e834b676f7db07d00 Mon Sep 17 00:00:00 2001 From: jvalvo Date: Tue, 29 Aug 2023 11:12:00 -0700 Subject: [PATCH 09/11] feat: condensed similar functions to same function --- Dockerfile | 2 +- querybook/server/datasources/tag.py | 16 +- .../lib/elasticsearch/search_datadoc.py | 2 +- querybook/server/logic/elasticsearch.py | 1 - querybook/server/logic/tag.py | 43 ------ .../test_elasticsearch/test_elasticsearch.py | 3 - .../DataDocTags/CreateDataDocTag.scss | 0 .../DataDocTags/CreateDataDocTag.tsx | 61 +------- .../DataDocTags/DataDocTagConfigModal.tsx | 137 +----------------- .../DataTableTags/CreateDataTableTag.tsx | 61 +------- .../DataTableTags/TableTagConfigModal.tsx | 137 +----------------- .../components/GenericTags/CreateTag.tsx | 78 ++++++++++ .../components/GenericTags/TagConfigModal.tsx | 136 +++++++++++++++++ 13 files changed, 235 insertions(+), 442 deletions(-) delete mode 100644 querybook/webapp/components/DataDocTags/CreateDataDocTag.scss create mode 100644 querybook/webapp/components/GenericTags/CreateTag.tsx create mode 100644 querybook/webapp/components/GenericTags/TagConfigModal.tsx diff --git a/Dockerfile b/Dockerfile index ab4899a28..bee5fbad3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # Cannot upgrade to Python 3.10 until the following uWSGI release a new version: # https://github.com/unbit/uwsgi/pull/2363 # This caused websocket to fail -FROM python:3.9 +FROM python:3.9.16 ARG PRODUCTION=true ARG EXTRA_PIP_INSTALLS="" diff --git a/querybook/server/datasources/tag.py b/querybook/server/datasources/tag.py index da6c29568..ae8b54413 100644 --- a/querybook/server/datasources/tag.py +++ b/querybook/server/datasources/tag.py @@ -78,15 +78,13 @@ def add_tag_to_table(table_id, tag): methods=["POST"], ) def add_tag_to_datadoc(datadoc_id, tag): - with DBSession() as session: - verify_data_doc_permission(datadoc_id, session=session) - return logic.add_tag_to_datadoc( - datadoc_id=datadoc_id, - tag_name=tag, - uid=current_user.id, - user_is_admin=current_user.is_admin, - session=session, - ) + verify_data_doc_permission(datadoc_id) + return logic.add_tag_to_datadoc( + datadoc_id=datadoc_id, + tag_name=tag, + uid=current_user.id, + user_is_admin=current_user.is_admin, + ) @register( diff --git a/querybook/server/lib/elasticsearch/search_datadoc.py b/querybook/server/lib/elasticsearch/search_datadoc.py index 41f0d0b95..154ec4001 100644 --- a/querybook/server/lib/elasticsearch/search_datadoc.py +++ b/querybook/server/lib/elasticsearch/search_datadoc.py @@ -53,7 +53,7 @@ def construct_datadoc_query( "query": { "bool": combine_keyword_and_filter_query(keywords_query, search_filter) }, - "_source": ["id", "title", "owner_uid", "created_at", "scheduled", "tags"], + "_source": ["id", "title", "owner_uid", "created_at", "tags"], "size": limit, "from": offset, } diff --git a/querybook/server/logic/elasticsearch.py b/querybook/server/logic/elasticsearch.py index 323b82aed..b6cf36a7d 100644 --- a/querybook/server/logic/elasticsearch.py +++ b/querybook/server/logic/elasticsearch.py @@ -438,7 +438,6 @@ def datadocs_to_es(datadoc, fields=None, session=None): "title": datadoc.title, "public": datadoc.public, "readable_user_ids": lambda: _get_datadoc_editors(datadoc, session=session), - "scheduled": datadoc.scheduled, "tags": [tag.tag_name for tag in datadoc.tags], } return _get_dict_by_field(field_to_getter, fields=fields) diff --git a/querybook/server/logic/tag.py b/querybook/server/logic/tag.py index c3e60d7ad..2afe63325 100644 --- a/querybook/server/logic/tag.py +++ b/querybook/server/logic/tag.py @@ -204,49 +204,6 @@ def create_table_tags( session.flush() -@with_session -def create_datadoc_tags( - datadoc_id: int, - tags: list[DataTag] = [], - commit=True, - session=None, -): - """This function is used for loading datadoc tags from metastore.""" - # delete all tags from the table - session.query(DataDocTagItem).filter_by(datadoc_id=datadoc_id).delete() - - for tag in tags: - tag_color_name = ( - find_nearest_palette_color(tag.color)["name"] - if tag.color is not None - else None - ) - meta = { - "type": tag.type, - "tooltip": tag.description, - "color": tag_color_name, - "admin": True, - } - # filter out properties with none values - meta = {k: v for k, v in meta.items() if v is not None} - - # update or create a new tag if not exist - create_or_update_tag( - tag_name=tag.name, meta=meta, commit=commit, session=session - ) - - # add a new tag_item to associate with the datadoc - TagItem.create( - {"tag_name": tag.name, "datadoc_id": datadoc_id, "uid": None}, - session=session, - ) - - if commit: - session.commit() - else: - session.flush() - - @with_session def create_column_tags( column_id: int, diff --git a/querybook/tests/test_lib/test_elasticsearch/test_elasticsearch.py b/querybook/tests/test_lib/test_elasticsearch/test_elasticsearch.py index d76a7b0d5..f8711aeb7 100644 --- a/querybook/tests/test_lib/test_elasticsearch/test_elasticsearch.py +++ b/querybook/tests/test_lib/test_elasticsearch/test_elasticsearch.py @@ -245,7 +245,6 @@ class DataDocTestCase(TestCase): ENVIRONMENT_ID = 7 OWNER_UID = "bob" DATADOC_TITLE = "Test DataDoc" - SCHEDULED = True TAGS = [type("", (object,), {"tag_name": "1"})()] @@ -276,7 +275,6 @@ def _get_datadoc_mock(self): title=self.DATADOC_TITLE, public=False, cells=self._get_datadoc_cells_mock(), - scheduled=self.SCHEDULED, tags=self.TAGS, ) return mock_doc @@ -309,7 +307,6 @@ def test_data_doc_to_es(self): "title": self.DATADOC_TITLE, "public": False, "readable_user_ids": ["alice", "charlie"], - "scheduled": self.SCHEDULED, "tags": [tag.tag_name for tag in self.TAGS], } self.assertEqual(result, expected_result) diff --git a/querybook/webapp/components/DataDocTags/CreateDataDocTag.scss b/querybook/webapp/components/DataDocTags/CreateDataDocTag.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/querybook/webapp/components/DataDocTags/CreateDataDocTag.tsx b/querybook/webapp/components/DataDocTags/CreateDataDocTag.tsx index 73d02a4ee..7aaecc26e 100644 --- a/querybook/webapp/components/DataDocTags/CreateDataDocTag.tsx +++ b/querybook/webapp/components/DataDocTags/CreateDataDocTag.tsx @@ -1,15 +1,6 @@ import * as React from 'react'; -import { useDispatch } from 'react-redux'; - -import { EntitySelect } from 'components/Search/EntitySelect'; import { ITag } from 'const/tag'; -import { isTagValid } from 'lib/utils/tag'; -import { Dispatch } from 'redux/store/types'; -import { createDataDocTag } from 'redux/tag/action'; -import { DataDocTagResource } from 'resource/dataDoc'; -import { IconButton } from 'ui/Button/IconButton'; - -import './CreateDataDocTag.scss'; +import { CreateTag } from '../GenericTags/CreateTag'; interface IProps { datadocId: number; @@ -19,52 +10,4 @@ interface IProps { export const CreateDataDocTag: React.FunctionComponent = ({ datadocId, tags, -}) => { - const dispatch: Dispatch = useDispatch(); - const [showSelect, setShowSelect] = React.useState(false); - - const existingTags = React.useMemo( - () => (tags || []).map((tag) => tag.name), - [tags] - ); - - const createTag = React.useCallback( - (tag: string) => dispatch(createDataDocTag(datadocId, tag)), - [datadocId, dispatch] - ); - - const handleCreateTag = React.useCallback( - (val: string) => { - createTag(val).finally(() => setShowSelect(false)); - }, - [createTag] - ); - - return ( -
- {showSelect ? ( -
- -
- ) : ( - setShowSelect(true)} - tooltip="Add tag" - tooltipPos="down" - size={18} - invertCircle - /> - )} -
- ); -}; +}) => ; diff --git a/querybook/webapp/components/DataDocTags/DataDocTagConfigModal.tsx b/querybook/webapp/components/DataDocTags/DataDocTagConfigModal.tsx index 058c895f5..3dbce5561 100644 --- a/querybook/webapp/components/DataDocTags/DataDocTagConfigModal.tsx +++ b/querybook/webapp/components/DataDocTags/DataDocTagConfigModal.tsx @@ -1,137 +1,8 @@ -import { Formik } from 'formik'; -import React, { useCallback, useMemo } from 'react'; -import toast from 'react-hot-toast'; -import { useDispatch, useSelector } from 'react-redux'; - -import { ColorPalette } from 'const/chartColors'; -import { ITag, ITagMeta } from 'const/tag'; -import { Dispatch, IStoreState } from 'redux/store/types'; -import { updateTag } from 'redux/tag/action'; -import { AsyncButton } from 'ui/AsyncButton/AsyncButton'; -import { FormWrapper } from 'ui/Form/FormWrapper'; -import { SimpleField } from 'ui/FormikField/SimpleField'; -import { Icon } from 'ui/Icon/Icon'; -import AllLucideIcons from 'ui/Icon/LucideIcons'; -import { Modal } from 'ui/Modal/Modal'; +import React from 'react'; +import { ITag } from 'const/tag'; +import { TagConfigModal } from '../GenericTags/TagConfigModal'; export const DataDocTagConfigModal: React.FC<{ tag: ITag; onHide: () => void; -}> = ({ tag, onHide }) => { - const isAdmin = useSelector( - (state: IStoreState) => state.user.myUserInfo.isAdmin - ); - - const colorOptions = useMemo( - () => - ColorPalette.map((color) => ({ - value: color.name, - label: color.name, - color: color.color, - })), - [] - ); - - const iconOptions = useMemo( - () => - Object.keys(AllLucideIcons).map((iconName) => ({ - value: iconName, - label: ( - - - {iconName} - - ), - })), - [] - ); - - const initialValues: ITagMeta = useMemo( - () => ({ - rank: 0, - tooltip: undefined, - admin: false, - color: undefined, - icon: undefined, - ...tag.meta, - }), - [tag] - ); - - const dispatch = useDispatch(); - const handleSubmit = useCallback( - async (values: ITagMeta) => { - toast.promise( - dispatch( - updateTag({ - ...tag, - meta: values, - }) - ), - { - success: 'Tag updated!', - loading: 'Updating tag', - error: 'Failed to updated tag', - } - ); - onHide(); - }, - [dispatch, onHide, tag] - ); - - const form = ( - - {({ submitForm }) => ( -
- - - - - - {isAdmin && ( - - )} - - -
- -
-
- )} -
- ); - - return ( - - {form} - - ); -}; +}> = ({ tag, onHide }) => ; diff --git a/querybook/webapp/components/DataTableTags/CreateDataTableTag.tsx b/querybook/webapp/components/DataTableTags/CreateDataTableTag.tsx index eb8bc77fd..3cbc30e6a 100644 --- a/querybook/webapp/components/DataTableTags/CreateDataTableTag.tsx +++ b/querybook/webapp/components/DataTableTags/CreateDataTableTag.tsx @@ -1,15 +1,6 @@ import * as React from 'react'; -import { useDispatch } from 'react-redux'; - -import { EntitySelect } from 'components/Search/EntitySelect'; import { ITag } from 'const/tag'; -import { isTagValid } from 'lib/utils/tag'; -import { Dispatch } from 'redux/store/types'; -import { createTableTag } from 'redux/tag/action'; -import { TableTagResource } from 'resource/table'; -import { IconButton } from 'ui/Button/IconButton'; - -import './CreateDataTableTag.scss'; +import { CreateTag } from '../GenericTags/CreateTag'; interface IProps { tableId: number; @@ -19,52 +10,4 @@ interface IProps { export const CreateDataTableTag: React.FunctionComponent = ({ tableId, tags, -}) => { - const dispatch: Dispatch = useDispatch(); - const [showSelect, setShowSelect] = React.useState(false); - - const existingTags = React.useMemo( - () => (tags || []).map((tag) => tag.name), - [tags] - ); - - const createTag = React.useCallback( - (tag: string) => dispatch(createTableTag(tableId, tag)), - [tableId, dispatch] - ); - - const handleCreateTag = React.useCallback( - (val: string) => { - createTag(val).finally(() => setShowSelect(false)); - }, - [createTag] - ); - - return ( -
- {showSelect ? ( -
- -
- ) : ( - setShowSelect(true)} - tooltip="Add tag" - tooltipPos="down" - size={18} - invertCircle - /> - )} -
- ); -}; +}) => ; diff --git a/querybook/webapp/components/DataTableTags/TableTagConfigModal.tsx b/querybook/webapp/components/DataTableTags/TableTagConfigModal.tsx index 1ac4804bd..3b0aba559 100644 --- a/querybook/webapp/components/DataTableTags/TableTagConfigModal.tsx +++ b/querybook/webapp/components/DataTableTags/TableTagConfigModal.tsx @@ -1,137 +1,8 @@ -import { Formik } from 'formik'; -import React, { useCallback, useMemo } from 'react'; -import toast from 'react-hot-toast'; -import { useDispatch, useSelector } from 'react-redux'; - -import { ColorPalette } from 'const/chartColors'; -import { ITag, ITagMeta } from 'const/tag'; -import { Dispatch, IStoreState } from 'redux/store/types'; -import { updateTag } from 'redux/tag/action'; -import { AsyncButton } from 'ui/AsyncButton/AsyncButton'; -import { FormWrapper } from 'ui/Form/FormWrapper'; -import { SimpleField } from 'ui/FormikField/SimpleField'; -import { Icon } from 'ui/Icon/Icon'; -import AllLucideIcons from 'ui/Icon/LucideIcons'; -import { Modal } from 'ui/Modal/Modal'; +import React from 'react'; +import { ITag } from 'const/tag'; +import { TagConfigModal } from '../GenericTags/TagConfigModal'; export const TableTagConfigModal: React.FC<{ tag: ITag; onHide: () => void; -}> = ({ tag, onHide }) => { - const isAdmin = useSelector( - (state: IStoreState) => state.user.myUserInfo.isAdmin - ); - - const colorOptions = useMemo( - () => - ColorPalette.map((color) => ({ - value: color.name, - label: color.name, - color: color.color, - })), - [] - ); - - const iconOptions = useMemo( - () => - Object.keys(AllLucideIcons).map((iconName) => ({ - value: iconName, - label: ( - - - {iconName} - - ), - })), - [] - ); - - const initialValues: ITagMeta = useMemo( - () => ({ - rank: 0, - tooltip: undefined, - admin: false, - color: undefined, - icon: undefined, - ...tag.meta, - }), - [tag] - ); - - const dispatch = useDispatch(); - const handleSubmit = useCallback( - async (values: ITagMeta) => { - toast.promise( - dispatch( - updateTag({ - ...tag, - meta: values, - }) - ), - { - success: 'Tag updated!', - loading: 'Updating tag', - error: 'Failed to updated tag', - } - ); - onHide(); - }, - [dispatch, onHide, tag] - ); - - const form = ( - - {({ submitForm }) => ( -
- - - - - - {isAdmin && ( - - )} - - -
- -
-
- )} -
- ); - - return ( - - {form} - - ); -}; +}> = ({ tag, onHide }) => ; diff --git a/querybook/webapp/components/GenericTags/CreateTag.tsx b/querybook/webapp/components/GenericTags/CreateTag.tsx new file mode 100644 index 000000000..559e929a5 --- /dev/null +++ b/querybook/webapp/components/GenericTags/CreateTag.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import { useDispatch } from 'react-redux'; + +import { EntitySelect } from 'components/Search/EntitySelect'; +import { ITag } from 'const/tag'; +import { isTagValid } from 'lib/utils/tag'; +import { Dispatch } from 'redux/store/types'; +import { createDataDocTag } from 'redux/tag/action'; +import { IconButton } from 'ui/Button/IconButton'; +import { DataDocTagResource } from 'resource/dataDoc'; +import { TableTagResource } from 'resource/table'; + +interface IProps { + id: number; + tag_type: string; + tags: ITag[]; +} + +export const CreateTag: React.FunctionComponent = ({ + id, + tag_type, + tags, +}) => { + const dispatch: Dispatch = useDispatch(); + const [showSelect, setShowSelect] = React.useState(false); + + const existingTags = React.useMemo( + () => (tags || []).map((tag) => tag.name), + [tags] + ); + + const createTag = React.useCallback( + (tag: string) => dispatch(createDataDocTag(id, tag)), + [id, dispatch] + ); + + const handleCreateTag = React.useCallback( + (val: string) => { + createTag(val).finally(() => setShowSelect(false)); + }, + [createTag] + ); + + let resource; + if (tag_type === 'DataDoc') { + resource = DataDocTagResource; + } else if (tag_type === 'Table') { + resource = TableTagResource; + } + + return ( +
+ {showSelect ? ( +
+ +
+ ) : ( + setShowSelect(true)} + tooltip="Add tag" + tooltipPos="down" + size={18} + invertCircle + /> + )} +
+ ); +}; diff --git a/querybook/webapp/components/GenericTags/TagConfigModal.tsx b/querybook/webapp/components/GenericTags/TagConfigModal.tsx new file mode 100644 index 000000000..f9265ef1d --- /dev/null +++ b/querybook/webapp/components/GenericTags/TagConfigModal.tsx @@ -0,0 +1,136 @@ +import React, { useCallback, useMemo } from 'react'; +import { ITag, ITagMeta } from '../../const/tag'; +import { useDispatch, useSelector } from 'react-redux'; +import { Dispatch, IStoreState } from '../../redux/store/types'; +import { ColorPalette } from '../../const/chartColors'; +import AllLucideIcons from '../../ui/Icon/LucideIcons'; +import { Icon } from '../../ui/Icon/Icon'; +import toast from 'react-hot-toast'; +import { updateTag } from '../../redux/tag/action'; +import { Formik } from 'formik'; +import { FormWrapper } from '../../ui/Form/FormWrapper'; +import { SimpleField } from '../../ui/FormikField/SimpleField'; +import { AsyncButton } from '../../ui/AsyncButton/AsyncButton'; +import { Modal } from '../../ui/Modal/Modal'; + +export const TagConfigModal: React.FC<{ + tag: ITag; + onHide: () => void; +}> = ({ tag, onHide }) => { + const isAdmin = useSelector( + (state: IStoreState) => state.user.myUserInfo.isAdmin + ); + + const colorOptions = useMemo( + () => + ColorPalette.map((color) => ({ + value: color.name, + label: color.name, + color: color.color, + })), + [] + ); + + const iconOptions = useMemo( + () => + Object.keys(AllLucideIcons).map((iconName) => ({ + value: iconName, + label: ( + + + {iconName} + + ), + })), + [] + ); + + const initialValues: ITagMeta = useMemo( + () => ({ + rank: 0, + tooltip: undefined, + admin: false, + color: undefined, + icon: undefined, + ...tag.meta, + }), + [tag] + ); + + const dispatch = useDispatch(); + const handleSubmit = useCallback( + async (values: ITagMeta) => { + toast.promise( + dispatch( + updateTag({ + ...tag, + meta: values, + }) + ), + { + success: 'Tag updated!', + loading: 'Updating tag', + error: 'Failed to updated tag', + } + ); + onHide(); + }, + [dispatch, onHide, tag] + ); + + const form = ( + + {({ submitForm }) => ( +
+ + + + + + {isAdmin && ( + + )} + + +
+ +
+
+ )} +
+ ); + + return ( + + {form} + + ); +}; From f5ece0622a75b3c1a536a151c8b7a370ece5557b Mon Sep 17 00:00:00 2001 From: jvalvo Date: Tue, 29 Aug 2023 13:44:12 -0700 Subject: [PATCH 10/11] feat: resolved comments --- .../DataDocTags/CreateDataDocTag.tsx | 2 +- .../DataDocTags/DataDocTagGroupSelect.tsx | 47 +--- .../DataDocTags/DataDocTagSelect.tsx | 88 +------- .../components/DataDocTags/DataDocTags.tsx | 159 ++----------- .../DataTableTags/CreateDataTableTag.tsx | 2 +- .../DataTableTags/DataTableTags.tsx | 157 ++----------- .../DataTableTags/TableTagGroupSelect.tsx | 47 +--- .../DataTableTags/TableTagSelect.tsx | 88 +------- .../components/GenericTags/CreateTag.tsx | 8 +- .../components/GenericTags/TagConfigModal.tsx | 20 +- .../components/GenericTags/TagGroupSelect.tsx | 61 +++++ .../components/GenericTags/TagSelect.tsx | 99 ++++++++ .../webapp/components/GenericTags/Tags.tsx | 211 ++++++++++++++++++ 13 files changed, 457 insertions(+), 532 deletions(-) create mode 100644 querybook/webapp/components/GenericTags/TagGroupSelect.tsx create mode 100644 querybook/webapp/components/GenericTags/TagSelect.tsx create mode 100644 querybook/webapp/components/GenericTags/Tags.tsx diff --git a/querybook/webapp/components/DataDocTags/CreateDataDocTag.tsx b/querybook/webapp/components/DataDocTags/CreateDataDocTag.tsx index 7aaecc26e..a85eda301 100644 --- a/querybook/webapp/components/DataDocTags/CreateDataDocTag.tsx +++ b/querybook/webapp/components/DataDocTags/CreateDataDocTag.tsx @@ -10,4 +10,4 @@ interface IProps { export const CreateDataDocTag: React.FunctionComponent = ({ datadocId, tags, -}) => ; +}) => ; diff --git a/querybook/webapp/components/DataDocTags/DataDocTagGroupSelect.tsx b/querybook/webapp/components/DataDocTags/DataDocTagGroupSelect.tsx index 581b0cc97..e5ce16da5 100644 --- a/querybook/webapp/components/DataDocTags/DataDocTagGroupSelect.tsx +++ b/querybook/webapp/components/DataDocTags/DataDocTagGroupSelect.tsx @@ -1,46 +1,9 @@ -import React, { useMemo } from 'react'; - -import { HoverIconTag } from 'ui/Tag/HoverIconTag'; - -import { DataDocTagSelect } from './DataDocTagSelect'; +import React from 'react'; +import { TagGroupSelect } from '../GenericTags/TagGroupSelect'; export const DataDocTagGroupSelect: React.FC<{ tags?: string[]; updateTags: (newTags: string[]) => void; -}> = ({ tags: propsTag, updateTags }) => { - const tags = useMemo(() => propsTag ?? [], [propsTag]); - - const handleTagSelect = React.useCallback( - (tag: string) => { - updateTags([...tags, tag]); - }, - [tags, updateTags] - ); - - const handleTagRemove = React.useCallback( - (tag: string) => { - updateTags(tags.filter((existingTag) => existingTag !== tag)); - }, - [tags, updateTags] - ); - - const tagsListDOM = tags.length ? ( -
- {tags.map((tag) => ( - handleTagRemove(tag)} - /> - ))} -
- ) : null; - - return ( -
- {tagsListDOM} - -
- ); -}; +}> = ({ tags, updateTags }) => ( + +); diff --git a/querybook/webapp/components/DataDocTags/DataDocTagSelect.tsx b/querybook/webapp/components/DataDocTags/DataDocTagSelect.tsx index e52db0f0a..644e960fc 100644 --- a/querybook/webapp/components/DataDocTags/DataDocTagSelect.tsx +++ b/querybook/webapp/components/DataDocTags/DataDocTagSelect.tsx @@ -1,13 +1,5 @@ import * as React from 'react'; - -import { useDebounce } from 'hooks/useDebounce'; -import { useResource } from 'hooks/useResource'; -import { - makeReactSelectStyle, - miniReactSelectStyles, -} from 'lib/utils/react-select'; -import { DataDocTagResource } from 'resource/dataDoc'; -import { SimpleReactSelect } from 'ui/SimpleReactSelect/SimpleReactSelect'; +import { TagSelect } from '../GenericTags/TagSelect'; import './DataDocTagSelect.scss'; @@ -17,77 +9,15 @@ interface IProps { creatable?: boolean; } -const tagReactSelectStyle = makeReactSelectStyle(true, miniReactSelectStyles); - -function isTagValid(val: string, existingTags: string[]) { - const regex = /^[a-z0-9_ ]{1,255}$/i; - const match = val.match(regex); - return Boolean(match && !existingTags.includes(val)); -} - export const DataDocTagSelect: React.FunctionComponent = ({ onSelect, existingTags = [], creatable = false, -}) => { - const [tagString, setTagString] = React.useState(''); - const [isTyping, setIsTyping] = React.useState(false); - const debouncedTagString = useDebounce(tagString, 500); - - const { data: rawTagSuggestions } = useResource( - React.useCallback( - () => DataDocTagResource.search(debouncedTagString), - [debouncedTagString] - ) - ); - - const tagSuggestions = React.useMemo( - () => - (rawTagSuggestions || []).filter( - (str) => !existingTags.includes(str) - ), - [rawTagSuggestions, existingTags] - ); - - const isValid = React.useMemo( - () => - isTyping ? !tagString || isTagValid(tagString, existingTags) : true, - [existingTags, tagString, isTyping] - ); - - const handleSelect = React.useCallback( - (val: string) => { - const tagVal = val ?? tagString; - const valid = isTagValid(tagVal, existingTags); - if (valid) { - setTagString(''); - onSelect(tagVal); - } - }, - [tagString, onSelect, existingTags] - ); - - return ( -
- handleSelect(val)} - selectProps={{ - onInputChange: (newValue) => setTagString(newValue), - placeholder: 'alphanumeric only', - styles: tagReactSelectStyle, - onFocus: () => setIsTyping(true), - onBlur: () => setIsTyping(false), - noOptionsMessage: () => null, - }} - clearAfterSelect - /> -
- ); -}; +}) => ( + +); diff --git a/querybook/webapp/components/DataDocTags/DataDocTags.tsx b/querybook/webapp/components/DataDocTags/DataDocTags.tsx index 9e3bc53c1..45f279881 100644 --- a/querybook/webapp/components/DataDocTags/DataDocTags.tsx +++ b/querybook/webapp/components/DataDocTags/DataDocTags.tsx @@ -1,24 +1,6 @@ -import qs from 'qs'; import * as React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - import { ITag } from 'const/tag'; -import { stopPropagationAndDefault } from 'lib/utils/noop'; -import { navigateWithinEnv } from 'lib/utils/query-string'; -import { useRankedTags } from 'lib/utils/tag'; -import { Dispatch, IStoreState } from 'redux/store/types'; -import { - deleteDataDocTag, - fetchDataDocTagsFromTableIfNeeded, -} from 'redux/tag/action'; -import { tagsInDataDocSelector } from 'redux/tag/selector'; -import { ContextMenu } from 'ui/ContextMenu/ContextMenu'; -import { Icon } from 'ui/Icon/Icon'; -import { Menu, MenuItem } from 'ui/Menu/Menu'; -import { HoverIconTag } from 'ui/Tag/HoverIconTag'; - -import { CreateDataDocTag } from './CreateDataDocTag'; -import { DataDocTagConfigModal } from './DataDocTagConfigModal'; +import { GenericTag, GenericTags } from '../GenericTags/Tags'; import './DataDocTags.scss'; @@ -34,51 +16,15 @@ export const DataDocTags: React.FunctionComponent = ({ readonly = false, mini = false, showType = true, -}) => { - const isUserAdmin = useSelector( - (state: IStoreState) => state.user.myUserInfo.isAdmin - ); - const dispatch: Dispatch = useDispatch(); - const loadTags = React.useCallback( - () => dispatch(fetchDataDocTagsFromTableIfNeeded(datadocId)), - [datadocId] - ); - const deleteTag = React.useCallback( - (tagName: string) => dispatch(deleteDataDocTag(datadocId, tagName)), - [datadocId] - ); - - const tags = useRankedTags( - useSelector((state: IStoreState) => - tagsInDataDocSelector(state, datadocId) - ) - ); - - React.useEffect(() => { - loadTags(); - }, []); - - const listDOM = (tags || []).map((tag) => ( - - )); - - return ( -
- {listDOM} - {readonly ? null : ( - - )} -
- ); -}; +}) => ( + +); export const DataDocTag: React.FC<{ tag: ITag; @@ -88,77 +34,14 @@ export const DataDocTag: React.FC<{ deleteTag?: (tagName: string) => void; mini?: boolean; showType?: boolean; -}> = ({ tag, readonly, deleteTag, isUserAdmin, mini, showType = true }) => { - const tagMeta = tag.meta ?? {}; - const tagRef = React.useRef(); - const [showConfigModal, setShowConfigModal] = React.useState(false); - - const handleDeleteTag = React.useCallback( - (e: React.MouseEvent) => { - stopPropagationAndDefault(e); - deleteTag(tag.name); - }, - [deleteTag, tag.name] - ); - const handleTagClick = React.useCallback(() => { - navigateWithinEnv( - `/search/?${qs.stringify({ - searchType: 'DataDoc', - 'searchFilters[tags][0]': tag.name, - })}`, - { - isModal: true, - } - ); - }, [tag.name]); - - // user can update if it is not readonly and passes the admin check - const canUserUpdate = !(readonly || (tagMeta.admin && !isUserAdmin)); - const canUserDelete = !readonly && canUserUpdate; - - const renderContextMenu = () => ( - - setShowConfigModal(true)}> - - Configure Tag - - - - Remove Tag - - - ); - - return ( -
- {canUserUpdate && ( - - )} - - {showConfigModal && ( - setShowConfigModal(false)} - /> - )} - -
- ); -}; +}> = ({ tag, readonly, deleteTag, isUserAdmin, mini, showType = true }) => ( + +); diff --git a/querybook/webapp/components/DataTableTags/CreateDataTableTag.tsx b/querybook/webapp/components/DataTableTags/CreateDataTableTag.tsx index 3cbc30e6a..449df133a 100644 --- a/querybook/webapp/components/DataTableTags/CreateDataTableTag.tsx +++ b/querybook/webapp/components/DataTableTags/CreateDataTableTag.tsx @@ -10,4 +10,4 @@ interface IProps { export const CreateDataTableTag: React.FunctionComponent = ({ tableId, tags, -}) => ; +}) => ; diff --git a/querybook/webapp/components/DataTableTags/DataTableTags.tsx b/querybook/webapp/components/DataTableTags/DataTableTags.tsx index 172d02138..193e813f8 100644 --- a/querybook/webapp/components/DataTableTags/DataTableTags.tsx +++ b/querybook/webapp/components/DataTableTags/DataTableTags.tsx @@ -1,24 +1,6 @@ -import qs from 'qs'; import * as React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - import { ITag } from 'const/tag'; -import { stopPropagationAndDefault } from 'lib/utils/noop'; -import { navigateWithinEnv } from 'lib/utils/query-string'; -import { useRankedTags } from 'lib/utils/tag'; -import { Dispatch, IStoreState } from 'redux/store/types'; -import { - deleteTableTag, - fetchTableTagsFromTableIfNeeded, -} from 'redux/tag/action'; -import { tagsInTableSelector } from 'redux/tag/selector'; -import { ContextMenu } from 'ui/ContextMenu/ContextMenu'; -import { Icon } from 'ui/Icon/Icon'; -import { Menu, MenuItem } from 'ui/Menu/Menu'; -import { HoverIconTag } from 'ui/Tag/HoverIconTag'; - -import { CreateDataTableTag } from './CreateDataTableTag'; -import { TableTagConfigModal } from './TableTagConfigModal'; +import { GenericTag, GenericTags } from '../GenericTags/Tags'; import './DataTableTags.scss'; @@ -34,49 +16,15 @@ export const DataTableTags: React.FunctionComponent = ({ readonly = false, mini = false, showType = true, -}) => { - const isUserAdmin = useSelector( - (state: IStoreState) => state.user.myUserInfo.isAdmin - ); - const dispatch: Dispatch = useDispatch(); - const loadTags = React.useCallback( - () => dispatch(fetchTableTagsFromTableIfNeeded(tableId)), - [tableId] - ); - const deleteTag = React.useCallback( - (tagName: string) => dispatch(deleteTableTag(tableId, tagName)), - [tableId] - ); - - const tags = useRankedTags( - useSelector((state: IStoreState) => tagsInTableSelector(state, tableId)) - ); - - React.useEffect(() => { - loadTags(); - }, []); - - const listDOM = (tags || []).map((tag) => ( - - )); - - return ( -
- {listDOM} - {readonly ? null : ( - - )} -
- ); -}; +}) => ( + +); export const TableTag: React.FC<{ tag: ITag; @@ -86,77 +34,14 @@ export const TableTag: React.FC<{ deleteTag?: (tagName: string) => void; mini?: boolean; showType?: boolean; -}> = ({ tag, readonly, deleteTag, isUserAdmin, mini, showType = true }) => { - const tagMeta = tag.meta ?? {}; - const tagRef = React.useRef(); - const [showConfigModal, setShowConfigModal] = React.useState(false); - - const handleDeleteTag = React.useCallback( - (e: React.MouseEvent) => { - stopPropagationAndDefault(e); - deleteTag(tag.name); - }, - [deleteTag, tag.name] - ); - const handleTagClick = React.useCallback(() => { - navigateWithinEnv( - `/search/?${qs.stringify({ - searchType: 'Table', - 'searchFilters[tags][0]': tag.name, - })}`, - { - isModal: true, - } - ); - }, [tag.name]); - - // user can update if it is not readonly and passes the admin check - const canUserUpdate = !(readonly || (tagMeta.admin && !isUserAdmin)); - const canUserDelete = !readonly && canUserUpdate; - - const renderContextMenu = () => ( - - setShowConfigModal(true)}> - - Configure Tag - - - - Remove Tag - - - ); - - return ( -
- {canUserUpdate && ( - - )} - - {showConfigModal && ( - setShowConfigModal(false)} - /> - )} - -
- ); -}; +}> = ({ tag, readonly, deleteTag, isUserAdmin, mini, showType = true }) => ( + +); diff --git a/querybook/webapp/components/DataTableTags/TableTagGroupSelect.tsx b/querybook/webapp/components/DataTableTags/TableTagGroupSelect.tsx index f7ec6e3c5..90a166879 100644 --- a/querybook/webapp/components/DataTableTags/TableTagGroupSelect.tsx +++ b/querybook/webapp/components/DataTableTags/TableTagGroupSelect.tsx @@ -1,46 +1,9 @@ -import React, { useMemo } from 'react'; - -import { HoverIconTag } from 'ui/Tag/HoverIconTag'; - -import { TableTagSelect } from './TableTagSelect'; +import React from 'react'; +import { TagGroupSelect } from '../GenericTags/TagGroupSelect'; export const TableTagGroupSelect: React.FC<{ tags?: string[]; updateTags: (newTags: string[]) => void; -}> = ({ tags: propsTag, updateTags }) => { - const tags = useMemo(() => propsTag ?? [], [propsTag]); - - const handleTagSelect = React.useCallback( - (tag: string) => { - updateTags([...tags, tag]); - }, - [tags, updateTags] - ); - - const handleTagRemove = React.useCallback( - (tag: string) => { - updateTags(tags.filter((existingTag) => existingTag !== tag)); - }, - [tags, updateTags] - ); - - const tagsListDOM = tags.length ? ( -
- {tags.map((tag) => ( - handleTagRemove(tag)} - /> - ))} -
- ) : null; - - return ( -
- {tagsListDOM} - -
- ); -}; +}> = ({ tags, updateTags }) => ( + +); diff --git a/querybook/webapp/components/DataTableTags/TableTagSelect.tsx b/querybook/webapp/components/DataTableTags/TableTagSelect.tsx index d3185cafd..42d48b67b 100644 --- a/querybook/webapp/components/DataTableTags/TableTagSelect.tsx +++ b/querybook/webapp/components/DataTableTags/TableTagSelect.tsx @@ -1,13 +1,5 @@ import * as React from 'react'; - -import { useDebounce } from 'hooks/useDebounce'; -import { useResource } from 'hooks/useResource'; -import { - makeReactSelectStyle, - miniReactSelectStyles, -} from 'lib/utils/react-select'; -import { TableTagResource } from 'resource/table'; -import { SimpleReactSelect } from 'ui/SimpleReactSelect/SimpleReactSelect'; +import { TagSelect } from '../GenericTags/TagSelect'; import './TableTagSelect.scss'; @@ -17,77 +9,15 @@ interface IProps { creatable?: boolean; } -const tagReactSelectStyle = makeReactSelectStyle(true, miniReactSelectStyles); - -function isTagValid(val: string, existingTags: string[]) { - const regex = /^[a-z0-9_ ]{1,255}$/i; - const match = val.match(regex); - return Boolean(match && !existingTags.includes(val)); -} - export const TableTagSelect: React.FunctionComponent = ({ onSelect, existingTags = [], creatable = false, -}) => { - const [tagString, setTagString] = React.useState(''); - const [isTyping, setIsTyping] = React.useState(false); - const debouncedTagString = useDebounce(tagString, 500); - - const { data: rawTagSuggestions } = useResource( - React.useCallback( - () => TableTagResource.search(debouncedTagString), - [debouncedTagString] - ) - ); - - const tagSuggestions = React.useMemo( - () => - (rawTagSuggestions || []).filter( - (str) => !existingTags.includes(str) - ), - [rawTagSuggestions, existingTags] - ); - - const isValid = React.useMemo( - () => - isTyping ? !tagString || isTagValid(tagString, existingTags) : true, - [existingTags, tagString, isTyping] - ); - - const handleSelect = React.useCallback( - (val: string) => { - const tagVal = val ?? tagString; - const valid = isTagValid(tagVal, existingTags); - if (valid) { - setTagString(''); - onSelect(tagVal); - } - }, - [tagString, onSelect, existingTags] - ); - - return ( -
- handleSelect(val)} - selectProps={{ - onInputChange: (newValue) => setTagString(newValue), - placeholder: 'alphanumeric only', - styles: tagReactSelectStyle, - onFocus: () => setIsTyping(true), - onBlur: () => setIsTyping(false), - noOptionsMessage: () => null, - }} - clearAfterSelect - /> -
- ); -}; +}) => ( + +); diff --git a/querybook/webapp/components/GenericTags/CreateTag.tsx b/querybook/webapp/components/GenericTags/CreateTag.tsx index 559e929a5..be4463ae0 100644 --- a/querybook/webapp/components/GenericTags/CreateTag.tsx +++ b/querybook/webapp/components/GenericTags/CreateTag.tsx @@ -12,13 +12,13 @@ import { TableTagResource } from 'resource/table'; interface IProps { id: number; - tag_type: string; + tagType: string; tags: ITag[]; } export const CreateTag: React.FunctionComponent = ({ id, - tag_type, + tagType, tags, }) => { const dispatch: Dispatch = useDispatch(); @@ -42,9 +42,9 @@ export const CreateTag: React.FunctionComponent = ({ ); let resource; - if (tag_type === 'DataDoc') { + if (tagType === 'DataDoc') { resource = DataDocTagResource; - } else if (tag_type === 'Table') { + } else if (tagType === 'Table') { resource = TableTagResource; } diff --git a/querybook/webapp/components/GenericTags/TagConfigModal.tsx b/querybook/webapp/components/GenericTags/TagConfigModal.tsx index f9265ef1d..115c62b57 100644 --- a/querybook/webapp/components/GenericTags/TagConfigModal.tsx +++ b/querybook/webapp/components/GenericTags/TagConfigModal.tsx @@ -1,17 +1,17 @@ import React, { useCallback, useMemo } from 'react'; -import { ITag, ITagMeta } from '../../const/tag'; +import { ITag, ITagMeta } from 'const/tag'; import { useDispatch, useSelector } from 'react-redux'; -import { Dispatch, IStoreState } from '../../redux/store/types'; -import { ColorPalette } from '../../const/chartColors'; -import AllLucideIcons from '../../ui/Icon/LucideIcons'; -import { Icon } from '../../ui/Icon/Icon'; +import { Dispatch, IStoreState } from 'redux/store/types'; +import { ColorPalette } from 'const/chartColors'; +import AllLucideIcons from 'ui/Icon/LucideIcons'; +import { Icon } from 'ui/Icon/Icon'; import toast from 'react-hot-toast'; -import { updateTag } from '../../redux/tag/action'; +import { updateTag } from 'redux/tag/action'; import { Formik } from 'formik'; -import { FormWrapper } from '../../ui/Form/FormWrapper'; -import { SimpleField } from '../../ui/FormikField/SimpleField'; -import { AsyncButton } from '../../ui/AsyncButton/AsyncButton'; -import { Modal } from '../../ui/Modal/Modal'; +import { FormWrapper } from 'ui/Form/FormWrapper'; +import { SimpleField } from 'ui/FormikField/SimpleField'; +import { AsyncButton } from 'ui/AsyncButton/AsyncButton'; +import { Modal } from 'ui/Modal/Modal'; export const TagConfigModal: React.FC<{ tag: ITag; diff --git a/querybook/webapp/components/GenericTags/TagGroupSelect.tsx b/querybook/webapp/components/GenericTags/TagGroupSelect.tsx new file mode 100644 index 000000000..fff56b824 --- /dev/null +++ b/querybook/webapp/components/GenericTags/TagGroupSelect.tsx @@ -0,0 +1,61 @@ +import React, { useMemo } from 'react'; +import { HoverIconTag } from 'ui/Tag/HoverIconTag'; +import { DataDocTagSelect } from '../DataDocTags/DataDocTagSelect'; +import { TableTagSelect } from '../DataTableTags/TableTagSelect'; + +export const TagGroupSelect: React.FC<{ + tags?: string[]; + tagType: string; + updateTags: (newTags: string[]) => void; +}> = ({ tags: propsTag, tagType, updateTags }) => { + const tags = useMemo(() => propsTag ?? [], [propsTag]); + + const handleTagSelect = React.useCallback( + (tag: string) => { + updateTags([...tags, tag]); + }, + [tags, updateTags] + ); + + const handleTagRemove = React.useCallback( + (tag: string) => { + updateTags(tags.filter((existingTag) => existingTag !== tag)); + }, + [tags, updateTags] + ); + + const tagsListDOM = tags.length ? ( +
+ {tags.map((tag) => ( + handleTagRemove(tag)} + /> + ))} +
+ ) : null; + + if (tagType === 'DataDoc') { + return ( +
+ {tagsListDOM} + +
+ ); + } else if (tagType === 'Table') { + return ( +
+ {tagsListDOM} + +
+ ); + } +}; diff --git a/querybook/webapp/components/GenericTags/TagSelect.tsx b/querybook/webapp/components/GenericTags/TagSelect.tsx new file mode 100644 index 000000000..e5a43f859 --- /dev/null +++ b/querybook/webapp/components/GenericTags/TagSelect.tsx @@ -0,0 +1,99 @@ +import { + makeReactSelectStyle, + miniReactSelectStyles, +} from '../../lib/utils/react-select'; +import * as React from 'react'; +import { useDebounce } from '../../hooks/useDebounce'; +import { useResource } from '../../hooks/useResource'; +import { DataDocTagResource } from '../../resource/dataDoc'; +import { SimpleReactSelect } from '../../ui/SimpleReactSelect/SimpleReactSelect'; +import { TableTagResource } from '../../resource/table'; + +interface IProps { + onSelect: (val: string) => any; + existingTags?: string[]; + creatable?: boolean; + tagType: string; +} + +const tagReactSelectStyle = makeReactSelectStyle(true, miniReactSelectStyles); + +function isTagValid(val: string, existingTags: string[]) { + const regex = /^[a-z0-9_ ]{1,255}$/i; + const match = val.match(regex); + return Boolean(match && !existingTags.includes(val)); +} + +export const TagSelect: React.FunctionComponent = ({ + onSelect, + existingTags = [], + creatable = false, + tagType, +}) => { + const [tagString, setTagString] = React.useState(''); + const [isTyping, setIsTyping] = React.useState(false); + const debouncedTagString = useDebounce(tagString, 500); + + let resource; + let className; + if (tagType === 'DataDoc') { + resource = DataDocTagResource; + className = 'DataDocTagSelect'; + } else if (tagType === 'Table') { + resource = TableTagResource; + className = 'TableTagSelect'; + } + + const { data: rawTagSuggestions } = useResource( + React.useCallback( + () => resource.search(debouncedTagString), + [debouncedTagString] + ) + ); + + const tagSuggestions = React.useMemo( + () => + ((rawTagSuggestions as string[]) || []).filter( + (str) => !existingTags.includes(str) + ), + [rawTagSuggestions, existingTags] + ); + + const isValid = React.useMemo( + () => + isTyping ? !tagString || isTagValid(tagString, existingTags) : true, + [existingTags, tagString, isTyping] + ); + + const handleSelect = React.useCallback( + (val: string) => { + const tagVal = val ?? tagString; + const valid = isTagValid(tagVal, existingTags); + if (valid) { + setTagString(''); + onSelect(tagVal); + } + }, + [tagString, onSelect, existingTags] + ); + + return ( +
+ handleSelect(val)} + selectProps={{ + onInputChange: (newValue) => setTagString(newValue), + placeholder: 'alphanumeric only', + styles: tagReactSelectStyle, + onFocus: () => setIsTyping(true), + onBlur: () => setIsTyping(false), + noOptionsMessage: () => null, + }} + clearAfterSelect + /> +
+ ); +}; diff --git a/querybook/webapp/components/GenericTags/Tags.tsx b/querybook/webapp/components/GenericTags/Tags.tsx new file mode 100644 index 000000000..3cb3e6224 --- /dev/null +++ b/querybook/webapp/components/GenericTags/Tags.tsx @@ -0,0 +1,211 @@ +import * as React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Dispatch, IStoreState } from 'redux/store/types'; +import { + deleteDataDocTag, + deleteTableTag, + fetchDataDocTagsFromTableIfNeeded, + fetchTableTagsFromTableIfNeeded, +} from 'redux/tag/action'; +import { useRankedTags } from '../../lib/utils/tag'; +import { tagsInDataDocSelector, tagsInTableSelector } from 'redux/tag/selector'; +import { CreateDataDocTag } from '../DataDocTags/CreateDataDocTag'; +import { DataDocTag } from '../DataDocTags/DataDocTags'; +import { CreateDataTableTag } from '../DataTableTags/CreateDataTableTag'; +import { ITag } from 'const/tag'; +import { stopPropagationAndDefault } from 'lib/utils/noop'; +import { navigateWithinEnv } from 'lib/utils/query-string'; +import qs from 'qs'; +import { Menu, MenuItem } from 'ui/Menu/Menu'; +import { Icon } from 'ui/Icon/Icon'; +import { ContextMenu } from 'ui/ContextMenu/ContextMenu'; +import { DataDocTagConfigModal } from '../DataDocTags/DataDocTagConfigModal'; +import { HoverIconTag } from 'ui/Tag/HoverIconTag'; +import { TableTagConfigModal } from '../DataTableTags/TableTagConfigModal'; + +interface IProps { + id: number; + readonly?: boolean; + mini?: boolean; + showType?: boolean; + tagType: string; +} + +export const GenericTags: React.FunctionComponent = ({ + id, + readonly = false, + mini = false, + showType = true, + tagType, +}) => { + const isUserAdmin = useSelector( + (state: IStoreState) => state.user.myUserInfo.isAdmin + ); + const dispatch: Dispatch = useDispatch(); + + let fetchFunc; + let deleteFunc; + let selector; + if (tagType === 'DataDoc') { + fetchFunc = fetchDataDocTagsFromTableIfNeeded; + deleteFunc = deleteDataDocTag; + selector = tagsInDataDocSelector; + } else if (tagType === 'Table') { + fetchFunc = fetchTableTagsFromTableIfNeeded; + deleteFunc = deleteTableTag; + selector = tagsInTableSelector; + } + + const loadTags = React.useCallback(() => dispatch(fetchFunc(id)), [id]); + const deleteTag = React.useCallback( + (tagName: string) => dispatch(deleteFunc(id, tagName)), + [id] + ); + + const tags = useRankedTags( + useSelector((state: IStoreState) => selector(state, id)) + ); + + React.useEffect(() => { + loadTags(); + }, []); + + const listDOM = (tags || []).map((tag) => ( + + )); + + if (tagType === 'DataDoc') { + return ( +
+ {listDOM} + {readonly ? null : ( + + )} +
+ ); + } else if (tagType === 'Table') { + return ( +
+ {listDOM} + {readonly ? null : ( + + )} +
+ ); + } +}; + +export const GenericTag: React.FC<{ + tag: ITag; + + isUserAdmin?: boolean; + readonly?: boolean; + deleteTag?: (tagName: string) => void; + mini?: boolean; + showType?: boolean; + tagType: string; +}> = ({ + tag, + readonly, + deleteTag, + isUserAdmin, + mini, + showType = true, + tagType, +}) => { + const tagMeta = tag.meta ?? {}; + const tagRef = React.useRef(); + const [showConfigModal, setShowConfigModal] = React.useState(false); + + const handleDeleteTag = React.useCallback( + (e: React.MouseEvent) => { + stopPropagationAndDefault(e); + deleteTag(tag.name); + }, + [deleteTag, tag.name] + ); + const handleTagClick = React.useCallback(() => { + navigateWithinEnv( + `/search/?${qs.stringify({ + searchType: tagType, + 'searchFilters[tags][0]': tag.name, + })}`, + { + isModal: true, + } + ); + }, [tag.name]); + + // user can update if it is not readonly and passes the admin check + const canUserUpdate = !(readonly || (tagMeta.admin && !isUserAdmin)); + const canUserDelete = !readonly && canUserUpdate; + + const renderContextMenu = () => ( + + setShowConfigModal(true)}> + + Configure Tag + + + + Remove Tag + + + ); + + let configModal; + let className; + if (tagType === 'DataDoc') { + className = 'DataDocTag'; + configModal = ( + setShowConfigModal(false)} + /> + ); + } else if (tagType === 'Table') { + className = 'TableTag'; + configModal = ( + setShowConfigModal(false)} + /> + ); + } + + return ( +
+ {canUserUpdate && ( + + )} + + {showConfigModal && configModal} + +
+ ); +}; From 992bb0711765f12dbf40af757afa1d51dde9e6c5 Mon Sep 17 00:00:00 2001 From: jvalvo Date: Tue, 29 Aug 2023 14:21:50 -0700 Subject: [PATCH 11/11] feat: fixed some lint errors --- querybook/webapp/components/DataDoc/DataDoc.tsx | 1 - querybook/webapp/redux/tag/selector.ts | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/querybook/webapp/components/DataDoc/DataDoc.tsx b/querybook/webapp/components/DataDoc/DataDoc.tsx index 9e93224b7..39714e024 100644 --- a/querybook/webapp/components/DataDoc/DataDoc.tsx +++ b/querybook/webapp/components/DataDoc/DataDoc.tsx @@ -752,7 +752,6 @@ class DataDocComponent extends React.PureComponent { className="header-subtitle mr20" weight="bold" color="lightest" - > Tags diff --git a/querybook/webapp/redux/tag/selector.ts b/querybook/webapp/redux/tag/selector.ts index 3748a2642..b23e9402a 100644 --- a/querybook/webapp/redux/tag/selector.ts +++ b/querybook/webapp/redux/tag/selector.ts @@ -14,7 +14,8 @@ export const tagsInTableSelector = createSelector( const datadocTagNameSelector = (state: IStoreState, datadocId: number) => state.tag.datadocIdToTagName[datadocId] || []; -const datadocTagByNameSelector = (state: IStoreState) => + +const datadocTagByNameSelector = (state: IStoreState) => state.tag.datadocTagByName; export const tagsInDataDocSelector = createSelector(