feat(React UI): SearchPage and SearchResultsPage (#2130)

Co-authored-by: John Joyce <john@acryl.io>
This commit is contained in:
John Joyce 2021-02-23 12:45:42 -08:00 committed by GitHub
parent e818ecc702
commit 03f673b16c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 715 additions and 301 deletions

View File

@ -477,6 +477,7 @@ export const mocks = [
total: 1,
entities: [
{
__typename: 'Dataset',
...dataset3,
},
],
@ -535,6 +536,7 @@ export const mocks = [
total: 1,
entities: [
{
__typename: 'Dataset',
...dataset3,
},
],

View File

@ -1,15 +1,15 @@
import React from 'react';
import { Switch, Route, RouteProps, Redirect } from 'react-router-dom';
import { useReactiveVar } from '@apollo/client';
import { BrowseTypesPage } from './browse/BrowseTypesPage';
import { BrowseResultsPage } from './browse/BrowseResultsPage';
import { SearchPage } from './search/SearchPage';
import { LogIn } from './auth/LogIn';
import { NoPageFound } from './shared/NoPageFound';
import { isLoggedInVar } from './auth/checkAuthStatus';
import { EntityPage } from './entity/EntityPage';
import { PageRoutes } from '../conf/Global';
import { useEntityRegistry } from './useEntityRegistry';
import { HomePage } from './home/HomePage';
import { SearchPage } from './search/SearchPage';
const ProtectedRoute = ({
isLoggedIn,
@ -33,7 +33,7 @@ export const Routes = (): JSX.Element => {
return (
<div>
<Switch>
<ProtectedRoute isLoggedIn={isLoggedIn} exact path="/" render={() => <BrowseTypesPage />} />
<ProtectedRoute isLoggedIn={isLoggedIn} exact path="/" render={() => <HomePage />} />
<Route path={PageRoutes.LOG_IN} component={LogIn} />
{entityRegistry.getEntities().map((entity) => (
@ -49,12 +49,6 @@ export const Routes = (): JSX.Element => {
path={PageRoutes.SEARCH_RESULTS}
render={() => <SearchPage />}
/>
<ProtectedRoute
isLoggedIn={isLoggedIn}
exact
path={PageRoutes.BROWSE}
render={() => <BrowseTypesPage />}
/>
<ProtectedRoute
isLoggedIn={isLoggedIn}
path={PageRoutes.BROWSE_RESULTS}

View File

@ -44,8 +44,7 @@ export const LogIn: React.VFC<LogInProps> = () => {
);
if (isLoggedIn) {
// Redirect to search only for Demo Purposes
return <Redirect to="/search?type=dataset&query=test" />;
return <Redirect to="/" />;
}
return (

View File

@ -9,6 +9,7 @@ import { useGetBrowseResultsQuery } from '../../graphql/browse.generated';
import { BrowsePath } from './BrowsePath';
import { PageRoutes } from '../../conf/Global';
import { useEntityRegistry } from '../useEntityRegistry';
import { Message } from '../shared/Message';
type BrowseResultsPageParams = {
type: string;
@ -39,10 +40,6 @@ export const BrowseResultsPage = () => {
},
});
if (loading) {
return <Alert type="info" message="Loading" />;
}
if (error || (!loading && !error && !data)) {
return <Alert type="error" message={error?.message || 'Entity failed to load'} />;
}
@ -63,6 +60,7 @@ export const BrowseResultsPage = () => {
<Affix offsetTop={64}>
<BrowsePath type={entityType} path={path} />
</Affix>
{loading && <Message type="loading" content="Loading..." style={{ marginTop: '10%' }} />}
{data && data.browse && (
<BrowseResults
type={entityType}

View File

@ -1,40 +0,0 @@
import React from 'react';
import 'antd/dist/antd.css';
import { Link } from 'react-router-dom';
import { Col, Row, Card } from 'antd';
import { Content } from 'antd/lib/layout/layout';
import { SearchablePage } from '../search/SearchablePage';
import { PageRoutes } from '../../conf/Global';
import '../../App.css';
import { useEntityRegistry } from '../useEntityRegistry';
export const BrowseTypesPage = () => {
const entityRegistry = useEntityRegistry();
return (
<SearchablePage>
<Content style={{ backgroundColor: 'white', padding: '25px 100px' }}>
<h1 className="ant-typography">Browse</h1>
<Row gutter={[16, 16]}>
{entityRegistry.getBrowseEntityTypes().map((entityType) => (
<Col xs={24} sm={24} md={8}>
<Link to={`${PageRoutes.BROWSE}/${entityRegistry.getPathName(entityType)}`}>
<Card
style={{
padding: '30px 0px',
display: 'flex',
justifyContent: 'center',
}}
hoverable
>
<div style={{ fontSize: '18px', fontWeight: 'bold', color: '#0073b1' }}>
{entityRegistry.getCollectionName(entityType)}
</div>
</Card>
</Link>
</Col>
))}
</Row>
</Content>
</SearchablePage>
);
};

View File

@ -15,6 +15,21 @@ export enum PreviewType {
PREVIEW,
}
export enum IconStyleType {
/**
* Colored Icon
*/
HIGHLIGHT,
/**
* Grayed out icon
*/
ACCENT,
/**
* Rendered in a Tab pane header
*/
TAB_VIEW,
}
/**
* Base interface used for authoring DataHub Entities on the client side.
*
@ -26,6 +41,12 @@ export interface Entity<T> {
*/
type: EntityType;
/**
* Ant-design icon associated with the Entity. For a list of all candidate icons, see
* https://ant.design/components/icon/
*/
icon: (fontSize: number, styleType: IconStyleType) => JSX.Element;
/**
* Returns whether the entity search is enabled
*/

View File

@ -1,5 +1,5 @@
import { EntityType } from '../../types.generated';
import { Entity, PreviewType } from './Entity';
import { Entity, IconStyleType, PreviewType } from './Entity';
function validatedGet<K, V>(key: K, map: Map<K, V>): V {
if (map.has(key)) {
@ -43,6 +43,11 @@ export default class EntityRegistry {
return this.entities.filter((entity) => entity.isBrowseEnabled()).map((entity) => entity.type);
}
getIcon(type: EntityType, fontSize: number, styleType: IconStyleType): JSX.Element {
const entity = validatedGet(type, this.entityTypeToEntity);
return entity.icon(fontSize, styleType);
}
getCollectionName(type: EntityType): string {
const entity = validatedGet(type, this.entityTypeToEntity);
return entity.getCollectionName();

View File

@ -1,6 +1,7 @@
import { LineChartOutlined } from '@ant-design/icons';
import * as React from 'react';
import { Chart, EntityType } from '../../../types.generated';
import { Entity, PreviewType } from '../Entity';
import { Entity, IconStyleType, PreviewType } from '../Entity';
import { ChartPreview } from './preview/ChartPreview';
import ChartProfile from './profile/ChartProfile';
@ -10,6 +11,25 @@ import ChartProfile from './profile/ChartProfile';
export class ChartEntity implements Entity<Chart> {
type: EntityType = EntityType.Chart;
icon = (fontSize: number, styleType: IconStyleType) => {
if (styleType === IconStyleType.TAB_VIEW) {
return <LineChartOutlined style={{ fontSize }} />;
}
if (styleType === IconStyleType.HIGHLIGHT) {
return <LineChartOutlined style={{ fontSize, color: 'rgb(144 163 236)' }} />;
}
return (
<LineChartOutlined
style={{
fontSize,
color: '#BFBFBF',
}}
/>
);
};
isSearchEnabled = () => true;
isBrowseEnabled = () => false;
@ -29,6 +49,8 @@ export class ChartEntity implements Entity<Chart> {
platform={data.tool}
name={data.info?.name}
description={data.info?.description}
access={data.info?.access}
owners={data.ownership?.owners}
/>
);
};

View File

@ -0,0 +1,23 @@
import lookerLogo from '../../../images/lookerlogo.png';
import hdfsLogo from '../../../images/hadooplogo.png';
import kafkaLogo from '../../../images/kafkalogo.png';
import hiveLogo from '../../../images/hivelogo.png';
/**
* TODO: This is a temporary solution, until the backend can push logos for all data platform types.
*/
export function getLogoFromPlatform(platform: string) {
if (platform === 'Looker') {
return lookerLogo;
}
if (platform === 'hdfs') {
return hdfsLogo;
}
if (platform === 'kafka') {
return kafkaLogo;
}
if (platform === 'hive') {
return hiveLogo;
}
return undefined;
}

View File

@ -1,40 +1,45 @@
import React from 'react';
import { EntityType } from '../../../../types.generated';
import { AccessLevel, EntityType, Owner } from '../../../../types.generated';
import DefaultPreviewCard from '../../../preview/DefaultPreviewCard';
import { useEntityRegistry } from '../../../useEntityRegistry';
import { getLogoFromPlatform } from '../getLogoFromPlatform';
export const ChartPreview = ({
urn,
name,
description,
platform,
access,
owners,
}: {
urn: string;
platform: string;
name?: string;
description?: string | null;
access?: AccessLevel | null;
owners?: Array<Owner> | null;
}): JSX.Element => {
const entityRegistry = useEntityRegistry();
return (
<DefaultPreviewCard
url={`/${entityRegistry.getPathName(EntityType.Chart)}/${urn}`}
title={<div style={{ margin: '5px 0px 5px 2px', fontSize: '20px', fontWeight: 'bold' }}>{name}</div>}
>
<>
<div style={{ margin: '0px 0px 15px 0px' }}>{description}</div>
<div
style={{
width: '150px',
margin: '5px 0px 5px 0px',
display: 'flex',
justifyContent: 'space-between',
}}
>
<b style={{ justifySelf: 'left' }}>Platform</b>
<div style={{ justifySelf: 'right' }}>{platform}</div>
</div>
</>
</DefaultPreviewCard>
name={name || ''}
description={description || ''}
type="Chart"
logoUrl={getLogoFromPlatform(platform) || ''}
platform={platform}
qualifier={access}
tags={[]}
owners={
owners?.map((owner) => {
return {
urn: owner.owner.urn,
name: owner.owner.info?.fullName || '',
photoUrl: owner.owner.editableInfo?.pictureLink || '',
};
}) || []
}
/>
);
};

View File

@ -1,10 +1,17 @@
import { Avatar, Button, Row, Space, Tooltip, Typography } from 'antd';
import { Avatar, Button, Divider, Row, Space, Tooltip, Typography } from 'antd';
import React from 'react';
import { Link } from 'react-router-dom';
import { AuditStamp, EntityType, Ownership } from '../../../../types.generated';
import { useEntityRegistry } from '../../../useEntityRegistry';
import defaultAvatar from '../../../../images/default_avatar.png';
const styles = {
content: { width: '100%' },
typeLabel: { color: 'rgba(0, 0, 0, 0.45)' },
platformLabel: { color: 'rgba(0, 0, 0, 0.45)' },
lastUpdatedLabel: { color: 'rgba(0, 0, 0, 0.45)' },
};
export type Props = {
platform: string;
description?: string;
@ -17,37 +24,31 @@ export default function ChartHeader({ platform, description, ownership, url, las
const entityRegistry = useEntityRegistry();
return (
<>
<Space direction="vertical" size={15} style={styles.content}>
<Row justify="space-between">
<Typography.Title level={5} style={{ color: 'gray' }}>
{platform}
</Typography.Title>
<Space split={<Divider type="vertical" />}>
<Typography.Text style={styles.typeLabel}>Chart</Typography.Text>
<Typography.Text strong style={styles.platformLabel}>
{platform}
</Typography.Text>
</Space>
{url && <Button href={url}>View in {platform}</Button>}
</Row>
<Typography.Paragraph>{description}</Typography.Paragraph>
<Space direction="vertical">
<Avatar.Group maxCount={6} size="large">
{ownership &&
ownership.owners &&
ownership.owners.map((owner: any) => (
<Tooltip title={owner.owner.info?.fullName}>
<Link to={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${owner.owner.urn}`}>
<Avatar
style={{
color: '#f56a00',
backgroundColor: '#fde3cf',
}}
src={
(owner.owner.editableInfo && owner.owner.editableInfo.pictureLink) ||
defaultAvatar
}
/>
</Link>
</Tooltip>
))}
</Avatar.Group>
{lastModified && <div>Last modified at {new Date(lastModified.time).toLocaleDateString('en-US')}</div>}
</Space>
</>
<Avatar.Group maxCount={6} size="large">
{ownership?.owners?.map((owner: any) => (
<Tooltip title={owner.owner.info?.fullName}>
<Link to={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${owner.owner.urn}`}>
<Avatar src={owner.owner.editableInfo.pictureLink || defaultAvatar} />
</Link>
</Tooltip>
))}
</Avatar.Group>
{lastModified && (
<Typography.Text style={styles.lastUpdatedLabel}>
Last modified at {new Date(lastModified.time).toLocaleDateString('en-US')}
</Typography.Text>
)}
</Space>
);
}

View File

@ -7,6 +7,7 @@ import { EntityProfile } from '../../../shared/EntityProfile';
import ChartHeader from './ChartHeader';
import { useGetChartQuery } from '../../../../graphql/chart.generated';
import ChartSources from './ChartSources';
import { Message } from '../../../shared/Message';
const PageContainer = styled.div`
background-color: white;
@ -23,10 +24,6 @@ const ENABLED_TAB_TYPES = [TabType.Ownership, TabType.Sources];
export default function ChartProfile({ urn }: { urn: string }) {
const { loading, error, data } = useGetChartQuery({ variables: { urn } });
if (loading) {
return <Alert type="info" message="Loading" />;
}
if (error || (!loading && !error && !data)) {
return <Alert type="error" message={error?.message || 'Entity failed to load'} />;
}
@ -65,6 +62,7 @@ export default function ChartProfile({ urn }: { urn: string }) {
return (
<PageContainer>
<>
{loading && <Message type="loading" content="Loading..." style={{ marginTop: '10%' }} />}
{data && data.chart && (
<EntityProfile
title={data.chart.info?.name || ''}

View File

@ -1,26 +1,31 @@
import { List, Space, Typography } from 'antd';
import { List, Typography } from 'antd';
import React from 'react';
import { Dataset, EntityType } from '../../../../types.generated';
import { useEntityRegistry } from '../../../useEntityRegistry';
import { PreviewType } from '../../Entity';
const styles = {
list: { marginTop: '12px', padding: '16px 32px' },
item: { paddingTop: '20px' },
};
export type Props = {
datasets: Array<Dataset>;
};
export default function ChartSources({ datasets }: Props) {
const entityRegistry = useEntityRegistry();
return (
<Space direction="vertical" style={{ width: '100%' }} size="large">
<List
bordered
dataSource={datasets}
header={<Typography.Title level={3}>Source Datasets</Typography.Title>}
renderItem={(item) => (
<List.Item>{entityRegistry.renderPreview(EntityType.Dataset, PreviewType.PREVIEW, item)}</List.Item>
)}
/>
</Space>
<List
style={styles.list}
bordered
dataSource={datasets}
header={<Typography.Title level={3}>Source Datasets</Typography.Title>}
renderItem={(item) => (
<List.Item style={styles.item}>
{entityRegistry.renderPreview(EntityType.Dataset, PreviewType.PREVIEW, item)}
</List.Item>
)}
/>
);
}

View File

@ -1,6 +1,7 @@
import { DashboardFilled, DashboardOutlined } from '@ant-design/icons';
import * as React from 'react';
import { Dashboard, EntityType } from '../../../types.generated';
import { Entity, PreviewType } from '../Entity';
import { Entity, IconStyleType, PreviewType } from '../Entity';
import { DashboardPreview } from './preview/DashboardPreview';
import DashboardProfile from './profile/DashboardProfile';
@ -10,6 +11,25 @@ import DashboardProfile from './profile/DashboardProfile';
export class DashboardEntity implements Entity<Dashboard> {
type: EntityType = EntityType.Dashboard;
icon = (fontSize: number, styleType: IconStyleType) => {
if (styleType === IconStyleType.TAB_VIEW) {
return <DashboardOutlined style={{ fontSize }} />;
}
if (styleType === IconStyleType.HIGHLIGHT) {
return <DashboardFilled style={{ fontSize, color: 'rgb(144 163 236)' }} />;
}
return (
<DashboardOutlined
style={{
fontSize,
color: '#BFBFBF',
}}
/>
);
};
isSearchEnabled = () => true;
isBrowseEnabled = () => false;
@ -29,6 +49,8 @@ export class DashboardEntity implements Entity<Dashboard> {
platform={data.tool}
name={data.info?.name}
description={data.info?.description}
access={data.info?.access}
owners={data.ownership?.owners}
/>
);
};

View File

@ -1,40 +1,45 @@
import React from 'react';
import { EntityType } from '../../../../types.generated';
import { AccessLevel, EntityType, Owner } from '../../../../types.generated';
import DefaultPreviewCard from '../../../preview/DefaultPreviewCard';
import { useEntityRegistry } from '../../../useEntityRegistry';
import { getLogoFromPlatform } from '../../chart/getLogoFromPlatform';
export const DashboardPreview = ({
urn,
name,
description,
platform,
access,
owners,
}: {
urn: string;
platform: string;
name?: string;
description?: string | null;
access?: AccessLevel | null;
owners?: Array<Owner> | null;
}): JSX.Element => {
const entityRegistry = useEntityRegistry();
return (
<DefaultPreviewCard
url={`/${entityRegistry.getPathName(EntityType.Dashboard)}/${urn}`}
title={<div style={{ margin: '5px 0px 5px 2px', fontSize: '20px', fontWeight: 'bold' }}>{name}</div>}
>
<>
<div style={{ margin: '0px 0px 15px 0px' }}>{description}</div>
<div
style={{
width: '150px',
margin: '5px 0px 5px 0px',
display: 'flex',
justifyContent: 'space-between',
}}
>
<b style={{ justifySelf: 'left' }}>Platform</b>
<div style={{ justifySelf: 'right' }}>{platform}</div>
</div>
</>
</DefaultPreviewCard>
name={name || ''}
description={description || ''}
type="Dashboard"
logoUrl={getLogoFromPlatform(platform) || ''}
platform={platform}
qualifier={access}
tags={[]}
owners={
owners?.map((owner) => {
return {
urn: owner.owner.urn,
name: owner.owner.info?.fullName || '',
photoUrl: owner.owner.editableInfo?.pictureLink || '',
};
}) || []
}
/>
);
};

View File

@ -1,9 +1,14 @@
import { List, Space, Typography } from 'antd';
import { List, Typography } from 'antd';
import React from 'react';
import { Chart, EntityType } from '../../../../types.generated';
import { useEntityRegistry } from '../../../useEntityRegistry';
import { PreviewType } from '../../Entity';
const styles = {
list: { marginTop: '12px', padding: '16px 32px' },
item: { paddingTop: '20px' },
};
export type Props = {
charts: Array<Chart>;
};
@ -12,15 +17,16 @@ export default function DashboardCharts({ charts }: Props) {
const entityRegistry = useEntityRegistry();
return (
<Space direction="vertical" style={{ width: '100%' }} size="large">
<List
bordered
dataSource={charts}
header={<Typography.Title level={3}>Charts</Typography.Title>}
renderItem={(item) => (
<List.Item>{entityRegistry.renderPreview(EntityType.Chart, PreviewType.PREVIEW, item)}</List.Item>
)}
/>
</Space>
<List
style={styles.list}
bordered
dataSource={charts}
header={<Typography.Title level={3}>Charts</Typography.Title>}
renderItem={(item) => (
<List.Item style={styles.item}>
{entityRegistry.renderPreview(EntityType.Chart, PreviewType.PREVIEW, item)}
</List.Item>
)}
/>
);
}

View File

@ -1,10 +1,17 @@
import { Avatar, Button, Row, Space, Tooltip, Typography } from 'antd';
import { Avatar, Button, Divider, Row, Space, Tooltip, Typography } from 'antd';
import React from 'react';
import { Link } from 'react-router-dom';
import { AuditStamp, EntityType, Ownership } from '../../../../types.generated';
import { useEntityRegistry } from '../../../useEntityRegistry';
import defaultAvatar from '../../../../images/default_avatar.png';
const styles = {
content: { width: '100%' },
typeLabel: { color: 'rgba(0, 0, 0, 0.45)' },
platformLabel: { color: 'rgba(0, 0, 0, 0.45)' },
lastUpdatedLabel: { color: 'rgba(0, 0, 0, 0.45)' },
};
export type Props = {
platform: string;
description?: string;
@ -16,37 +23,31 @@ export type Props = {
export default function DashboardHeader({ platform, description, ownership, url, lastModified }: Props) {
const entityRegistry = useEntityRegistry();
return (
<>
<Space direction="vertical" size={16} style={styles.content}>
<Row justify="space-between">
<Typography.Title level={5} style={{ color: 'gray' }}>
{platform}
</Typography.Title>
<Space split={<Divider type="vertical" />}>
<Typography.Text style={styles.typeLabel}>Dashboard</Typography.Text>
<Typography.Text strong style={styles.platformLabel}>
{platform}
</Typography.Text>
</Space>
{url && <Button href={url}>View in {platform}</Button>}
</Row>
<Typography.Paragraph>{description}</Typography.Paragraph>
<Space direction="vertical">
<Avatar.Group maxCount={6} size="large">
{ownership &&
ownership.owners &&
ownership.owners.map((owner: any) => (
<Tooltip title={owner.owner.info?.fullName}>
<Link to={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${owner.owner.urn}`}>
<Avatar
style={{
color: '#f56a00',
backgroundColor: '#fde3cf',
}}
src={
(owner.owner.editableInfo && owner.owner.editableInfo.pictureLink) ||
defaultAvatar
}
/>
</Link>
</Tooltip>
))}
</Avatar.Group>
{lastModified && <div>Last modified at {new Date(lastModified.time).toLocaleDateString('en-US')}</div>}
</Space>
</>
<Avatar.Group maxCount={6} size="large">
{ownership?.owners?.map((owner: any) => (
<Tooltip title={owner.owner.info?.fullName}>
<Link to={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${owner.owner.urn}`}>
<Avatar src={owner.owner.editableInfo.pictureLink || defaultAvatar} />
</Link>
</Tooltip>
))}
</Avatar.Group>
{lastModified && (
<Typography.Text style={styles.lastUpdatedLabel}>
Last modified at {new Date(lastModified.time).toLocaleDateString('en-US')}
</Typography.Text>
)}
</Space>
);
}

View File

@ -7,6 +7,7 @@ import { Ownership as OwnershipView } from '../../shared/Ownership';
import { EntityProfile } from '../../../shared/EntityProfile';
import DashboardHeader from './DashboardHeader';
import DashboardCharts from './DashboardCharts';
import { Message } from '../../../shared/Message';
const PageContainer = styled.div`
background-color: white;
@ -26,10 +27,6 @@ const ENABLED_TAB_TYPES = [TabType.Ownership, TabType.Charts];
export default function DashboardProfile({ urn }: { urn: string }) {
const { loading, error, data } = useGetDashboardQuery({ variables: { urn } });
if (loading) {
return <Alert type="info" message="Loading" />;
}
if (error || (!loading && !error && !data)) {
return <Alert type="error" message={error?.message || 'Entity failed to load'} />;
}
@ -68,6 +65,7 @@ export default function DashboardProfile({ urn }: { urn: string }) {
return (
<PageContainer>
<>
{loading && <Message type="loading" content="Loading..." style={{ marginTop: '10%' }} />}
{data && data.dashboard && (
<EntityProfile
title={data.dashboard.info?.name || ''}

View File

@ -1,7 +1,8 @@
import * as React from 'react';
import { DatabaseFilled, DatabaseOutlined } from '@ant-design/icons';
import { Dataset, EntityType } from '../../../types.generated';
import { Profile } from './profile/Profile';
import { Entity, PreviewType } from '../Entity';
import { Entity, IconStyleType, PreviewType } from '../Entity';
import { Preview } from './preview/Preview';
/**
@ -10,6 +11,25 @@ import { Preview } from './preview/Preview';
export class DatasetEntity implements Entity<Dataset> {
type: EntityType = EntityType.Dataset;
icon = (fontSize: number, styleType: IconStyleType) => {
if (styleType === IconStyleType.TAB_VIEW) {
return <DatabaseOutlined style={{ fontSize }} />;
}
if (styleType === IconStyleType.HIGHLIGHT) {
return <DatabaseFilled style={{ fontSize, color: '#B37FEB' }} />;
}
return (
<DatabaseFilled
style={{
fontSize,
color: '#BFBFBF',
}}
/>
);
};
isSearchEnabled = () => true;
isBrowseEnabled = () => true;
@ -23,13 +43,16 @@ export class DatasetEntity implements Entity<Dataset> {
renderProfile = (urn: string) => <Profile urn={urn} />;
renderPreview = (_: PreviewType, data: Dataset) => {
console.log(data);
return (
<Preview
urn={data.urn}
name={data.name}
origin={data.origin}
description={data.description}
platformNativeType={data.platformNativeType}
platformName={data.platform.name}
tags={data.tags}
owners={data.ownership?.owners}
/>
);
};

View File

@ -1,54 +1,47 @@
import React from 'react';
import { EntityType, FabricType, PlatformNativeType } from '../../../../types.generated';
import { EntityType, FabricType, Owner } from '../../../../types.generated';
import DefaultPreviewCard from '../../../preview/DefaultPreviewCard';
import { useEntityRegistry } from '../../../useEntityRegistry';
import { getLogoFromPlatform } from '../../chart/getLogoFromPlatform';
export const Preview = ({
urn,
name,
origin,
description,
platformNativeType,
platformName,
tags,
owners,
}: {
urn: string;
name: string;
origin: FabricType;
description?: string | null;
platformNativeType?: PlatformNativeType | null;
platformName: string;
tags: Array<string>;
owners?: Array<Owner> | null;
}): JSX.Element => {
const entityRegistry = useEntityRegistry();
// TODO: Should we rename the search result card?
return (
<DefaultPreviewCard
url={`/${entityRegistry.getPathName(EntityType.Dataset)}/${urn}`}
title={<div style={{ margin: '5px 0px 5px 2px', fontSize: '20px', fontWeight: 'bold' }}>{name}</div>}
>
<>
<div style={{ margin: '0px 0px 15px 0px' }}>{description}</div>
<div
style={{
width: '150px',
margin: '5px 0px 5px 0px',
display: 'flex',
justifyContent: 'space-between',
}}
>
<b style={{ justifySelf: 'left' }}>Data Origin</b>
<div style={{ justifySelf: 'right' }}>{origin}</div>
</div>
<div
style={{
width: '150px',
margin: '5px 0px 5px 0px',
display: 'flex',
justifyContent: 'space-between',
}}
>
<b>Platform</b>
<div>{platformNativeType}</div>
</div>
</>
</DefaultPreviewCard>
name={name || ''}
description={description || ''}
type="Dataset"
logoUrl={getLogoFromPlatform(platformName) || ''}
platform={platformName}
qualifier={origin}
tags={tags}
owners={
owners?.map((owner) => {
return {
urn: owner.owner.urn,
name: owner.owner.info?.fullName || '',
photoUrl: owner.owner.editableInfo?.pictureLink || '',
};
}) || []
}
/>
);
};

View File

@ -1,4 +1,4 @@
import { Avatar, Badge, Popover, Space, Tooltip, Typography } from 'antd';
import { Avatar, Badge, Divider, Popover, Space, Tooltip, Typography } from 'antd';
import React from 'react';
import { Link } from 'react-router-dom';
import { Dataset, EntityType } from '../../../../types.generated';
@ -9,32 +9,36 @@ export type Props = {
dataset: Dataset;
};
export default function DatasetHeader({ dataset: { description, ownership, deprecation } }: Props) {
export default function DatasetHeader({ dataset: { description, ownership, deprecation, platform } }: Props) {
const entityRegistry = useEntityRegistry();
return (
<>
<Typography.Paragraph>{description}</Typography.Paragraph>
<Space direction="vertical">
<Space direction="vertical" size="middle">
<Space split={<Divider type="vertical" />}>
<Typography.Text style={{ color: 'grey' }}>Dataset</Typography.Text>
<Typography.Text strong style={{ color: '#214F55' }}>
{platform.name}
</Typography.Text>
</Space>
<Typography.Paragraph>{description}</Typography.Paragraph>
<Avatar.Group maxCount={6} size="large">
{ownership &&
ownership.owners &&
ownership.owners.map((owner: any) => (
<Tooltip title={owner.owner.info?.fullName}>
<Link to={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${owner.owner.urn}`}>
<Avatar
style={{
color: '#f56a00',
backgroundColor: '#fde3cf',
}}
src={
(owner.owner.editableInfo && owner.owner.editableInfo.pictureLink) ||
defaultAvatar
}
/>
</Link>
</Tooltip>
))}
{ownership?.owners?.map((owner: any) => (
<Tooltip title={owner.owner.info?.fullName}>
<Link to={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${owner.owner.urn}`}>
<Avatar
style={{
color: '#f56a00',
backgroundColor: '#fde3cf',
}}
src={
(owner.owner.editableInfo && owner.owner.editableInfo.pictureLink) ||
defaultAvatar
}
/>
</Link>
</Tooltip>
))}
</Avatar.Group>
<div>
{deprecation?.deprecated && (

View File

@ -15,21 +15,27 @@ export default function Lineage({ upstreamLineage, downstreamLineage }: Props) {
const downstreamEntities = downstreamLineage?.downstreams.map((downstream) => downstream.dataset);
return (
<Space direction="vertical" style={{ width: '100%' }} size="large">
<Space direction="vertical" style={{ width: '100%', margin: '24px' }} size="large">
<List
style={{ marginTop: '12px', padding: '16px 32px' }}
bordered
dataSource={upstreamEntities}
header={<Typography.Title level={3}>Upstream</Typography.Title>}
renderItem={(item) => (
<List.Item>{entityRegistry.renderPreview(EntityType.Dataset, PreviewType.PREVIEW, item)}</List.Item>
<List.Item style={{ paddingTop: '20px' }}>
{entityRegistry.renderPreview(EntityType.Dataset, PreviewType.PREVIEW, item)}
</List.Item>
)}
/>
<List
style={{ marginTop: '12px', padding: '16px 32px' }}
bordered
dataSource={downstreamEntities}
header={<Typography.Title level={3}>Downstream</Typography.Title>}
renderItem={(item) => (
<List.Item>{entityRegistry.renderPreview(EntityType.Dataset, PreviewType.PREVIEW, item)}</List.Item>
<List.Item style={{ paddingTop: '20px' }}>
{entityRegistry.renderPreview(EntityType.Dataset, PreviewType.PREVIEW, item)}
</List.Item>
)}
/>
</Space>

View File

@ -9,6 +9,7 @@ import LineageView from './Lineage';
import PropertiesView from './Properties';
import DocumentsView from './Documentation';
import DatasetHeader from './DatasetHeader';
import { Message } from '../../../shared/Message';
export enum TabType {
Ownership = 'Ownership',
@ -28,10 +29,6 @@ export const Profile = ({ urn }: { urn: string }): JSX.Element => {
const { loading, error, data } = useGetDatasetQuery({ variables: { urn } });
const [updateDataset] = useUpdateDatasetMutation();
if (loading) {
return <Alert type="info" message="Loading" />;
}
if (error || (!loading && !error && !data)) {
return <Alert type="error" message={error?.message || 'Entity failed to load'} />;
}
@ -92,6 +89,7 @@ export const Profile = ({ urn }: { urn: string }): JSX.Element => {
return (
<>
{loading && <Message type="loading" content="Loading..." style={{ marginTop: '10%' }} />}
{data && data.dataset && (
<EntityProfile
title={data.dataset.name}

View File

@ -1,6 +1,7 @@
import { UserOutlined } from '@ant-design/icons';
import * as React from 'react';
import { CorpUser, EntityType } from '../../../types.generated';
import { Entity, PreviewType } from '../Entity';
import { Entity, IconStyleType, PreviewType } from '../Entity';
import { Preview } from './preview/Preview';
import UserProfile from './UserProfile';
@ -10,6 +11,25 @@ import UserProfile from './UserProfile';
export class UserEntity implements Entity<CorpUser> {
type: EntityType = EntityType.CorpUser;
icon = (fontSize: number, styleType: IconStyleType) => {
if (styleType === IconStyleType.TAB_VIEW) {
return <UserOutlined style={{ fontSize }} />;
}
if (styleType === IconStyleType.HIGHLIGHT) {
return <UserOutlined style={{ fontSize, color: 'rgb(144 163 236)' }} />;
}
return (
<UserOutlined
style={{
fontSize,
color: '#BFBFBF',
}}
/>
);
};
isSearchEnabled = () => true;
isBrowseEnabled = () => false;

View File

@ -12,12 +12,20 @@ const ownerships = {
origin: 'PROD',
description: 'this is a dataset',
platformNativeType: PlatformNativeType.Table,
platform: {
name: 'hive',
},
tags: [],
},
{
name: 'KafkaDataset',
origin: 'PROD',
description: 'this is also a dataset',
platformNativeType: PlatformNativeType.Table,
platform: {
name: 'kafka',
},
tags: [],
},
],
};

View File

@ -11,12 +11,20 @@ const ownerships = {
origin: 'PROD',
description: 'this is a dataset',
platformNativeType: PlatformNativeType.Table,
platform: {
name: 'hive',
},
tags: [],
},
{
name: 'KafkaDataset',
origin: 'PROD',
description: 'this is also a dataset',
platformNativeType: PlatformNativeType.Table,
platform: {
name: 'kafka',
},
tags: [],
},
],
};

View File

@ -1,10 +1,15 @@
import React from 'react';
import { Avatar, Space, Typography } from 'antd';
import { Link } from 'react-router-dom';
import { EntityType } from '../../../../types.generated';
import DefaultPreviewCard from '../../../preview/DefaultPreviewCard';
import { useEntityRegistry } from '../../../useEntityRegistry';
import defaultAvatar from '../../../../images/default_avatar.png';
const styles = {
name: { margin: 0, color: '#0073b1' },
title: { color: 'rgba(0, 0, 0, 0.45)' },
};
export const Preview = ({
urn,
name,
@ -19,19 +24,16 @@ export const Preview = ({
const entityRegistry = useEntityRegistry();
return (
<DefaultPreviewCard
url={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${urn}`}
title={
<Space size="large">
<Avatar size="large" src={photoUrl || defaultAvatar} />
<Space direction="vertical" size={4}>
<Typography.Title style={{ margin: '0', color: '#0073b1' }} level={3}>
{name}
</Typography.Title>
<Typography.Text style={{ color: '#gray' }}>{title}</Typography.Text>
</Space>
<Link to={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${urn}`}>
<Space size={28}>
<Avatar size={60} src={photoUrl || defaultAvatar} />
<Space direction="vertical" size={4}>
<Typography.Title style={styles.name} level={3}>
{name}
</Typography.Title>
<Typography.Text style={styles.title}>{title}</Typography.Text>
</Space>
}
/>
</Space>
</Link>
);
};

View File

@ -0,0 +1,12 @@
import React from 'react';
import { HomePageHeader } from './HomePageHeader';
import { HomePageBody } from './HomePageBody';
export const HomePage = () => {
return (
<>
<HomePageHeader />
<HomePageBody />
</>
);
};

View File

@ -0,0 +1,32 @@
import React from 'react';
import { Typography, Row, Col } from 'antd';
import { useEntityRegistry } from '../useEntityRegistry';
import { BrowseEntityCard } from '../search/BrowseEntityCard';
const styles = {
title: {
margin: '0px 0px 0px 120px',
fontSize: 32,
},
entityGrid: {
padding: '40px 100px',
},
};
export const HomePageBody = () => {
const entityRegistry = useEntityRegistry();
return (
<>
<Typography.Text style={styles.title}>
<b>Explore</b> your data
</Typography.Text>
<Row gutter={[16, 16]} style={styles.entityGrid}>
{entityRegistry.getBrowseEntityTypes().map((entityType) => (
<Col xs={24} sm={24} md={8}>
<BrowseEntityCard entityType={entityType} />
</Col>
))}
</Row>
</>
);
};

View File

@ -0,0 +1,82 @@
import React from 'react';
import { useHistory } from 'react-router';
import { Typography, Image, Space, AutoComplete, Input, Row } from 'antd';
import { ManageAccount } from '../shared/ManageAccount';
import { useGetAuthenticatedUser } from '../useGetAuthenticatedUser';
import { GlobalCfg, SearchCfg } from '../../conf';
import { useEntityRegistry } from '../useEntityRegistry';
import { navigateToSearchUrl } from '../search/utils/navigateToSearchUrl';
import { useGetAutoCompleteResultsLazyQuery } from '../../graphql/search.generated';
const styles = {
background: {
width: '100%',
backgroundImage: 'linear-gradient(#132935, #FFFFFF)',
},
navBar: { padding: '24px' },
welcomeText: { color: '#FFFFFF', fontSize: 16 },
searchContainer: { width: '100%', marginTop: '40px', marginBottom: '160px' },
logoImage: { width: 140 },
searchBox: { width: 540, margin: '40px 0px' },
subHeaderText: { color: '#FFFFFF', fontSize: 20 },
};
export const HomePageHeader = () => {
const history = useHistory();
const entityRegistry = useEntityRegistry();
const { data } = useGetAuthenticatedUser();
const [getAutoCompleteResults, { data: suggestionsData }] = useGetAutoCompleteResultsLazyQuery();
const onSearch = (query: string) => {
navigateToSearchUrl({
query,
history,
entityRegistry,
});
};
const onAutoComplete = (query: string) => {
getAutoCompleteResults({
variables: {
input: {
type: entityRegistry.getDefaultSearchEntityType(),
query,
},
},
});
};
return (
<Space direction="vertical" style={styles.background}>
<Row justify="space-between" style={styles.navBar}>
<Typography.Text style={styles.welcomeText}>
Welcome back, <b>{data?.corpUser?.info?.firstName || data?.corpUser?.username}</b>.
</Typography.Text>
<ManageAccount
urn={data?.corpUser?.urn || ''}
pictureLink={data?.corpUser?.editableInfo?.pictureLink || ''}
/>
</Row>
<Space direction="vertical" align="center" style={styles.searchContainer}>
<Image src={GlobalCfg.LOGO_IMAGE} preview={false} style={styles.logoImage} />
<AutoComplete
style={styles.searchBox}
options={suggestionsData?.autoComplete?.suggestions.map((result: string) => ({
value: result,
}))}
onSelect={(value: string) => onSearch(value)}
onSearch={(value: string) => onAutoComplete(value)}
>
<Input.Search
placeholder={SearchCfg.SEARCH_BAR_PLACEHOLDER_TEXT}
onSearch={(value: string) => onSearch(value)}
/>
</AutoComplete>
<Typography.Text style={styles.subHeaderText}>
Find <b>data</b> you can count on.
</Typography.Text>
</Space>
</Space>
);
};

View File

@ -1,21 +1,88 @@
import { Space } from 'antd';
import { Avatar, Divider, Image, Row, Space, Tag, Tooltip, Typography } from 'antd';
import React from 'react';
import { Link } from 'react-router-dom';
import { EntityType } from '../../types.generated';
import defaultAvatar from '../../images/default_avatar.png';
import { useEntityRegistry } from '../useEntityRegistry';
interface Props extends React.PropsWithChildren<any> {
title?: React.ReactNode;
interface Props {
name: string;
logoUrl?: string;
url: string;
description: string;
type: string;
platform: string;
qualifier?: string | null;
tags: Array<string>;
owners: Array<{ urn: string; name?: string; photoUrl?: string }>;
}
export default function DefaultPreviewCard({ title, url, children }: Props) {
const styles = {
row: { width: '100%' },
leftColumn: { maxWidth: '75%' },
rightColumn: { maxWidth: '25%' },
logoImage: { width: '48px' },
name: { color: '#214F55', fontSize: '18px' },
typeName: { color: '#585858' },
platformName: { color: '#585858' },
ownedBy: { color: '#585858' },
};
export default function DefaultPreviewCard({
name,
logoUrl,
url,
description,
type,
platform,
qualifier,
tags,
owners,
}: Props) {
const entityRegistry = useEntityRegistry();
return (
<Space direction="vertical" style={{ width: '100%' }}>
<div style={{ padding: '8px' }}>
<Link to={url} style={{ color: '#0073b1' }} type="link">
{title}
<Row style={styles.row} justify="space-between">
<Space direction="vertical" align="start" size={28} style={styles.leftColumn}>
<Link to={url}>
<Space direction="horizontal" size={28} align="center">
{logoUrl && <Image style={styles.logoImage} src={logoUrl} preview />}
<Space direction="vertical" size={8}>
<Typography.Text strong style={styles.name}>
{name}
</Typography.Text>
<Space split={<Divider type="vertical" />} size={16}>
<Typography.Text style={styles.typeName}>{type}</Typography.Text>
<Typography.Text style={styles.platformName} strong>
{platform}
</Typography.Text>
<Tag>{qualifier}</Tag>
</Space>
</Space>
</Space>
</Link>
{children}
</div>
</Space>
<Typography.Paragraph>{description}</Typography.Paragraph>
</Space>
<Space direction="vertical" align="end" size={36} style={styles.rightColumn}>
<Space>
{tags.map((tag) => (
<Tag color="processing">{tag}</Tag>
))}
</Space>
<Space direction="vertical" size={12}>
<Typography.Text strong style={styles.ownedBy}>
Owned By
</Typography.Text>
<Avatar.Group maxCount={4}>
{owners.map((owner) => (
<Tooltip title={owner.name}>
<Link to={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${owner.urn}`}>
<Avatar src={owner.photoUrl || defaultAvatar} />
</Link>
</Tooltip>
))}
</Avatar.Group>
</Space>
</Space>
</Row>
);
}

View File

@ -0,0 +1,34 @@
import React from 'react';
import 'antd/dist/antd.css';
import { Card, Typography, Row } from 'antd';
import { Link } from 'react-router-dom';
import '../../App.css';
import { useEntityRegistry } from '../useEntityRegistry';
import { PageRoutes } from '../../conf/Global';
import { IconStyleType } from '../entity/Entity';
import { EntityType } from '../../types.generated';
const styles = {
card: { width: 360 },
title: { margin: 0, color: '#525252' },
iconFlag: { right: '32px', top: '-28px' },
icon: { padding: '16px 24px' },
};
export const BrowseEntityCard = ({ entityType }: { entityType: EntityType }) => {
const entityRegistry = useEntityRegistry();
return (
<Link to={`${PageRoutes.BROWSE}/${entityRegistry.getPathName(entityType)}`}>
<Card hoverable>
<Row justify="space-between" align="middle" style={styles.card}>
<Typography.Title style={styles.title} level={4}>
{entityRegistry.getCollectionName(entityType)}
</Typography.Title>
<Card bodyStyle={styles.icon} style={{ ...styles.iconFlag, position: 'absolute' }}>
{entityRegistry.getIcon(entityType, 24, IconStyleType.HIGHLIGHT)}
</Card>
</Row>
</Card>
</Link>
);
};

View File

@ -12,15 +12,14 @@ import { useEntityRegistry } from '../useEntityRegistry';
import { FacetFilterInput } from '../../types.generated';
import useFilters from './utils/useFilters';
import { navigateToSearchUrl } from './utils/navigateToSearchUrl';
import { Message } from '../shared/Message';
type SearchPageParams = {
type?: string;
};
/**
* A dedicated search page.
*
* TODO: Read / write filter parameters from / to the URL query parameters.
* A search results page.
*/
export const SearchPage = () => {
const history = useHistory();
@ -51,10 +50,6 @@ export const SearchPage = () => {
},
});
if (loading) {
return <Alert type="info" message="Loading" />;
}
if (error || (!loading && !error && !data)) {
return <Alert type="error" message={error?.message || 'Entity failed to load'} />;
}
@ -79,7 +74,9 @@ export const SearchPage = () => {
const toSearchResults = (elements: any) => (
<List
dataSource={elements}
renderItem={(item) => <List.Item>{entityRegistry.renderSearchResult(activeType, item)}</List.Item>}
renderItem={(item) => (
<List.Item style={{ padding: 32 }}>{entityRegistry.renderSearchResult(activeType, item)}</List.Item>
)}
bordered
/>
);
@ -104,6 +101,7 @@ export const SearchPage = () => {
))}
</Tabs>
</Affix>
{loading && <Message type="loading" content="Loading..." style={{ marginTop: '10%' }} />}
<Row style={{ width: '80%', margin: 'auto auto', backgroundColor: 'white' }}>
<Col style={{ margin: '24px 0px 0px 0px', padding: '0px 16px' }} span={6}>
<SearchFilters

View File

@ -25,7 +25,7 @@ describe('SearchPage', () => {
</TestPageContainer>
</MockedProvider>,
);
expect(getByText('Loading')).toBeInTheDocument();
expect(getByText('Loading...')).toBeInTheDocument();
});
it('renders the selected filters as checked', async () => {
@ -87,9 +87,9 @@ describe('SearchPage', () => {
const hdfsPlatformBox = getByTestId('facet-platform-hdfs');
expect(hdfsPlatformBox).toHaveProperty('checked', false);
expect(queryByText('Loading')).not.toBeInTheDocument();
expect(queryByText('Loading...')).not.toBeInTheDocument();
fireEvent.click(hdfsPlatformBox);
expect(queryByText('Loading')).toBeInTheDocument();
expect(queryByText('Loading...')).toBeInTheDocument();
await waitFor(() => expect(queryByTestId('facet-platform-kafka')).toBeInTheDocument());

View File

@ -5,9 +5,10 @@ type MessageType = 'loading' | 'info' | 'error' | 'warning' | 'success';
export type MessageProps = {
type: MessageType;
content: ReactNode;
style?: React.CSSProperties;
};
export const Message = ({ type, content }: MessageProps): JSX.Element => {
export const Message = ({ type, content, style }: MessageProps): JSX.Element => {
const key = useMemo(() => {
// We don't actually care about cryptographic security, but instead
// just want something unique. That's why it's OK to use Math.random
@ -21,11 +22,12 @@ export const Message = ({ type, content }: MessageProps): JSX.Element => {
type,
content,
duration: 0,
style,
});
return () => {
hide();
};
}, [key, type, content]);
}, [key, type, content, style]);
return <></>;
};

View File

@ -13,7 +13,34 @@ query getBrowseResults($input: BrowseInput!) {
name
origin
description
platformNativeType
platform {
name
}
tags
ownership {
owners {
owner {
urn
type
username
info {
active
displayName
title
firstName
lastName
fullName
}
editableInfo {
pictureLink
}
}
type
}
lastModified {
time
}
}
}
}
start

View File

@ -12,6 +12,9 @@ query getChart($urn: String!) {
name
origin
description
platform {
name
}
platformNativeType
tags
lastModified {

View File

@ -5,6 +5,9 @@ fragment nonRecursiveDatasetFields on Dataset {
origin
description
uri
platform {
name
}
platformNativeType
tags
properties {
@ -69,13 +72,13 @@ fragment nonRecursiveDatasetFields on Dataset {
}
}
fields {
fieldPath
jsonPath
nullable
description
type
nativeDataType
recursive
fieldPath
jsonPath
nullable
description
type
nativeDataType
recursive
}
primaryKeys
}

View File

@ -18,6 +18,9 @@ query getSearchResults($input: SearchInput!) {
origin
description
uri
platform {
name
}
platformNativeType
tags
properties {
@ -87,12 +90,24 @@ query getSearchResults($input: SearchInput!) {
owners {
owner {
urn
type
username
info {
active
displayName
title
firstName
lastName
fullName
}
editableInfo {
pictureLink
}
}
type
source {
type
url
}
}
lastModified {
time
}
}
}
@ -115,12 +130,24 @@ query getSearchResults($input: SearchInput!) {
owners {
owner {
urn
type
username
info {
active
displayName
title
firstName
lastName
fullName
}
editableInfo {
pictureLink
}
}
type
source {
type
url
}
}
lastModified {
time
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB