mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-24 08:28:12 +00:00
fix(UI) Fix multiple UI usability issues (#4975)
This commit is contained in:
parent
39fb7e5eb3
commit
3aa841c2e1
@ -1774,6 +1774,14 @@ export const mocks = [
|
||||
displayName: 'origin',
|
||||
aggregations: [{ value: 'PROD', count: 3, entity: null }],
|
||||
},
|
||||
{
|
||||
field: 'entity',
|
||||
displayName: 'Type',
|
||||
aggregations: [
|
||||
{ count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' },
|
||||
{ count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'platform',
|
||||
displayName: 'platform',
|
||||
@ -1837,6 +1845,14 @@ export const mocks = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'entity',
|
||||
displayName: 'Type',
|
||||
aggregations: [
|
||||
{ count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' },
|
||||
{ count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' },
|
||||
],
|
||||
},
|
||||
{
|
||||
__typename: 'FacetMetadata',
|
||||
field: 'platform',
|
||||
@ -1895,6 +1911,14 @@ export const mocks = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'entity',
|
||||
displayName: 'Type',
|
||||
aggregations: [
|
||||
{ count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' },
|
||||
{ count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'platform',
|
||||
displayName: 'platform',
|
||||
@ -1987,6 +2011,14 @@ export const mocks = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'entity',
|
||||
displayName: 'Type',
|
||||
aggregations: [
|
||||
{ count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' },
|
||||
{ count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'platform',
|
||||
displayName: 'platform',
|
||||
@ -2143,6 +2175,14 @@ export const mocks = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'entity',
|
||||
displayName: 'Type',
|
||||
aggregations: [
|
||||
{ count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' },
|
||||
{ count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'platform',
|
||||
displayName: 'platform',
|
||||
@ -2208,6 +2248,14 @@ export const mocks = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'entity',
|
||||
displayName: 'Type',
|
||||
aggregations: [
|
||||
{ count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' },
|
||||
{ count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'platform',
|
||||
displayName: 'platform',
|
||||
@ -2490,6 +2538,26 @@ export const mocks = [
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// displayName: 'Domain',
|
||||
// field: 'domains',
|
||||
// __typename: 'FacetMetadata',
|
||||
// aggregations: [
|
||||
// {
|
||||
// value: 'urn:li:domain:baedb9f9-98ef-4846-8a0c-2a88680f213e',
|
||||
// count: 1,
|
||||
// __typename: 'AggregationMetadata',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
field: 'entity',
|
||||
displayName: 'Type',
|
||||
aggregations: [
|
||||
{ count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' },
|
||||
{ count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' },
|
||||
],
|
||||
},
|
||||
{
|
||||
__typename: 'FacetMetadata',
|
||||
field: 'platform',
|
||||
@ -2665,6 +2733,14 @@ export const mocks = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'entity',
|
||||
displayName: 'Type',
|
||||
aggregations: [
|
||||
{ count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' },
|
||||
{ count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'platform',
|
||||
displayName: 'platform',
|
||||
@ -2731,69 +2807,11 @@ export const mocks = [
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'platform',
|
||||
displayName: 'platform',
|
||||
field: 'entity',
|
||||
displayName: 'Type',
|
||||
aggregations: [
|
||||
{ value: 'hdfs', count: 1, entity: null },
|
||||
{ value: 'mysql', count: 1, entity: null },
|
||||
{ value: 'kafka', count: 1, entity: null },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
} as GetSearchResultsForMultipleQuery,
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GetSearchResultsForMultipleDocument,
|
||||
variables: {
|
||||
input: {
|
||||
types: ['DATASET'],
|
||||
query: 'test',
|
||||
start: 0,
|
||||
count: 10,
|
||||
filters: [
|
||||
{
|
||||
field: 'platform',
|
||||
value: 'kafka',
|
||||
},
|
||||
{
|
||||
field: 'platform',
|
||||
value: 'hdfs',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
__typename: 'Query',
|
||||
searchAcrossEntities: {
|
||||
__typename: 'SearchResults',
|
||||
start: 0,
|
||||
count: 1,
|
||||
total: 1,
|
||||
searchResults: [
|
||||
{
|
||||
entity: {
|
||||
__typename: 'Dataset',
|
||||
...dataset3,
|
||||
},
|
||||
matchedFields: [],
|
||||
insights: [],
|
||||
},
|
||||
],
|
||||
facets: [
|
||||
{
|
||||
field: 'origin',
|
||||
displayName: 'origin',
|
||||
aggregations: [
|
||||
{
|
||||
value: 'PROD',
|
||||
count: 3,
|
||||
entity: null,
|
||||
},
|
||||
{ count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' },
|
||||
{ count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -2862,6 +2880,88 @@ export const mocks = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'entity',
|
||||
displayName: 'Type',
|
||||
aggregations: [
|
||||
{ count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' },
|
||||
{ count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'platform',
|
||||
displayName: 'platform',
|
||||
aggregations: [
|
||||
{ value: 'hdfs', count: 1, entity: null },
|
||||
{ value: 'mysql', count: 1, entity: null },
|
||||
{ value: 'kafka', count: 1, entity: null },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
} as GetSearchResultsForMultipleQuery,
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GetSearchResultsForMultipleDocument,
|
||||
variables: {
|
||||
input: {
|
||||
types: ['DATASET'],
|
||||
query: 'test',
|
||||
start: 0,
|
||||
count: 10,
|
||||
filters: [
|
||||
{
|
||||
field: 'platform',
|
||||
value: 'kafka',
|
||||
},
|
||||
{
|
||||
field: 'platform',
|
||||
value: 'hdfs',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
__typename: 'Query',
|
||||
searchAcrossEntities: {
|
||||
__typename: 'SearchResults',
|
||||
start: 0,
|
||||
count: 1,
|
||||
total: 1,
|
||||
searchResults: [
|
||||
{
|
||||
entity: {
|
||||
__typename: 'Dataset',
|
||||
...dataset3,
|
||||
},
|
||||
matchedFields: [],
|
||||
insights: [],
|
||||
},
|
||||
],
|
||||
facets: [
|
||||
{
|
||||
field: 'origin',
|
||||
displayName: 'origin',
|
||||
aggregations: [
|
||||
{
|
||||
value: 'PROD',
|
||||
count: 3,
|
||||
entity: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'entity',
|
||||
displayName: 'Type',
|
||||
aggregations: [
|
||||
{ count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' },
|
||||
{ count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'platform',
|
||||
displayName: 'platform',
|
||||
@ -3096,6 +3196,14 @@ export const mocks = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'entity',
|
||||
displayName: 'Type',
|
||||
aggregations: [
|
||||
{ count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' },
|
||||
{ count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'platform',
|
||||
displayName: 'platform',
|
||||
|
||||
@ -8,6 +8,7 @@ import getAvatarColor from '../../../shared/avatar/getAvatarColor';
|
||||
|
||||
export type Props = {
|
||||
users?: (UserUsageCounts | null)[] | null;
|
||||
maxNumberDisplayed?: number;
|
||||
};
|
||||
|
||||
const AvatarStyled = styled(Avatar)<{ backgroundColor: string }>`
|
||||
@ -15,12 +16,16 @@ const AvatarStyled = styled(Avatar)<{ backgroundColor: string }>`
|
||||
background-color: ${(props) => props.backgroundColor};
|
||||
`;
|
||||
|
||||
export default function UsageFacepile({ users }: Props) {
|
||||
export default function UsageFacepile({ users, maxNumberDisplayed }: Props) {
|
||||
const sortedUsers = useMemo(() => users?.slice().sort((a, b) => (b?.count || 0) - (a?.count || 0)), [users]);
|
||||
let displayedUsers = sortedUsers;
|
||||
if (maxNumberDisplayed) {
|
||||
displayedUsers = displayedUsers?.slice(0, maxNumberDisplayed);
|
||||
}
|
||||
|
||||
return (
|
||||
<SpacedAvatarGroup maxCount={2}>
|
||||
{sortedUsers?.map((user) => (
|
||||
{displayedUsers?.map((user) => (
|
||||
<Tooltip title={user?.userEmail}>
|
||||
<AvatarStyled backgroundColor={getAvatarColor(user?.userEmail || undefined)}>
|
||||
{user?.userEmail?.charAt(0).toUpperCase()}
|
||||
|
||||
@ -100,7 +100,7 @@ export const SidebarStatsSection = () => {
|
||||
) : null}
|
||||
{(usageStats?.aggregations?.users?.length || 0) > 0 ? (
|
||||
<InfoItem title="Top Users" width={INFO_ITEM_WIDTH_PX}>
|
||||
<UsageFacepile users={usageStats?.aggregations?.users} />
|
||||
<UsageFacepile users={usageStats?.aggregations?.users} maxNumberDisplayed={10} />
|
||||
</InfoItem>
|
||||
) : null}
|
||||
</StatsRow>
|
||||
|
||||
@ -70,7 +70,7 @@ export default function TableStats({
|
||||
{users && (
|
||||
<InfoItem title="Top Users">
|
||||
<div style={{ paddingTop: 8 }}>
|
||||
<UsageFacepile users={users} />
|
||||
<UsageFacepile users={users} maxNumberDisplayed={10} />
|
||||
</div>
|
||||
</InfoItem>
|
||||
)}
|
||||
|
||||
@ -38,7 +38,7 @@ const styles = {
|
||||
navBar: { padding: '24px' },
|
||||
searchContainer: { width: '100%', marginTop: '40px' },
|
||||
logoImage: { width: 140 },
|
||||
searchBox: { width: '40vw', minWidth: 400, margin: '40px 0px', marginBottom: '12px' },
|
||||
searchBox: { width: '47vw', minWidth: 400, margin: '40px 0px', marginBottom: '12px', maxWidth: '650px' },
|
||||
subtitle: { marginTop: '28px', color: '#FFFFFF', fontSize: 12 },
|
||||
};
|
||||
|
||||
@ -58,8 +58,9 @@ const NavGroup = styled.div`
|
||||
`;
|
||||
|
||||
const SuggestionsContainer = styled.div`
|
||||
padding: 0px 30px;
|
||||
max-width: 540px;
|
||||
margin: 0px 30px;
|
||||
max-width: 650px;
|
||||
width: 47vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: left;
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import React, { useContext, useMemo, useState } from 'react';
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { Group } from '@vx/group';
|
||||
import { LinkHorizontal } from '@vx/shape';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useEntityRegistry } from '../useEntityRegistry';
|
||||
import { IconStyleType } from '../entity/Entity';
|
||||
import { NodeData, Direction, VizNode, EntitySelectParams } from './types';
|
||||
import { NodeData, Direction, VizNode, EntitySelectParams, EntityAndType } from './types';
|
||||
import { ANTD_GRAY } from '../entity/shared/constants';
|
||||
import { capitalizeFirstLetter } from '../shared/textUtil';
|
||||
import { nodeHeightFromTitleLength } from './utils/nodeHeightFromTitleLength';
|
||||
import { LineageExplorerContext } from './utils/LineageExplorerContext';
|
||||
import useLazyGetEntityQuery from './utils/useLazyGetEntityQuery';
|
||||
|
||||
const CLICK_DELAY_THRESHOLD = 1000;
|
||||
const DRAG_DISTANCE_THRESHOLD = 20;
|
||||
@ -81,13 +82,20 @@ export default function LineageEntityNode({
|
||||
onEntityCenter: (EntitySelectParams) => void;
|
||||
onHover: (EntitySelectParams) => void;
|
||||
onDrag: (params: EntitySelectParams, event: React.MouseEvent) => void;
|
||||
onExpandClick: (LineageExpandParams) => void;
|
||||
onExpandClick: (data: EntityAndType) => void;
|
||||
direction: Direction;
|
||||
nodesToRenderByUrn: Record<string, VizNode>;
|
||||
}) {
|
||||
const { expandTitles } = useContext(LineageExplorerContext);
|
||||
const [isExpanding, setIsExpanding] = useState(false);
|
||||
const [expandHover, setExpandHover] = useState(false);
|
||||
const { getAsyncEntity, asyncData } = useLazyGetEntityQuery();
|
||||
|
||||
useEffect(() => {
|
||||
if (asyncData) {
|
||||
onExpandClick(asyncData);
|
||||
}
|
||||
}, [asyncData, onExpandClick]);
|
||||
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const unexploredHiddenChildren =
|
||||
@ -139,7 +147,9 @@ export default function LineageEntityNode({
|
||||
<Group
|
||||
onClick={() => {
|
||||
setIsExpanding(true);
|
||||
onExpandClick({ urn: node.data.urn, type: node.data.type, direction });
|
||||
if (node.data.urn && node.data.type) {
|
||||
getAsyncEntity(node.data.urn, node.data.type);
|
||||
}
|
||||
}}
|
||||
onMouseOver={() => {
|
||||
setExpandHover(true);
|
||||
|
||||
@ -8,10 +8,9 @@ import styled from 'styled-components';
|
||||
import { Message } from '../shared/Message';
|
||||
import { useEntityRegistry } from '../useEntityRegistry';
|
||||
import CompactContext from '../shared/CompactContext';
|
||||
import { EntityAndType, EntitySelectParams, FetchedEntities, LineageExpandParams } from './types';
|
||||
import { EntityAndType, EntitySelectParams, FetchedEntities } from './types';
|
||||
import LineageViz from './LineageViz';
|
||||
import extendAsyncEntities from './utils/extendAsyncEntities';
|
||||
import useLazyGetEntityQuery from './utils/useLazyGetEntityQuery';
|
||||
import useGetEntityQuery from './utils/useGetEntityQuery';
|
||||
import { EntityType } from '../../types.generated';
|
||||
import { capitalizeFirstLetter } from '../shared/textUtil';
|
||||
@ -58,7 +57,6 @@ export default function LineageExplorer({ urn, type }: Props) {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
|
||||
const { loading, error, data } = useGetEntityQuery(urn, type);
|
||||
const { getAsyncEntity, asyncData } = useLazyGetEntityQuery();
|
||||
|
||||
const [isDrawerVisible, setIsDrawVisible] = useState(false);
|
||||
const [selectedEntity, setSelectedEntity] = useState<EntitySelectParams | undefined>(undefined);
|
||||
@ -94,10 +92,7 @@ export default function LineageExplorer({ urn, type }: Props) {
|
||||
if (type && data) {
|
||||
maybeAddAsyncLoadedEntity(data);
|
||||
}
|
||||
if (asyncData) {
|
||||
maybeAddAsyncLoadedEntity(asyncData);
|
||||
}
|
||||
}, [data, asyncData, asyncEntities, setAsyncEntities, maybeAddAsyncLoadedEntity, urn, previousUrn, type]);
|
||||
}, [data, asyncEntities, setAsyncEntities, maybeAddAsyncLoadedEntity, urn, previousUrn, type]);
|
||||
|
||||
if (error || (!loading && !error && !data)) {
|
||||
return <Alert type="error" message={error?.message || 'Entity failed to load'} />;
|
||||
@ -124,8 +119,8 @@ export default function LineageExplorer({ urn, type }: Props) {
|
||||
`${entityRegistry.getEntityUrl(params.type, params.urn)}/?is_lineage_mode=true`,
|
||||
);
|
||||
}}
|
||||
onLineageExpand={(params: LineageExpandParams) => {
|
||||
getAsyncEntity(params.urn, params.type);
|
||||
onLineageExpand={(asyncData: EntityAndType) => {
|
||||
maybeAddAsyncLoadedEntity(asyncData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { TransformMatrix } from '@vx/zoom/lib/types';
|
||||
|
||||
import { NodeData, Direction, EntitySelectParams, TreeProps } from './types';
|
||||
import { NodeData, Direction, EntitySelectParams, TreeProps, EntityAndType } from './types';
|
||||
import LineageTreeNodeAndEdgeRenderer from './LineageTreeNodeAndEdgeRenderer';
|
||||
import layoutTree from './utils/layoutTree';
|
||||
import { LineageExplorerContext } from './utils/LineageExplorerContext';
|
||||
@ -13,7 +13,7 @@ type LineageTreeProps = {
|
||||
};
|
||||
onEntityClick: (EntitySelectParams) => void;
|
||||
onEntityCenter: (EntitySelectParams) => void;
|
||||
onLineageExpand: (LineageExpandParams) => void;
|
||||
onLineageExpand: (data: EntityAndType) => void;
|
||||
selectedEntity?: EntitySelectParams;
|
||||
hoveredEntity?: EntitySelectParams;
|
||||
setHoveredEntity: (EntitySelectParams) => void;
|
||||
|
||||
@ -4,7 +4,7 @@ import { curveBasis } from '@vx/curve';
|
||||
import { LinePath } from '@vx/shape';
|
||||
import { TransformMatrix } from '@vx/zoom/lib/types';
|
||||
|
||||
import { NodeData, Direction, EntitySelectParams, TreeProps, VizNode, VizEdge } from './types';
|
||||
import { NodeData, Direction, EntitySelectParams, TreeProps, VizNode, VizEdge, EntityAndType } from './types';
|
||||
import LineageEntityNode from './LineageEntityNode';
|
||||
import { ANTD_GRAY } from '../entity/shared/constants';
|
||||
|
||||
@ -15,7 +15,7 @@ type Props = {
|
||||
};
|
||||
onEntityClick: (EntitySelectParams) => void;
|
||||
onEntityCenter: (EntitySelectParams) => void;
|
||||
onLineageExpand: (LineageExpandParams) => void;
|
||||
onLineageExpand: (data: EntityAndType) => void;
|
||||
selectedEntity?: EntitySelectParams;
|
||||
hoveredEntity?: EntitySelectParams;
|
||||
setHoveredEntity: (EntitySelectParams) => void;
|
||||
|
||||
@ -68,7 +68,7 @@ type Props = {
|
||||
fetchedEntities: { [x: string]: FetchedEntity };
|
||||
onEntityClick: (EntitySelectParams) => void;
|
||||
onEntityCenter: (EntitySelectParams) => void;
|
||||
onLineageExpand: (LineageExpandParams) => void;
|
||||
onLineageExpand: (data: EntityAndType) => void;
|
||||
selectedEntity?: EntitySelectParams;
|
||||
zoom: ProvidedZoom & {
|
||||
transformMatrix: TransformMatrix;
|
||||
|
||||
@ -87,7 +87,7 @@ export type TreeProps = {
|
||||
fetchedEntities: { [x: string]: FetchedEntity };
|
||||
onEntityClick: (EntitySelectParams) => void;
|
||||
onEntityCenter: (EntitySelectParams) => void;
|
||||
onLineageExpand: (LineageExpandParams) => void;
|
||||
onLineageExpand: (data: EntityAndType) => void;
|
||||
selectedEntity?: EntitySelectParams;
|
||||
hoveredEntity?: EntitySelectParams;
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState, useRef } from 'react';
|
||||
import { Input, AutoComplete, Image, Typography } from 'antd';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import styled from 'styled-components';
|
||||
@ -37,7 +37,7 @@ const ExploreForEntity = styled.span`
|
||||
|
||||
const StyledAutoComplete = styled(AutoComplete)`
|
||||
width: 100%;
|
||||
max-width: 475px;
|
||||
max-width: 650px;
|
||||
`;
|
||||
|
||||
const AutoCompleteContainer = styled.div`
|
||||
@ -161,6 +161,7 @@ interface Props {
|
||||
autoCompleteStyle?: React.CSSProperties;
|
||||
entityRegistry: EntityRegistry;
|
||||
fixAutoComplete?: boolean;
|
||||
setIsSearchBarFocused?: (isSearchBarFocused: boolean) => void;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
@ -181,6 +182,7 @@ export const SearchBar = ({
|
||||
inputStyle,
|
||||
autoCompleteStyle,
|
||||
fixAutoComplete,
|
||||
setIsSearchBarFocused,
|
||||
}: Props) => {
|
||||
const history = useHistory();
|
||||
const [searchQuery, setSearchQuery] = useState<string>();
|
||||
@ -249,8 +251,20 @@ export const SearchBar = ({
|
||||
return emptyQueryOptions;
|
||||
}, [emptyQueryOptions, autoCompleteEntityOptions, autoCompleteQueryOptions]);
|
||||
|
||||
const searchBarWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
function handleSearchBarClick(isSearchBarFocused: boolean) {
|
||||
if (
|
||||
setIsSearchBarFocused &&
|
||||
(!isSearchBarFocused ||
|
||||
(searchBarWrapperRef && searchBarWrapperRef.current && searchBarWrapperRef.current.clientWidth < 590))
|
||||
) {
|
||||
setIsSearchBarFocused(isSearchBarFocused);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AutoCompleteContainer style={style}>
|
||||
<AutoCompleteContainer style={style} ref={searchBarWrapperRef}>
|
||||
<StyledAutoComplete
|
||||
defaultActiveFirstOption={false}
|
||||
style={autoCompleteStyle}
|
||||
@ -292,6 +306,8 @@ export const SearchBar = ({
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
data-testid="search-input"
|
||||
prefix={<SearchOutlined onClick={() => onSearch(filterSearchQuery(searchQuery || ''))} />}
|
||||
onFocus={() => handleSearchBarClick(true)}
|
||||
onBlur={() => handleSearchBarClick(false)}
|
||||
/>
|
||||
</StyledAutoComplete>
|
||||
</AutoCompleteContainer>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { DownOutlined, UpOutlined } from '@ant-design/icons';
|
||||
import { Button, Checkbox } from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
|
||||
import * as React from 'react';
|
||||
@ -6,7 +7,7 @@ import styled from 'styled-components';
|
||||
|
||||
import { FacetMetadata } from '../../types.generated';
|
||||
import { SearchFilterLabel } from './SearchFilterLabel';
|
||||
import { FILTERS_TO_TRUNCATE, TRUNCATED_FILTER_LENGTH } from './utils/constants';
|
||||
import { TRUNCATED_FILTER_LENGTH } from './utils/constants';
|
||||
|
||||
type Props = {
|
||||
facet: FacetMetadata;
|
||||
@ -15,6 +16,7 @@ type Props = {
|
||||
value: string;
|
||||
}>;
|
||||
onFilterSelect: (selected: boolean, field: string, value: string) => void;
|
||||
defaultDisplayFilters: boolean;
|
||||
};
|
||||
|
||||
const SearchFilterWrapper = styled.div`
|
||||
@ -22,8 +24,11 @@ const SearchFilterWrapper = styled.div`
|
||||
`;
|
||||
|
||||
const Title = styled.div`
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const CheckBox = styled(Checkbox)`
|
||||
@ -37,41 +42,63 @@ const ExpandButton = styled(Button)`
|
||||
}
|
||||
`;
|
||||
|
||||
export const SearchFilter = ({ facet, selectedFilters, onFilterSelect }: Props) => {
|
||||
const StyledUpOutlined = styled(UpOutlined)`
|
||||
font-size: 10px;
|
||||
`;
|
||||
|
||||
const StyledDownOutlined = styled(DownOutlined)`
|
||||
font-size: 10px;
|
||||
`;
|
||||
|
||||
export const SearchFilter = ({ facet, selectedFilters, onFilterSelect, defaultDisplayFilters }: Props) => {
|
||||
const [areFiltersVisible, setAreFiltersVisible] = useState(defaultDisplayFilters);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const shouldTruncate =
|
||||
FILTERS_TO_TRUNCATE.indexOf(facet.field) > -1 && facet.aggregations.length > TRUNCATED_FILTER_LENGTH;
|
||||
const shouldTruncate = facet.aggregations.length > TRUNCATED_FILTER_LENGTH;
|
||||
|
||||
return (
|
||||
<SearchFilterWrapper key={facet.field}>
|
||||
<Title>{facet?.displayName}</Title>
|
||||
{facet.aggregations.map((aggregation, i) => {
|
||||
if (i >= TRUNCATED_FILTER_LENGTH && !expanded && shouldTruncate) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span key={`${facet.field}-${aggregation.value}`}>
|
||||
<CheckBox
|
||||
data-testid={`facet-${facet.field}-${aggregation.value}`}
|
||||
checked={
|
||||
selectedFilters.find(
|
||||
(f) => f.field === facet.field && f.value === aggregation.value,
|
||||
) !== undefined
|
||||
}
|
||||
onChange={(e: CheckboxChangeEvent) =>
|
||||
onFilterSelect(e.target.checked, facet.field, aggregation.value)
|
||||
}
|
||||
>
|
||||
<SearchFilterLabel field={facet.field} aggregation={aggregation} />
|
||||
</CheckBox>
|
||||
<br />
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{shouldTruncate && (
|
||||
<ExpandButton type="text" onClick={() => setExpanded(!expanded)}>
|
||||
{expanded ? '- Less' : '+ More'}
|
||||
</ExpandButton>
|
||||
<Title>
|
||||
{facet?.displayName}
|
||||
{areFiltersVisible ? (
|
||||
<StyledUpOutlined onClick={() => setAreFiltersVisible(false)} />
|
||||
) : (
|
||||
<StyledDownOutlined
|
||||
data-testid={`expand-facet-${facet.field}`}
|
||||
onClick={() => setAreFiltersVisible(true)}
|
||||
/>
|
||||
)}
|
||||
</Title>
|
||||
{areFiltersVisible && (
|
||||
<>
|
||||
{facet.aggregations.map((aggregation, i) => {
|
||||
if (i >= TRUNCATED_FILTER_LENGTH && !expanded) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span key={`${facet.field}-${aggregation.value}`}>
|
||||
<CheckBox
|
||||
data-testid={`facet-${facet.field}-${aggregation.value}`}
|
||||
checked={
|
||||
selectedFilters.find(
|
||||
(f) => f.field === facet.field && f.value === aggregation.value,
|
||||
) !== undefined
|
||||
}
|
||||
onChange={(e: CheckboxChangeEvent) =>
|
||||
onFilterSelect(e.target.checked, facet.field, aggregation.value)
|
||||
}
|
||||
>
|
||||
<SearchFilterLabel field={facet.field} aggregation={aggregation} />
|
||||
</CheckBox>
|
||||
<br />
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{shouldTruncate && (
|
||||
<ExpandButton type="text" onClick={() => setExpanded(!expanded)}>
|
||||
{expanded ? '- Less' : '+ More'}
|
||||
</ExpandButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SearchFilterWrapper>
|
||||
);
|
||||
|
||||
@ -4,6 +4,8 @@ import { useEffect, useState } from 'react';
|
||||
import { FacetMetadata } from '../../types.generated';
|
||||
import { SearchFilter } from './SearchFilter';
|
||||
|
||||
const TOP_FILTERS = ['entity', 'tags', 'glossaryTerms', 'domains', 'owners'];
|
||||
|
||||
export const SearchFilterWrapper = styled.div`
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
@ -62,14 +64,21 @@ export const SearchFilters = ({ facets, selectedFilters, onFilterSelect, loading
|
||||
onFilterSelect(newFilters);
|
||||
};
|
||||
|
||||
const sortedFacets = cachedProps.facets.sort((facetA, facetB) => {
|
||||
if (TOP_FILTERS.indexOf(facetA.field) === -1) return 1;
|
||||
if (TOP_FILTERS.indexOf(facetB.field) === -1) return -1;
|
||||
return TOP_FILTERS.indexOf(facetA.field) - TOP_FILTERS.indexOf(facetB.field);
|
||||
});
|
||||
|
||||
return (
|
||||
<SearchFilterWrapper>
|
||||
{cachedProps.facets.map((facet) => (
|
||||
{sortedFacets.map((facet) => (
|
||||
<SearchFilter
|
||||
key={`${facet.displayName}-${facet.field}`}
|
||||
facet={facet}
|
||||
selectedFilters={cachedProps.selectedFilters}
|
||||
onFilterSelect={onFilterSelectAndSetCache}
|
||||
defaultDisplayFilters={TOP_FILTERS.includes(facet.field)}
|
||||
/>
|
||||
))}
|
||||
</SearchFilterWrapper>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Image, Layout } from 'antd';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled, { useTheme } from 'styled-components';
|
||||
@ -75,6 +75,7 @@ export const SearchHeader = ({
|
||||
authenticatedUserPictureLink,
|
||||
entityRegistry,
|
||||
}: Props) => {
|
||||
const [isSearchBarFocused, setIsSearchBarFocused] = useState(false);
|
||||
const themeConfig = useTheme();
|
||||
const appConfig = useAppConfig();
|
||||
|
||||
@ -98,11 +99,12 @@ export const SearchHeader = ({
|
||||
onSearch={onSearch}
|
||||
onQueryChange={onQueryChange}
|
||||
entityRegistry={entityRegistry}
|
||||
setIsSearchBarFocused={setIsSearchBarFocused}
|
||||
fixAutoComplete
|
||||
/>
|
||||
</LogoSearchContainer>
|
||||
<NavGroup>
|
||||
<AdminHeaderLinks />
|
||||
<AdminHeaderLinks areLinksHidden={isSearchBarFocused} />
|
||||
<ManageAccount urn={authenticatedUserUrn} pictureLink={authenticatedUserPictureLink || ''} />
|
||||
</NavGroup>
|
||||
</Header>
|
||||
|
||||
@ -26,7 +26,6 @@ describe('SearchPage', () => {
|
||||
});
|
||||
|
||||
it('renders the selected filters as checked', async () => {
|
||||
const promise = Promise.resolve();
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<MockedProvider
|
||||
mocks={mocks}
|
||||
@ -44,21 +43,16 @@ describe('SearchPage', () => {
|
||||
</MockedProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(queryByTestId('facet-platform-kafka')).toBeInTheDocument());
|
||||
await waitFor(() => expect(queryByTestId('facet-entity-DATASET')).toBeInTheDocument());
|
||||
|
||||
const kafkaPlatformBox = getByTestId('facet-platform-kafka');
|
||||
expect(kafkaPlatformBox).toHaveProperty('checked', true);
|
||||
const datasetEntityBox = getByTestId('facet-entity-DATASET');
|
||||
expect(datasetEntityBox).toHaveProperty('checked', true);
|
||||
|
||||
const hdfsPlatformBox = getByTestId('facet-platform-hdfs');
|
||||
expect(hdfsPlatformBox).toHaveProperty('checked', false);
|
||||
|
||||
const prodOriginBox = getByTestId('facet-origin-PROD');
|
||||
expect(prodOriginBox).toHaveProperty('checked', false);
|
||||
await act(() => promise);
|
||||
const chartEntityBox = getByTestId('facet-entity-CHART');
|
||||
expect(chartEntityBox).toHaveProperty('checked', false);
|
||||
});
|
||||
|
||||
it('renders multiple checked filters at once', async () => {
|
||||
const promise = Promise.resolve();
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<MockedProvider
|
||||
mocks={mocks}
|
||||
@ -76,17 +70,19 @@ describe('SearchPage', () => {
|
||||
</MockedProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(queryByTestId('facet-platform-kafka')).toBeInTheDocument());
|
||||
await waitFor(() => expect(queryByTestId('facet-entity-DATASET')).toBeInTheDocument());
|
||||
|
||||
const kafkaPlatformBox = getByTestId('facet-platform-kafka');
|
||||
expect(kafkaPlatformBox).toHaveProperty('checked', true);
|
||||
const datasetEntityBox = getByTestId('facet-entity-DATASET');
|
||||
expect(datasetEntityBox).toHaveProperty('checked', true);
|
||||
|
||||
const expandButton = getByTestId('expand-facet-platform');
|
||||
act(() => {
|
||||
fireEvent.click(expandButton);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(queryByTestId('facet-platform-hdfs')).toBeInTheDocument());
|
||||
const hdfsPlatformBox = getByTestId('facet-platform-hdfs');
|
||||
expect(hdfsPlatformBox).toHaveProperty('checked', true);
|
||||
|
||||
const prodOriginBox = getByTestId('facet-origin-PROD');
|
||||
expect(prodOriginBox).toHaveProperty('checked', false);
|
||||
await act(() => promise);
|
||||
});
|
||||
|
||||
it('clicking a filter selects a new filter', async () => {
|
||||
@ -108,24 +104,24 @@ describe('SearchPage', () => {
|
||||
</MockedProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(queryByTestId('facet-platform-kafka')).toBeInTheDocument());
|
||||
await waitFor(() => expect(queryByTestId('facet-entity-DATASET')).toBeInTheDocument());
|
||||
|
||||
const kafkaPlatformBox = getByTestId('facet-platform-kafka');
|
||||
expect(kafkaPlatformBox).toHaveProperty('checked', true);
|
||||
const datasetEntityBox = getByTestId('facet-entity-DATASET');
|
||||
expect(datasetEntityBox).toHaveProperty('checked', true);
|
||||
|
||||
const hdfsPlatformBox = getByTestId('facet-platform-hdfs');
|
||||
expect(hdfsPlatformBox).toHaveProperty('checked', false);
|
||||
const chartEntityBox = getByTestId('facet-entity-CHART');
|
||||
expect(chartEntityBox).toHaveProperty('checked', false);
|
||||
act(() => {
|
||||
fireEvent.click(hdfsPlatformBox);
|
||||
fireEvent.click(chartEntityBox);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(queryByTestId('facet-platform-kafka')).toBeInTheDocument());
|
||||
await waitFor(() => expect(queryByTestId('facet-entity-DATASET')).toBeInTheDocument());
|
||||
|
||||
const kafkaPlatformBox2 = getByTestId('facet-platform-kafka');
|
||||
expect(kafkaPlatformBox2).toHaveProperty('checked', true);
|
||||
const datasetEntityBox2 = getByTestId('facet-entity-DATASET');
|
||||
expect(datasetEntityBox2).toHaveProperty('checked', true);
|
||||
|
||||
const hdfsPlatformBox2 = getByTestId('facet-platform-hdfs');
|
||||
expect(hdfsPlatformBox2).toHaveProperty('checked', true);
|
||||
const chartEntityBox2 = getByTestId('facet-entity-CHART');
|
||||
expect(chartEntityBox2).toHaveProperty('checked', true);
|
||||
await act(() => promise);
|
||||
});
|
||||
});
|
||||
|
||||
@ -32,6 +32,10 @@ const DownArrow = styled(CaretDownOutlined)`
|
||||
color: ${ANTD_GRAY[7]};
|
||||
`;
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
urn: string;
|
||||
pictureLink?: string;
|
||||
@ -85,10 +89,10 @@ export const ManageAccount = ({ urn: _urn, pictureLink: _pictureLink, name }: Pr
|
||||
|
||||
return (
|
||||
<Dropdown overlay={menu}>
|
||||
<Link to={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${_urn}`}>
|
||||
<StyledLink to={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${_urn}`}>
|
||||
<CustomAvatar photoUrl={_pictureLink} style={{ marginRight: 5 }} name={name} />
|
||||
<DownArrow />
|
||||
</Link>
|
||||
</StyledLink>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
@ -17,7 +17,25 @@ const AdminLink = styled.span`
|
||||
margin-right: 4px;
|
||||
`;
|
||||
|
||||
export function AdminHeaderLinks() {
|
||||
const LinksWrapper = styled.div<{ areLinksHidden?: boolean }>`
|
||||
opacity: 1;
|
||||
white-space: nowrap;
|
||||
transition: opacity 0.5s;
|
||||
|
||||
${(props) =>
|
||||
props.areLinksHidden &&
|
||||
`
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
`}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
areLinksHidden?: boolean;
|
||||
}
|
||||
|
||||
export function AdminHeaderLinks(props: Props) {
|
||||
const { areLinksHidden } = props;
|
||||
const me = useGetAuthenticatedUser();
|
||||
const { config } = useAppConfig();
|
||||
|
||||
@ -36,7 +54,7 @@ export function AdminHeaderLinks() {
|
||||
const showDomains = me?.platformPrivileges?.manageDomains || false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<LinksWrapper areLinksHidden={areLinksHidden}>
|
||||
{showAnalytics && (
|
||||
<AdminLink>
|
||||
<Link to="/analytics">
|
||||
@ -91,6 +109,6 @@ export function AdminHeaderLinks() {
|
||||
</Link>
|
||||
</AdminLink>
|
||||
)}
|
||||
</>
|
||||
</LinksWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user