diff --git a/datahub-web-react/src/app/entityV2/view/ManageViews.tsx b/datahub-web-react/src/app/entityV2/view/ManageViews.tsx index 1633a8ddb8af52..34a61633792d94 100644 --- a/datahub-web-react/src/app/entityV2/view/ManageViews.tsx +++ b/datahub-web-react/src/app/entityV2/view/ManageViews.tsx @@ -1,51 +1,140 @@ -import { Typography } from 'antd'; -import React from 'react'; +import { Button, PageTitle, Tabs, colors } from '@components'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useLocation } from 'react-router'; import styled from 'styled-components'; +import { Tab } from '@components/components/Tabs/Tabs'; + +import { ViewBuilder } from '@app/entity/view/builder/ViewBuilder'; +import { ViewBuilderMode } from '@app/entity/view/builder/types'; import { ViewsList } from '@app/entityV2/view/ViewsList'; +import { DataHubViewType } from '@types'; + const PageContainer = styled.div` - padding-top: 20px; + padding: 16px 20px; width: 100%; + overflow: hidden; + flex: 1; + gap: 20px; display: flex; flex-direction: column; - overflow: auto; `; const PageHeaderContainer = styled.div` && { - padding-left: 24px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; } `; -const PageTitle = styled(Typography.Title)` - && { - margin-bottom: 12px; - } +const TitleContainer = styled.div` + flex: 1; +`; + +const HeaderActionsContainer = styled.div` + display: flex; + justify-content: flex-end; `; const ListContainer = styled.div` + flex: 1; display: flex; flex-direction: column; + &&& .ant-tabs-nav { + margin: 0; + } + color: ${colors.gray[600]}; overflow: auto; `; +enum TabType { + Personal = 'My Views', + Global = 'Public Views', +} + +const tabUrlMap = { + [TabType.Personal]: '/settings/views/personal', + [TabType.Global]: '/settings/views/public', +}; + /** * Component used for displaying the 'Manage Views' experience. */ export const ManageViews = () => { + const location = useLocation(); + const [showViewBuilder, setShowViewBuilder] = useState(false); + const [selectedTab, setSelectedTab] = useState(); + + const onCloseModal = () => { + setShowViewBuilder(false); + }; + + const tabs: Tab[] = [ + { + component: , + key: TabType.Personal, + name: TabType.Personal, + }, + { + component: , + key: TabType.Global, + name: TabType.Global, + }, + ]; + + useEffect(() => { + if (selectedTab === undefined) { + const currentPath = location.pathname; + + const currentTab = Object.entries(tabUrlMap).find(([, url]) => url === currentPath)?.[0] as TabType; + if (currentTab) { + setSelectedTab(currentTab); + } else { + setSelectedTab(null); + } + } + }, [selectedTab, location.pathname]); + + const getCurrentUrl = useCallback(() => location.pathname, [location.pathname]); + return ( - Manage Views - - Create, edit, and remove your Views. Views allow you to save and share sets of filters for reuse - when browsing DataHub. - + + + + + + - + setSelectedTab(tab as TabType)} + urlMap={tabUrlMap} + defaultTab={TabType.Personal} + getCurrentUrl={getCurrentUrl} + /> + {showViewBuilder && ( + + )} ); }; diff --git a/datahub-web-react/src/app/entityV2/view/ViewTypeLabel.tsx b/datahub-web-react/src/app/entityV2/view/ViewTypeLabel.tsx index bb3d34ca8da8ea..706090de66933c 100644 --- a/datahub-web-react/src/app/entityV2/view/ViewTypeLabel.tsx +++ b/datahub-web-react/src/app/entityV2/view/ViewTypeLabel.tsx @@ -1,20 +1,10 @@ -import { GlobalOutlined, LockOutlined } from '@ant-design/icons'; +import { Icon, Tooltip } from '@components'; import { Typography } from 'antd'; import React from 'react'; import styled from 'styled-components'; import { DataHubViewType } from '@types'; -const StyledLockOutlined = styled(LockOutlined)<{ color }>` - color: ${(props) => props.color}; - margin-right: 4px; -`; - -const StyledGlobalOutlined = styled(GlobalOutlined)<{ color }>` - color: ${(props) => props.color}; - margin-right: 4px; -`; - const StyledText = styled(Typography.Text)<{ color }>` && { color: ${(props) => props.color}; @@ -27,31 +17,29 @@ type Props = { onClick?: () => void; }; +const ViewNameContainer = styled.div` + display: flex; + align-items: center; + gap: 4px; +`; + /** * Label used to describe View Types * * @param param0 the color of the text and iconography */ export const ViewTypeLabel = ({ type, color, onClick }: Props) => { - const copy = - type === DataHubViewType.Personal ? ( - <> - Private - only visible to you. - - ) : ( - <> - Public - visible to everyone. - - ); - const Icon = type === DataHubViewType.Global ? StyledGlobalOutlined : StyledLockOutlined; - + const isPersonal = type === DataHubViewType.Personal; return ( // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions -
- - - {copy} - -
+ + + {!isPersonal && } + {isPersonal && } + + {!isPersonal ? 'Public' : 'Private'} + + + ); }; diff --git a/datahub-web-react/src/app/entityV2/view/ViewsList.tsx b/datahub-web-react/src/app/entityV2/view/ViewsList.tsx index 1d02cb9cf38a10..a4d14604efeb6c 100644 --- a/datahub-web-react/src/app/entityV2/view/ViewsList.tsx +++ b/datahub-web-react/src/app/entityV2/view/ViewsList.tsx @@ -1,21 +1,15 @@ -import { PlusOutlined } from '@ant-design/icons'; -import { Button, Pagination, message } from 'antd'; -import * as QueryString from 'query-string'; -import React, { useEffect, useState } from 'react'; -import { useLocation } from 'react-router'; +import { SearchBar, Text } from '@components'; +import { Pagination, message } from 'antd'; +import React, { useState } from 'react'; import styled from 'styled-components'; -import TabToolbar from '@app/entityV2/shared/components/styled/TabToolbar'; import { ViewsTable } from '@app/entityV2/view/ViewsTable'; -import { ViewBuilder } from '@app/entityV2/view/builder/ViewBuilder'; -import { ViewBuilderMode } from '@app/entityV2/view/builder/types'; import { DEFAULT_LIST_VIEWS_PAGE_SIZE, searchViews } from '@app/entityV2/view/utils'; -import { SearchBar } from '@app/search/SearchBar'; import { Message } from '@app/shared/Message'; import { scrollToTop } from '@app/shared/searchUtils'; -import { useEntityRegistry } from '@app/useEntityRegistry'; -import { useListMyViewsQuery } from '@graphql/view.generated'; +import { useListGlobalViewsQuery, useListMyViewsQuery } from '@graphql/view.generated'; +import { DataHubViewType } from '@types'; const PaginationContainer = styled.div` display: flex; @@ -26,67 +20,119 @@ const StyledPagination = styled(Pagination)` margin: 40px; `; -const searchBarStyle = { - maxWidth: 220, - padding: 0, +type Props = { + viewType?: DataHubViewType; }; -const searchBarInputStyle = { - height: 32, - fontSize: 12, -}; +const StyledTabToolbar = styled.div` + display: flex; + justify-content: space-between; + padding: 1px 0 16px 0; // 1px at the top to prevent Select's border outline from cutting-off + height: auto; + z-index: unset; + box-shadow: none; + flex-shrink: 0; +`; + +export const EmptyContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + gap: 16px; + + svg { + width: 160px; + height: 160px; + } +`; + +const TableContainer = styled.div` + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + max-height: calc(100vh - 330px); /* Constrain to page height minus header/filters space */ + overflow: auto; + + /* Make table header sticky */ + .ant-table-thead { + position: sticky; + top: 0; + z-index: 1; + background: white; + } + + /* Ensure header cells have proper background */ + .ant-table-thead > tr > th { + background: white !important; + border-bottom: 1px solid #f0f0f0; + } +`; + +const SearchContainer = styled.div` + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; +`; + +const ViewsContainer = styled.div` + display: flex; + flex-direction: column; + overflow: auto; + padding-top: 7px; +`; + +const StyledSearchBar = styled(SearchBar)` + width: 300px; +`; /** * This component renders a paginated, searchable list of Views. */ -export const ViewsList = () => { - /** - * Context - */ - const location = useLocation(); - const entityRegistry = useEntityRegistry(); - - /** - * Query Params - */ - const params = QueryString.parse(location.search, { arrayFormat: 'comma' }); - const paramsQuery = (params?.query as string) || undefined; - +export const ViewsList = ({ viewType = DataHubViewType.Personal }: Props) => { /** * State */ const [page, setPage] = useState(1); - const [selectedViewUrn, setSelectedViewUrn] = useState(undefined); - const [showViewBuilder, setShowViewBuilder] = useState(false); const [query, setQuery] = useState(undefined); - useEffect(() => setQuery(paramsQuery), [paramsQuery]); /** * Queries */ const pageSize = DEFAULT_LIST_VIEWS_PAGE_SIZE; const start = (page - 1) * pageSize; - const { loading, error, data } = useListMyViewsQuery({ + + const isPersonal = viewType === DataHubViewType.Personal; + + const { + loading: loadingPersonal, + error: errorPersonal, + data: dataPersonal, + } = useListMyViewsQuery({ variables: { start, count: pageSize, }, fetchPolicy: 'cache-first', + skip: !isPersonal, }); - const onClickCreateView = () => { - setShowViewBuilder(true); - }; - - const onClickEditView = (urn: string) => { - setShowViewBuilder(true); - setSelectedViewUrn(urn); - }; - - const onCloseModal = () => { - setShowViewBuilder(false); - setSelectedViewUrn(undefined); - }; + const { + loading: loadingGlobal, + error: errorGlobal, + data: dataGlobal, + } = useListGlobalViewsQuery({ + variables: { + start, + count: pageSize, + }, + fetchPolicy: 'cache-first', + skip: isPersonal, + }); const onChangePage = (newPage: number) => { scrollToTop(); @@ -96,51 +142,48 @@ export const ViewsList = () => { /** * Render variables. */ - const totalViews = data?.listMyViews?.total || 0; - const views = searchViews(data?.listMyViews?.views || [], query); - const selectedView = (selectedViewUrn && views.find((view) => view.urn === selectedViewUrn)) || undefined; + const viewsData = isPersonal ? dataPersonal?.listMyViews : dataGlobal?.listGlobalViews; + const loading = loadingPersonal || loadingGlobal; + const error = errorPersonal || errorGlobal; + const totalViews = viewsData?.total || 0; + const views = searchViews(viewsData?.views || [], query); + + if (!totalViews) { + return ( + + + No Views yet! + + + ); + } return ( <> - {!data && loading && } + {!viewsData && loading && } {error && message.error({ content: `Failed to load Views! An unexpected error occurred.`, duration: 3 })} - - - null} - onQueryChange={(q) => setQuery(q.length > 0 ? q : undefined)} - entityRegistry={entityRegistry} - /> - - - {totalViews >= pageSize && ( - - - - )} - {showViewBuilder && ( - - )} + + + + + + + + + + {totalViews >= pageSize && ( + + + + )} + ); }; diff --git a/datahub-web-react/src/app/entityV2/view/ViewsTable.tsx b/datahub-web-react/src/app/entityV2/view/ViewsTable.tsx index 2ab7042e83d450..d38a8e1fa040ae 100644 --- a/datahub-web-react/src/app/entityV2/view/ViewsTable.tsx +++ b/datahub-web-react/src/app/entityV2/view/ViewsTable.tsx @@ -1,7 +1,9 @@ -import { Empty } from 'antd'; +import { Table, Text } from '@components'; import React from 'react'; -import { StyledTable } from '@app/entityV2/shared/components/styled/StyledTable'; +import { AlignmentOptions } from '@components/theme/config'; + +import { EmptyContainer } from '@app/entityV2/view/ViewsList'; import { ActionsColumn, DescriptionColumn, @@ -13,7 +15,7 @@ import { DataHubView } from '@types'; type ViewsTableProps = { views: DataHubView[]; - onEditView: (urn) => void; + onEditView?: (urn) => void; }; /** @@ -25,24 +27,28 @@ export const ViewsTable = ({ views, onEditView }: ViewsTableProps) => { title: 'Name', dataIndex: 'name', key: 'name', - render: (name, record) => , + width: '25%', + render: (record) => , }, { title: 'Description', dataIndex: 'description', key: 'description', - render: (description) => , + render: (record) => , }, { title: 'Type', dataIndex: 'viewType', key: 'viewType', - render: (viewType) => , + width: '10%', + render: (record) => , }, { title: '', dataIndex: '', key: 'x', + width: '5%', + alignment: 'right' as AlignmentOptions, render: (record) => , }, ]; @@ -50,19 +56,20 @@ export const ViewsTable = ({ views, onEditView }: ViewsTableProps) => { /** * The data for the Views List. */ - const tableData = views.map((view) => ({ - ...view, - })); + const tableData = + views.map((view) => ({ + ...view, + })) || []; + + if (!views.length) { + return ( + + + No results! + + + ); + } - return ( - , - }} - pagination={false} - /> - ); + return ; }; diff --git a/datahub-web-react/src/app/entityV2/view/select/ViewsTableColumns.tsx b/datahub-web-react/src/app/entityV2/view/select/ViewsTableColumns.tsx index 14f79dc022f006..5b623817d2ea2f 100644 --- a/datahub-web-react/src/app/entityV2/view/select/ViewsTableColumns.tsx +++ b/datahub-web-react/src/app/entityV2/view/select/ViewsTableColumns.tsx @@ -1,4 +1,4 @@ -import { Button, Typography } from 'antd'; +import { Text } from '@components'; import React from 'react'; import styled from 'styled-components'; @@ -15,13 +15,6 @@ const StyledDescription = styled.div` max-width: 300px; `; -const ActionButtonsContainer = styled.div` - display: flex; - justify-content: center; - align-items: center; - padding-right: 8px; -`; - const NameContainer = styled.span` display: flex; align-items: center; @@ -37,7 +30,7 @@ const IconPlaceholder = styled.span` type NameColumnProps = { name: string; record: any; - onEditView: (urn) => void; + onEditView?: (urn) => void; }; export function NameColumn({ name, record, onEditView }: NameColumnProps) { @@ -54,9 +47,9 @@ export function NameColumn({ name, record, onEditView }: NameColumnProps) { {isUserDefault && } {isGlobalDefault && } - + onEditView?.(record.urn)}> + {name} + ); } @@ -66,11 +59,7 @@ type DescriptionColumnProps = { }; export function DescriptionColumn({ description }: DescriptionColumnProps) { - return ( - - {description || No description} - - ); + return {description || '-'}; } type ViewTypeColumnProps = { @@ -86,9 +75,5 @@ type ActionColumnProps = { }; export function ActionsColumn({ record }: ActionColumnProps) { - return ( - - - - ); + return ; } diff --git a/datahub-web-react/src/app/settingsV2/SettingsPage.tsx b/datahub-web-react/src/app/settingsV2/SettingsPage.tsx index eced8802c0e6a0..d45ec45ef82905 100644 --- a/datahub-web-react/src/app/settingsV2/SettingsPage.tsx +++ b/datahub-web-react/src/app/settingsV2/SettingsPage.tsx @@ -121,7 +121,7 @@ export const SettingsPage = () => { items: [ { type: NavBarMenuItemTypes.Item, - title: 'My Views', + title: 'Views', key: 'views', link: `${url}/views`, isHidden: !showViews, diff --git a/datahub-web-react/src/app/settingsV2/settingsPaths.tsx b/datahub-web-react/src/app/settingsV2/settingsPaths.tsx index 463acd75f7863c..6788df331f7684 100644 --- a/datahub-web-react/src/app/settingsV2/settingsPaths.tsx +++ b/datahub-web-react/src/app/settingsV2/settingsPaths.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { ManageViews } from '@app/entity/view/ManageViews'; import { ManageOwnership } from '@app/entityV2/ownership/ManageOwnership'; +import { ManageViews } from '@app/entityV2/view/ManageViews'; import { ManageIdentities } from '@app/identity/ManageIdentities'; import { ManagePermissions } from '@app/permissions/ManagePermissions'; import { ManagePolicies } from '@app/permissions/policy/ManagePolicies'; diff --git a/smoke-test/tests/cypress/cypress/e2e/viewV2/v2_manage_views.js b/smoke-test/tests/cypress/cypress/e2e/viewV2/v2_manage_views.js index 26fc1252f5a401..1af02d718af1a3 100644 --- a/smoke-test/tests/cypress/cypress/e2e/viewV2/v2_manage_views.js +++ b/smoke-test/tests/cypress/cypress/e2e/viewV2/v2_manage_views.js @@ -9,10 +9,8 @@ describe("manage views", () => { cy.goToViewsSettings(); cy.waitTextVisible("Settings"); cy.wait(1000); - cy.clickOptionWithText("Create new View"); - cy.get(".ant-input-affix-wrapper > input[type='text']") - .first() - .type(viewName); + cy.clickOptionWithText("Create View"); + cy.get('[data-testid="view-name-input"]').click().type(viewName); cy.clickOptionWithTestId("view-builder-save"); // Confirm that the test has been created. @@ -21,8 +19,8 @@ describe("manage views", () => { // Now edit the View cy.clickFirstOptionWithTestId("views-table-dropdown"); cy.get('[data-testid="view-dropdown-edit"]').click({ force: true }); - cy.get(".ant-input-affix-wrapper > input[type='text']") - .first() + cy.get('[data-testid="view-name-input"]') + .click() .clear() .type("New View Name"); cy.clickOptionWithTestId("view-builder-save"); diff --git a/smoke-test/tests/cypress/cypress/support/commands.js b/smoke-test/tests/cypress/cypress/support/commands.js index 7085c2b1ddb006..5baf593b84d40f 100644 --- a/smoke-test/tests/cypress/cypress/support/commands.js +++ b/smoke-test/tests/cypress/cypress/support/commands.js @@ -125,7 +125,7 @@ Cypress.Commands.add("goToDomainList", () => { Cypress.Commands.add("goToViewsSettings", () => { cy.visit("/settings/views"); - cy.waitTextVisible("Manage Views"); + cy.waitTextVisible("Views"); }); Cypress.Commands.add("goToOwnershipTypesSettings", () => {