feat(React UI): SearchPage and SearchResultsPage (#2130)
Co-authored-by: John Joyce <john@acryl.io>
@ -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,
|
||||
},
|
||||
],
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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 || '',
|
||||
};
|
||||
}) || []
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 || ''}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 || '',
|
||||
};
|
||||
}) || []
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 || ''}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 || '',
|
||||
};
|
||||
}) || []
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@ -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: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
12
datahub-web-react/src/app/home/HomePage.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { HomePageHeader } from './HomePageHeader';
|
||||
import { HomePageBody } from './HomePageBody';
|
||||
|
||||
export const HomePage = () => {
|
||||
return (
|
||||
<>
|
||||
<HomePageHeader />
|
||||
<HomePageBody />
|
||||
</>
|
||||
);
|
||||
};
|
||||
32
datahub-web-react/src/app/home/HomePageBody.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
82
datahub-web-react/src/app/home/HomePageHeader.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
34
datahub-web-react/src/app/search/BrowseEntityCard.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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
|
||||
|
||||
@ -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());
|
||||
|
||||
|
||||
@ -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 <></>;
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -12,6 +12,9 @@ query getChart($urn: String!) {
|
||||
name
|
||||
origin
|
||||
description
|
||||
platform {
|
||||
name
|
||||
}
|
||||
platformNativeType
|
||||
tags
|
||||
lastModified {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
datahub-web-react/src/images/defaultlogo.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
datahub-web-react/src/images/hadooplogo.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
datahub-web-react/src/images/hivelogo.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
datahub-web-react/src/images/kafkalogo.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
datahub-web-react/src/images/lookerlogo.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
datahub-web-react/src/images/mysqllogo.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
datahub-web-react/src/images/postgreslogo.png
Normal file
|
After Width: | Height: | Size: 120 KiB |