mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-24 08:28:12 +00:00
refactor(React Incubation): Entity Interface & EntityRegistry (#2077)
* React App: Entity Registry! (#15) Introducing the Entity Registry: an Entity-oriented Architecture Co-authored-by: John Joyce <john@acryl.io>
This commit is contained in:
parent
4cdef602b4
commit
9d38ae4dd4
@ -175,7 +175,7 @@ type Dataset {
|
||||
|
||||
name: String!
|
||||
|
||||
origin: FabricType
|
||||
origin: FabricType!
|
||||
|
||||
description: String
|
||||
|
||||
@ -282,7 +282,7 @@ type AggregationMetadata {
|
||||
input AutoCompleteInput {
|
||||
type: EntityType!
|
||||
query: String!
|
||||
field: String! # Field name
|
||||
field: String # Field name
|
||||
limit: Int
|
||||
filters: [FacetFilterInput!]
|
||||
}
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import './App.css';
|
||||
import { Routes } from './components/Routes';
|
||||
import { mocks } from './Mocks';
|
||||
import EntityRegistry from './components/entity/EntityRegistry';
|
||||
import { DatasetEntity } from './components/entity/dataset/DatasetEntity';
|
||||
import { UserEntity } from './components/entity/user/User';
|
||||
import { EntityRegistryContext } from './entityRegistryContext';
|
||||
|
||||
// Enable to use the Apollo MockProvider instead of a real HTTP client
|
||||
const MOCK_MODE = true;
|
||||
@ -25,18 +29,27 @@ const client = new ApolloClient({
|
||||
});
|
||||
|
||||
const App: React.VFC = () => {
|
||||
// TODO: Explore options to dynamically configure this.
|
||||
const entityRegistry = useMemo(() => {
|
||||
const register = new EntityRegistry();
|
||||
register.register(new DatasetEntity());
|
||||
register.register(new UserEntity());
|
||||
return register;
|
||||
}, []);
|
||||
return (
|
||||
<Router>
|
||||
{/* Temporary: For local testing during development. */}
|
||||
{MOCK_MODE ? (
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<Routes />
|
||||
</MockedProvider>
|
||||
) : (
|
||||
<ApolloProvider client={client}>
|
||||
<Routes />
|
||||
</ApolloProvider>
|
||||
)}
|
||||
<EntityRegistryContext.Provider value={entityRegistry}>
|
||||
{/* Temporary: For local testing during development. */}
|
||||
{MOCK_MODE ? (
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<Routes />
|
||||
</MockedProvider>
|
||||
) : (
|
||||
<ApolloProvider client={client}>
|
||||
<Routes />
|
||||
</ApolloProvider>
|
||||
)}
|
||||
</EntityRegistryContext.Provider>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
@ -304,7 +304,6 @@ export const mocks = [
|
||||
input: {
|
||||
type: 'DATASET',
|
||||
query: 't',
|
||||
field: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -324,7 +323,6 @@ export const mocks = [
|
||||
input: {
|
||||
type: 'USER',
|
||||
query: 'j',
|
||||
field: 'ldap',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -3,13 +3,13 @@ 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 { DatasetPage } from './entity/dataset/DatasetPage';
|
||||
import { UserPage } from './entity/user/UserPage';
|
||||
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';
|
||||
|
||||
const ProtectedRoute = ({
|
||||
isLoggedIn,
|
||||
@ -28,16 +28,22 @@ const ProtectedRoute = ({
|
||||
*/
|
||||
export const Routes = (): JSX.Element => {
|
||||
const isLoggedIn = useReactiveVar(isLoggedInVar);
|
||||
const entityRegistry = useEntityRegistry();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Switch>
|
||||
<ProtectedRoute isLoggedIn={isLoggedIn} exact path="/" render={() => <BrowseTypesPage />} />
|
||||
<Route path={PageRoutes.LOG_IN} component={LogIn} />
|
||||
<ProtectedRoute
|
||||
isLoggedIn={isLoggedIn}
|
||||
path={`${PageRoutes.DATASETS}/:urn`}
|
||||
render={() => <DatasetPage />}
|
||||
/>
|
||||
|
||||
{entityRegistry.getEntities().map((entity) => (
|
||||
<ProtectedRoute
|
||||
isLoggedIn={isLoggedIn}
|
||||
path={`/${entity.getPathName()}/:urn`}
|
||||
render={() => <EntityPage entityType={entity.type} />}
|
||||
/>
|
||||
))}
|
||||
|
||||
<ProtectedRoute isLoggedIn={isLoggedIn} path={PageRoutes.SEARCH} render={() => <SearchPage />} />
|
||||
<ProtectedRoute
|
||||
isLoggedIn={isLoggedIn}
|
||||
@ -55,7 +61,6 @@ export const Routes = (): JSX.Element => {
|
||||
path={PageRoutes.BROWSE_RESULTS}
|
||||
render={() => <BrowseResultsPage />}
|
||||
/>
|
||||
<ProtectedRoute isLoggedIn={isLoggedIn} path={PageRoutes.USERS} render={() => <UserPage />} />
|
||||
<Route component={NoPageFound} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
@ -7,7 +7,7 @@ import Cookies from 'js-cookie';
|
||||
import { Redirect } from 'react-router';
|
||||
import styles from './login.module.css';
|
||||
import { useLoginMutation } from '../../graphql/auth.generated';
|
||||
import { Message } from '../generic/Message';
|
||||
import { Message } from '../shared/Message';
|
||||
import { isLoggedInVar } from './checkAuthStatus';
|
||||
import { GlobalCfg } from '../../conf';
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { EntityType } from '../shared/EntityTypeUtil';
|
||||
import React from 'react';
|
||||
import { EntityType } from '../../types.generated';
|
||||
|
||||
interface Props {
|
||||
type: EntityType; // type of entity associated with the urn. may be able to provide a util to infer this from the urn. we use this to fetch the results.
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Breadcrumb, Row } from 'antd';
|
||||
import { PageRoutes } from '../../conf/Global';
|
||||
import { EntityType, toCollectionName, toPathName } from '../shared/EntityTypeUtil';
|
||||
import { useEntityRegistry } from '../useEntityRegistry';
|
||||
import { EntityType } from '../../types.generated';
|
||||
|
||||
interface Props {
|
||||
type: EntityType;
|
||||
@ -13,11 +14,13 @@ interface Props {
|
||||
* Responsible for rendering a clickable browse path view.
|
||||
*/
|
||||
export const BrowsePath = ({ type, path }: Props) => {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
|
||||
const createPartialPath = (parts: Array<string>) => {
|
||||
return parts.join('/');
|
||||
};
|
||||
|
||||
const baseBrowsePath = `${PageRoutes.BROWSE}/${toPathName(type)}`;
|
||||
const baseBrowsePath = `${PageRoutes.BROWSE}/${entityRegistry.getPathName(type)}`;
|
||||
|
||||
const pathCrumbs = path.map((part, index) => (
|
||||
<Breadcrumb.Item>
|
||||
@ -29,7 +32,7 @@ export const BrowsePath = ({ type, path }: Props) => {
|
||||
<Row style={{ backgroundColor: 'white', padding: '10px 100px', borderBottom: '1px solid #dcdcdc' }}>
|
||||
<Breadcrumb style={{ fontSize: '16px' }}>
|
||||
<Breadcrumb.Item>
|
||||
<Link to={baseBrowsePath}>{toCollectionName(type)}</Link>
|
||||
<Link to={baseBrowsePath}>{entityRegistry.getCollectionName(type)}</Link>
|
||||
</Breadcrumb.Item>
|
||||
{pathCrumbs}
|
||||
</Breadcrumb>
|
||||
|
||||
@ -2,13 +2,13 @@ import React from 'react';
|
||||
import { Card } from 'antd';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
type Props = {
|
||||
export interface BrowseResultProps {
|
||||
url: string;
|
||||
name: string;
|
||||
count?: number | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export default function BrowseResultCard({ url, count, name }: Props) {
|
||||
export default function BrowseResultCard({ url, count, name }: BrowseResultProps) {
|
||||
return (
|
||||
<Link to={url}>
|
||||
<Card hoverable>
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Col, Pagination, Row } from 'antd';
|
||||
import { Content } from 'antd/lib/layout/layout';
|
||||
import React from 'react';
|
||||
import { BrowseResultEntity, BrowseResultGroup } from '../../types.generated';
|
||||
import { BrowseResultEntity, BrowseResultGroup, EntityType } from '../../types.generated';
|
||||
import BrowseResultCard from './BrowseResultCard';
|
||||
import { browseEntityResultToUrl } from './util/entityToUrl';
|
||||
import { useEntityRegistry } from '../useEntityRegistry';
|
||||
|
||||
interface Props {
|
||||
type: EntityType;
|
||||
title: string;
|
||||
rootPath: string;
|
||||
pageStart: number;
|
||||
@ -20,6 +21,7 @@ interface Props {
|
||||
* Display browse groups + entities.
|
||||
*/
|
||||
export const BrowseResults = ({
|
||||
type,
|
||||
title,
|
||||
rootPath,
|
||||
pageStart,
|
||||
@ -29,6 +31,7 @@ export const BrowseResults = ({
|
||||
groups,
|
||||
onChangePage,
|
||||
}: Props) => {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
return (
|
||||
<div>
|
||||
<Content style={{ backgroundColor: 'white', padding: '25px 100px' }}>
|
||||
@ -40,9 +43,7 @@ export const BrowseResults = ({
|
||||
</Col>
|
||||
))}
|
||||
{entities.map((entity) => (
|
||||
<Col span={24}>
|
||||
<BrowseResultCard name={entity.name} url={browseEntityResultToUrl(entity)} />
|
||||
</Col>
|
||||
<Col span={24}>{entityRegistry.renderBrowse(type, entity)}</Col>
|
||||
))}
|
||||
<Col span={24}>
|
||||
<Pagination
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { Redirect, useHistory, useLocation, useParams } from 'react-router';
|
||||
import * as QueryString from 'query-string';
|
||||
import { Affix } from 'antd';
|
||||
import { fromPathName, toCollectionName } from '../shared/EntityTypeUtil';
|
||||
import { BrowseCfg } from '../../conf';
|
||||
import { BrowseResults } from './BrowseResults';
|
||||
import { SearchablePage } from '../search/SearchablePage';
|
||||
import { useGetBrowseResultsQuery } from '../../graphql/browse.generated';
|
||||
import { BrowsePath } from './BrowsePath';
|
||||
import { PageRoutes } from '../../conf/Global';
|
||||
|
||||
const { RESULTS_PER_PAGE } = BrowseCfg;
|
||||
import { useEntityRegistry } from '../useEntityRegistry';
|
||||
|
||||
type BrowseResultsPageParams = {
|
||||
type: string;
|
||||
@ -21,9 +19,11 @@ export const BrowseResultsPage = () => {
|
||||
const history = useHistory();
|
||||
const { type } = useParams<BrowseResultsPageParams>();
|
||||
|
||||
const entityRegistry = useEntityRegistry();
|
||||
|
||||
const rootPath = location.pathname;
|
||||
const params = QueryString.parse(location.search);
|
||||
const entityType = fromPathName(type);
|
||||
const entityType = entityRegistry.getTypeFromPathName(type);
|
||||
const path = rootPath.split('/').slice(3);
|
||||
const page = Number(params.page) || 1;
|
||||
|
||||
@ -32,8 +32,8 @@ export const BrowseResultsPage = () => {
|
||||
input: {
|
||||
type: entityType,
|
||||
path,
|
||||
start: (page - 1) * RESULTS_PER_PAGE,
|
||||
count: RESULTS_PER_PAGE,
|
||||
start: (page - 1) * BrowseCfg.RESULTS_PER_PAGE,
|
||||
count: BrowseCfg.RESULTS_PER_PAGE,
|
||||
filters: null,
|
||||
},
|
||||
},
|
||||
@ -59,10 +59,11 @@ export const BrowseResultsPage = () => {
|
||||
{loading && <p>Loading browse results...</p>}
|
||||
{data && data.browse && (
|
||||
<BrowseResults
|
||||
type={entityType}
|
||||
rootPath={rootPath}
|
||||
title={path.length > 0 ? path[path.length - 1] : toCollectionName(entityType)}
|
||||
pageSize={RESULTS_PER_PAGE}
|
||||
pageStart={page * RESULTS_PER_PAGE}
|
||||
title={path.length > 0 ? path[path.length - 1] : entityRegistry.getCollectionName(entityType)}
|
||||
pageSize={BrowseCfg.RESULTS_PER_PAGE}
|
||||
pageStart={page * BrowseCfg.RESULTS_PER_PAGE}
|
||||
groups={data.browse.metadata.groups}
|
||||
entities={data.browse.entities}
|
||||
totalResults={data.browse.total}
|
||||
|
||||
@ -1,25 +1,23 @@
|
||||
import * as React from 'react';
|
||||
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 { BrowseCfg } from '../../conf';
|
||||
import { SearchablePage } from '../search/SearchablePage';
|
||||
import { toCollectionName, toPathName } from '../shared/EntityTypeUtil';
|
||||
import { PageRoutes } from '../../conf/Global';
|
||||
import '../../App.css';
|
||||
|
||||
const { BROWSABLE_ENTITY_TYPES } = BrowseCfg;
|
||||
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]}>
|
||||
{BROWSABLE_ENTITY_TYPES.map((entityType) => (
|
||||
{entityRegistry.getBrowseEntityTypes().map((entityType) => (
|
||||
<Col xs={24} sm={24} md={8}>
|
||||
<Link to={`${PageRoutes.BROWSE}/${toPathName(entityType)}`}>
|
||||
<Link to={`${PageRoutes.BROWSE}/${entityRegistry.getPathName(entityType)}`}>
|
||||
<Card
|
||||
style={{
|
||||
padding: '30px 0px',
|
||||
@ -29,7 +27,7 @@ export const BrowseTypesPage = () => {
|
||||
hoverable
|
||||
>
|
||||
<div style={{ fontSize: '18px', fontWeight: 'bold', color: '#0073b1' }}>
|
||||
{toCollectionName(entityType)}
|
||||
{entityRegistry.getCollectionName(entityType)}
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
// NOTE: this file is a temporary solution. Soon, entity classes will provide the urls via their asBrowseResult method.
|
||||
|
||||
import { PageRoutes } from '../../../conf/Global';
|
||||
import { BrowseResultEntity } from '../../../types.generated';
|
||||
|
||||
export function browseEntityResultToUrl(browseResultEntity: BrowseResultEntity) {
|
||||
return `${PageRoutes.DATASETS}/${browseResultEntity.urn}`;
|
||||
}
|
||||
58
datahub-web-react/src/components/entity/Entity.tsx
Normal file
58
datahub-web-react/src/components/entity/Entity.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { EntityType } from '../../types.generated';
|
||||
|
||||
export enum PreviewType {
|
||||
/**
|
||||
* A preview shown within the search experience
|
||||
*/
|
||||
SEARCH,
|
||||
/**
|
||||
* A preview shown within the browse experience
|
||||
*/
|
||||
BROWSE,
|
||||
/**
|
||||
* A generic preview shown within other entity pages, etc.
|
||||
*/
|
||||
PREVIEW,
|
||||
}
|
||||
|
||||
/**
|
||||
* Base interface used for authoring DataHub Entities on the client side.
|
||||
*
|
||||
* <T> the generated GraphQL data type associated with the entity.
|
||||
*/
|
||||
export interface Entity<T> {
|
||||
/**
|
||||
* Corresponding GQL EntityType.
|
||||
*/
|
||||
type: EntityType;
|
||||
|
||||
/**
|
||||
* Returns whether the entity search is enabled
|
||||
*/
|
||||
isSearchEnabled: () => boolean;
|
||||
|
||||
/**
|
||||
* Returns whether the entity browse is enabled
|
||||
*/
|
||||
isBrowseEnabled: () => boolean;
|
||||
|
||||
/**
|
||||
* Returns the name of the entity as it appears in a URL, e.g. '/dataset/:urn'.
|
||||
*/
|
||||
getPathName: () => string;
|
||||
|
||||
/**
|
||||
* Returns the plural name of the entity used when displaying collections (search, browse results), e.g. 'Datasets'.
|
||||
*/
|
||||
getCollectionName: () => string;
|
||||
|
||||
/**
|
||||
* Renders the 'profile' of the entity on an entity details page.
|
||||
*/
|
||||
renderProfile: (urn: string) => JSX.Element;
|
||||
|
||||
/**
|
||||
* Renders a preview of the entity across different use cases like search, browse, etc.
|
||||
*/
|
||||
renderPreview: (type: PreviewType, data: T) => JSX.Element;
|
||||
}
|
||||
27
datahub-web-react/src/components/entity/EntityPage.tsx
Normal file
27
datahub-web-react/src/components/entity/EntityPage.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { EntityType } from '../../types.generated';
|
||||
import { BrowsableEntityPage } from '../browse/BrowsableEntityPage';
|
||||
import { useEntityRegistry } from '../useEntityRegistry';
|
||||
|
||||
interface RouteParams {
|
||||
urn: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
entityType: EntityType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for rendering an Entity Profile
|
||||
*/
|
||||
export const EntityPage = ({ entityType }: Props) => {
|
||||
const { urn } = useParams<RouteParams>();
|
||||
const entityRegistry = useEntityRegistry();
|
||||
|
||||
return (
|
||||
<BrowsableEntityPage urn={urn} type={entityType}>
|
||||
{entityRegistry.renderProfile(entityType, urn)}
|
||||
</BrowsableEntityPage>
|
||||
);
|
||||
};
|
||||
91
datahub-web-react/src/components/entity/EntityRegistry.tsx
Normal file
91
datahub-web-react/src/components/entity/EntityRegistry.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { EntityType } from '../../types.generated';
|
||||
import { Entity, PreviewType } from './Entity';
|
||||
|
||||
function validatedGet<K, V>(key: K, map: Map<K, V>): V {
|
||||
if (map.has(key)) {
|
||||
return map.get(key) as V;
|
||||
}
|
||||
throw new Error(`Unrecognized key ${key} provided in map ${JSON.stringify(map)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serves as a singleton registry for all DataHub entities to appear on the frontend.
|
||||
*/
|
||||
export default class EntityRegistry {
|
||||
entities: Array<Entity<any>> = new Array<Entity<any>>();
|
||||
|
||||
entityTypeToEntity: Map<EntityType, Entity<any>> = new Map<EntityType, Entity<any>>();
|
||||
|
||||
collectionNameToEntityType: Map<string, EntityType> = new Map<string, EntityType>();
|
||||
|
||||
pathNameToEntityType: Map<string, EntityType> = new Map<string, EntityType>();
|
||||
|
||||
register(entity: Entity<any>) {
|
||||
this.entities.push(entity);
|
||||
this.entityTypeToEntity.set(entity.type, entity);
|
||||
this.collectionNameToEntityType.set(entity.getCollectionName(), entity.type);
|
||||
this.pathNameToEntityType.set(entity.getPathName(), entity.type);
|
||||
}
|
||||
|
||||
getEntities(): Array<Entity<any>> {
|
||||
return this.entities;
|
||||
}
|
||||
|
||||
getSearchEntityTypes(): Array<EntityType> {
|
||||
return this.entities.filter((entity) => entity.isSearchEnabled()).map((entity) => entity.type);
|
||||
}
|
||||
|
||||
getDefaultSearchEntityType(): EntityType {
|
||||
return this.entities[0].type;
|
||||
}
|
||||
|
||||
getBrowseEntityTypes(): Array<EntityType> {
|
||||
return this.entities.filter((entity) => entity.isBrowseEnabled()).map((entity) => entity.type);
|
||||
}
|
||||
|
||||
getCollectionName(type: EntityType): string {
|
||||
const entity = validatedGet(type, this.entityTypeToEntity);
|
||||
return entity.getCollectionName();
|
||||
}
|
||||
|
||||
getTypeFromCollectionName(name: string): EntityType {
|
||||
return validatedGet(name, this.collectionNameToEntityType);
|
||||
}
|
||||
|
||||
getPathName(type: EntityType): string {
|
||||
const entity = validatedGet(type, this.entityTypeToEntity);
|
||||
return entity.getPathName();
|
||||
}
|
||||
|
||||
getTypeFromPathName(pathName: string): EntityType {
|
||||
return validatedGet(pathName, this.pathNameToEntityType);
|
||||
}
|
||||
|
||||
getTypeOrDefaultFromPathName(pathName: string, def: EntityType): EntityType {
|
||||
try {
|
||||
return validatedGet(pathName, this.pathNameToEntityType);
|
||||
} catch (e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
renderProfile(type: EntityType, urn: string): JSX.Element {
|
||||
const entity = validatedGet(type, this.entityTypeToEntity);
|
||||
return entity.renderProfile(urn);
|
||||
}
|
||||
|
||||
renderPreview<T>(entityType: EntityType, type: PreviewType, data: T): JSX.Element {
|
||||
const entity = validatedGet(entityType, this.entityTypeToEntity);
|
||||
return entity.renderPreview(type, data);
|
||||
}
|
||||
|
||||
renderSearchResult<T>(type: EntityType, data: T): JSX.Element {
|
||||
const entity = validatedGet(type, this.entityTypeToEntity);
|
||||
return entity.renderPreview(PreviewType.SEARCH, data);
|
||||
}
|
||||
|
||||
renderBrowse(type: EntityType, { urn, name }: { urn: string; name: string }): JSX.Element {
|
||||
const entity = validatedGet(type, this.entityTypeToEntity);
|
||||
return entity.renderPreview(PreviewType.BROWSE, { urn, name });
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import * as React from 'react';
|
||||
import { Dataset, EntityType } from '../../../types.generated';
|
||||
import { Profile } from './profile/Profile';
|
||||
import { Entity, PreviewType } from '../Entity';
|
||||
import { Preview } from './preview/Preview';
|
||||
|
||||
/**
|
||||
* Definition of the DataHub Dataset entity.
|
||||
*/
|
||||
export class DatasetEntity implements Entity<Dataset> {
|
||||
type: EntityType = EntityType.Dataset;
|
||||
|
||||
isSearchEnabled = () => true;
|
||||
|
||||
isBrowseEnabled = () => true;
|
||||
|
||||
getAutoCompleteFieldName = () => 'name';
|
||||
|
||||
getPathName = () => 'dataset';
|
||||
|
||||
getCollectionName = () => 'Datasets';
|
||||
|
||||
renderProfile = (urn: string) => <Profile urn={urn} />;
|
||||
|
||||
renderPreview = (_: PreviewType, data: any) => {
|
||||
return (
|
||||
<Preview
|
||||
urn={data.urn}
|
||||
name={data.name}
|
||||
origin={data.origin}
|
||||
description={data.description}
|
||||
platformNativeType={data.platformNativeType}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { EntityType, FabricType, PlatformNativeType } from '../../../../types.generated';
|
||||
import DefaultPreviewCard from '../../../preview/DefaultPreviewCard';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
|
||||
export const Preview = ({
|
||||
urn,
|
||||
name,
|
||||
origin,
|
||||
description,
|
||||
platformNativeType,
|
||||
}: {
|
||||
urn: string;
|
||||
name: string;
|
||||
origin: FabricType;
|
||||
description?: string | null;
|
||||
platformNativeType?: PlatformNativeType | 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>
|
||||
);
|
||||
};
|
||||
@ -1,10 +1,10 @@
|
||||
import { AutoComplete, Avatar, Button, Col, Row, Select, Table } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { EntityType, Owner, OwnershipType } from '../../../types.generated';
|
||||
import defaultAvatar from '../../../images/default_avatar.png';
|
||||
import { PageRoutes } from '../../../conf/Global';
|
||||
import { useGetAutoCompleteResultsLazyQuery } from '../../../graphql/search.generated';
|
||||
import { EntityType, Owner, OwnershipType } from '../../../../types.generated';
|
||||
import defaultAvatar from '../../../../images/default_avatar.png';
|
||||
import { useGetAutoCompleteResultsLazyQuery } from '../../../../graphql/search.generated';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
|
||||
const OWNER_SEARCH_PLACEHOLDER = 'Enter an LDAP...';
|
||||
const NUMBER_OWNERS_REQUIRED = 2;
|
||||
@ -25,6 +25,8 @@ export const Ownership: React.FC<Props> = ({
|
||||
}: Props): JSX.Element => {
|
||||
console.log(_lastModifiedAt);
|
||||
|
||||
const entityRegistry = useEntityRegistry();
|
||||
|
||||
const getOwnerTableData = (ownerArr: Array<Owner>) => {
|
||||
const rows = ownerArr.map((owner) => ({
|
||||
urn: owner.owner.urn,
|
||||
@ -93,7 +95,7 @@ export const Ownership: React.FC<Props> = ({
|
||||
title: 'LDAP',
|
||||
dataIndex: 'ldap',
|
||||
render: (text: string, record: any) => (
|
||||
<Link to={`${PageRoutes.USERS}/${record.urn}`}>
|
||||
<Link to={`${entityRegistry.getPathName(EntityType.User)}/${record.urn}`}>
|
||||
<Avatar
|
||||
style={{
|
||||
marginRight: '15px',
|
||||
@ -1,15 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import { Avatar, Col, Row, Tooltip } from 'antd';
|
||||
|
||||
import { BrowsableEntityPage } from '../../browse/BrowsableEntityPage';
|
||||
import { useGetDatasetQuery } from '../../../graphql/dataset.generated';
|
||||
import { EntityType } from '../../shared/EntityTypeUtil';
|
||||
import defaultAvatar from '../../../images/default_avatar.png';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useGetDatasetQuery } from '../../../../graphql/dataset.generated';
|
||||
import defaultAvatar from '../../../../images/default_avatar.png';
|
||||
import { Ownership as OwnershipView } from './Ownership';
|
||||
import { Schema as SchemaView } from './Schema';
|
||||
import { GenericEntityDetails } from '../../shared/GenericEntityDetails';
|
||||
import { PageRoutes } from '../../../conf/Global';
|
||||
import { EntityProfile } from '../../../shared/EntityProfile';
|
||||
import { EntityType } from '../../../../types.generated';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
|
||||
export enum TabType {
|
||||
Ownership = 'Ownership',
|
||||
@ -17,18 +15,13 @@ export enum TabType {
|
||||
}
|
||||
|
||||
const ENABLED_TAB_TYPES = [TabType.Ownership, TabType.Schema];
|
||||
|
||||
interface RouteParams {
|
||||
urn: string;
|
||||
}
|
||||
|
||||
const EMPTY_OWNER_ARR: never[] = [];
|
||||
|
||||
/**
|
||||
* Responsible for display the Dataset Page
|
||||
*/
|
||||
export const DatasetPage: React.VFC = () => {
|
||||
const { urn } = useParams<RouteParams>();
|
||||
export const Profile = ({ urn }: { urn: string }): JSX.Element => {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
|
||||
const { loading, error, data } = useGetDatasetQuery({ variables: { urn } });
|
||||
|
||||
@ -46,7 +39,7 @@ export const DatasetPage: React.VFC = () => {
|
||||
ownership.owners &&
|
||||
ownership.owners.map((owner: any) => (
|
||||
<Tooltip title={owner.owner.info?.fullName}>
|
||||
<Link to={`${PageRoutes.USERS}/${owner.owner.urn}`}>
|
||||
<Link to={`${entityRegistry.getPathName(EntityType.User)}/${owner.owner.urn}`}>
|
||||
<Avatar
|
||||
style={{
|
||||
color: '#f56a00',
|
||||
@ -87,11 +80,11 @@ export const DatasetPage: React.VFC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<BrowsableEntityPage urn={urn} type={EntityType.Dataset}>
|
||||
<>
|
||||
{loading && <p>Loading...</p>}
|
||||
{data && !data.dataset && !error && <p>Unable to find dataset with urn {urn}</p>}
|
||||
{data && data.dataset && !error && (
|
||||
<GenericEntityDetails
|
||||
<EntityProfile
|
||||
title={data.dataset.name}
|
||||
tags={data.dataset.tags}
|
||||
body={getBody(data.dataset?.description || '', data.dataset?.ownership)}
|
||||
@ -99,6 +92,6 @@ export const DatasetPage: React.VFC = () => {
|
||||
/>
|
||||
)}
|
||||
{error && <p>Failed to load dataset with urn {urn}</p>}
|
||||
</BrowsableEntityPage>
|
||||
</>
|
||||
);
|
||||
};
|
||||
25
datahub-web-react/src/components/entity/user/User.tsx
Normal file
25
datahub-web-react/src/components/entity/user/User.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import { CorpUser, EntityType } from '../../../types.generated';
|
||||
import { Entity, PreviewType } from '../Entity';
|
||||
import { UserPage } from './UserPage';
|
||||
|
||||
/**
|
||||
* Definition of the DataHub Dataset entity.
|
||||
*/
|
||||
export class UserEntity implements Entity<CorpUser> {
|
||||
type: EntityType = EntityType.User;
|
||||
|
||||
isSearchEnabled = () => true;
|
||||
|
||||
isBrowseEnabled = () => false;
|
||||
|
||||
getAutoCompleteFieldName = () => 'username';
|
||||
|
||||
getPathName: () => string = () => 'user';
|
||||
|
||||
getCollectionName: () => string = () => 'Users';
|
||||
|
||||
renderProfile: (urn: string) => JSX.Element = (_) => <UserPage />;
|
||||
|
||||
renderPreview = (_: PreviewType, _1: CorpUser) => <p>Hello</p>;
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import { Divider } from 'antd';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface Props extends React.PropsWithChildren<any> {
|
||||
title?: React.ReactNode;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export default function DefaultPreviewCard({ title, url, children }: Props) {
|
||||
return (
|
||||
<div style={{ padding: '0px 24px' }}>
|
||||
<Link to={url} style={{ color: '#0073b1', padding: '0px 24px' }} type="link">
|
||||
{title}
|
||||
</Link>
|
||||
<div style={{ padding: '20px 5px 5px 5px' }}>{children}</div>
|
||||
<Divider />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -2,20 +2,16 @@ import React from 'react';
|
||||
import * as QueryString from 'query-string';
|
||||
import { useHistory, useLocation, useParams } from 'react-router';
|
||||
import { Affix, Col, Row, Tabs, Layout } from 'antd';
|
||||
|
||||
import { SearchablePage } from './SearchablePage';
|
||||
import { fromCollectionName, fromPathnameOrEmptyString, toCollectionName } from '../shared/EntityTypeUtil';
|
||||
import { useGetSearchResultsQuery } from '../../graphql/search.generated';
|
||||
import { SearchResults } from './SearchResults';
|
||||
import { EntityType, FacetFilterInput, PlatformNativeType } from '../../types.generated';
|
||||
import { SearchFilters } from './SearchFilters';
|
||||
import { SearchCfg } from '../../conf';
|
||||
import { PageRoutes } from '../../conf/Global';
|
||||
import { useEntityRegistry } from '../useEntityRegistry';
|
||||
import { FacetFilterInput } from '../../types.generated';
|
||||
import useFilters from './utils/useFilters';
|
||||
import { navigateToSearchUrl } from './utils/navigateToSearchUrl';
|
||||
|
||||
const { SEARCHABLE_ENTITY_TYPES, RESULTS_PER_PAGE } = SearchCfg;
|
||||
|
||||
type SearchPageParams = {
|
||||
type?: string;
|
||||
};
|
||||
@ -23,14 +19,20 @@ type SearchPageParams = {
|
||||
/**
|
||||
* A dedicated search page.
|
||||
*
|
||||
* TODO: Read / write filter parameters from the URL query parameters.
|
||||
* TODO: Read / write filter parameters from / to the URL query parameters.
|
||||
*/
|
||||
export const SearchPage = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
|
||||
const params = QueryString.parse(location.search, { arrayFormat: 'comma' });
|
||||
const type = fromPathnameOrEmptyString(useParams<SearchPageParams>().type || '') || SEARCHABLE_ENTITY_TYPES[0];
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const searchTypes = entityRegistry.getSearchEntityTypes();
|
||||
|
||||
const params = QueryString.parse(location.search);
|
||||
const type = entityRegistry.getTypeOrDefaultFromPathName(
|
||||
useParams<SearchPageParams>().type || '',
|
||||
entityRegistry.getDefaultSearchEntityType(),
|
||||
);
|
||||
const query: string = params.query ? (params.query as string) : '';
|
||||
const page: number = params.page && Number(params.page as string) > 0 ? Number(params.page as string) : 1;
|
||||
const filters: Array<FacetFilterInput> = useFilters(params);
|
||||
@ -40,16 +42,16 @@ export const SearchPage = () => {
|
||||
input: {
|
||||
type,
|
||||
query,
|
||||
start: (page - 1) * RESULTS_PER_PAGE,
|
||||
count: RESULTS_PER_PAGE,
|
||||
start: (page - 1) * SearchCfg.RESULTS_PER_PAGE,
|
||||
count: SearchCfg.RESULTS_PER_PAGE,
|
||||
filters,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const onSearchTypeChange = (newType: string) => {
|
||||
const entityType = fromCollectionName(newType);
|
||||
navigateToSearchUrl({ type: entityType, query, page: 1, history });
|
||||
const entityType = entityRegistry.getTypeFromCollectionName(newType);
|
||||
navigateToSearchUrl({ type: entityType, query, page: 1, history, entityRegistry });
|
||||
};
|
||||
|
||||
const onFilterSelect = (selected: boolean, field: string, value: string) => {
|
||||
@ -57,68 +59,15 @@ export const SearchPage = () => {
|
||||
? [...filters, { field, value }]
|
||||
: filters.filter((filter) => filter.field !== field || filter.value !== value);
|
||||
|
||||
navigateToSearchUrl({ type, query, page: 1, filters: newFilters, history });
|
||||
navigateToSearchUrl({ type, query, page: 1, filters: newFilters, history, entityRegistry });
|
||||
};
|
||||
|
||||
const onResultsPageChange = (newPage: number) => {
|
||||
navigateToSearchUrl({ type, query, page: newPage, filters, history });
|
||||
};
|
||||
|
||||
const navigateToDataset = (urn: string) => {
|
||||
return history.push({
|
||||
pathname: `${PageRoutes.DATASETS}/${urn}`,
|
||||
});
|
||||
};
|
||||
|
||||
const toDatasetSearchResult = (dataset: {
|
||||
urn: string;
|
||||
name: string;
|
||||
origin: string;
|
||||
description: string;
|
||||
platformNativeType: PlatformNativeType;
|
||||
}) => {
|
||||
return {
|
||||
title: (
|
||||
<div style={{ margin: '5px 0px 5px 2px', fontSize: '20px', fontWeight: 'bold' }}>{dataset.name}</div>
|
||||
),
|
||||
preview: (
|
||||
<>
|
||||
<div style={{ margin: '0px 0px 15px 0px' }}>{dataset.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' }}>{dataset.origin}</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: '150px',
|
||||
margin: '5px 0px 5px 0px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<b>Platform</b>
|
||||
<div>{dataset.platformNativeType}</div>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
onNavigate: () => navigateToDataset(dataset.urn),
|
||||
};
|
||||
navigateToSearchUrl({ type, query, page: newPage, filters, history, entityRegistry });
|
||||
};
|
||||
|
||||
const toSearchResults = (elements: any) => {
|
||||
switch (type) {
|
||||
case EntityType.Dataset:
|
||||
return elements.map((element: any) => toDatasetSearchResult(element));
|
||||
default:
|
||||
throw new Error(`Search for entity of type ${type} currently not supported!`);
|
||||
}
|
||||
return elements.map((element: any) => entityRegistry.renderSearchResult(type, element));
|
||||
};
|
||||
|
||||
const searchResults = toSearchResults(data?.search?.elements || []);
|
||||
@ -129,12 +78,15 @@ export const SearchPage = () => {
|
||||
<Affix offsetTop={64}>
|
||||
<Tabs
|
||||
tabBarStyle={{ backgroundColor: 'white', padding: '0px 165px', marginBottom: '0px' }}
|
||||
activeKey={toCollectionName(type)}
|
||||
activeKey={entityRegistry.getCollectionName(type)}
|
||||
size="large"
|
||||
onChange={onSearchTypeChange}
|
||||
>
|
||||
{SEARCHABLE_ENTITY_TYPES.map((t) => (
|
||||
<Tabs.TabPane tab={toCollectionName(t)} key={toCollectionName(t)} />
|
||||
{searchTypes.map((t) => (
|
||||
<Tabs.TabPane
|
||||
tab={entityRegistry.getCollectionName(t)}
|
||||
key={entityRegistry.getCollectionName(t)}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</Affix>
|
||||
@ -151,7 +103,7 @@ export const SearchPage = () => {
|
||||
{error && !data && <p>Search error!</p>}
|
||||
{data?.search && (
|
||||
<SearchResults
|
||||
typeName={toCollectionName(type)}
|
||||
typeName={entityRegistry.getCollectionName(type)}
|
||||
results={searchResults}
|
||||
pageStart={data?.search?.start}
|
||||
pageSize={data.search?.count}
|
||||
|
||||
@ -1,58 +1,35 @@
|
||||
import { Button, Card, Divider, Pagination } from 'antd';
|
||||
import { Card, Pagination } from 'antd';
|
||||
import * as React from 'react';
|
||||
|
||||
interface SearchResult {
|
||||
title?: React.ReactNode; // Title content
|
||||
preview?: React.ReactNode; // Body content
|
||||
onNavigate: () => void; // Invoked when the search result is clicked.
|
||||
}
|
||||
|
||||
interface Props {
|
||||
typeName: string;
|
||||
pageStart: number;
|
||||
pageSize: number;
|
||||
totalResults: number;
|
||||
results: Array<SearchResult>;
|
||||
results: Array<JSX.Element>;
|
||||
onChangePage: (page: number) => void;
|
||||
}
|
||||
|
||||
export const SearchResults = ({
|
||||
typeName: _typeName,
|
||||
pageStart: _pageStart,
|
||||
pageSize: _pageSize,
|
||||
totalResults: _totalResults,
|
||||
results: _results,
|
||||
onChangePage: _onChangePage,
|
||||
}: Props) => {
|
||||
export const SearchResults = ({ typeName, pageStart, pageSize, totalResults, results, onChangePage }: Props) => {
|
||||
return (
|
||||
<Card
|
||||
style={{ border: '1px solid #d2d2d2' }}
|
||||
title={<h1 style={{ marginBottom: '0px' }}>{_typeName}</h1>}
|
||||
title={<h1 style={{ marginBottom: '0px' }}>{typeName}</h1>}
|
||||
bodyStyle={{ padding: '24px 0px' }}
|
||||
extra={
|
||||
<div style={{ color: 'grey' }}>
|
||||
Showing {_pageStart * _pageSize} - {_pageStart * _pageSize + _pageSize} of {_totalResults} results
|
||||
Showing {pageStart * pageSize} - {pageStart * pageSize + pageSize} of {totalResults} results
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{_results.map((result) => (
|
||||
<>
|
||||
<div style={{ padding: '0px 24px' }}>
|
||||
<Button onClick={result.onNavigate} style={{ color: '#0073b1', padding: '0px' }} type="link">
|
||||
{result.title}
|
||||
</Button>
|
||||
<div style={{ padding: '20px 5px 5px 5px' }}>{result.preview}</div>
|
||||
</div>
|
||||
<Divider />
|
||||
</>
|
||||
))}
|
||||
{results}
|
||||
<Pagination
|
||||
style={{ width: '100%', display: 'flex', justifyContent: 'center' }}
|
||||
current={_pageStart}
|
||||
pageSize={_pageSize}
|
||||
total={_totalResults / _pageSize}
|
||||
current={pageStart}
|
||||
pageSize={pageSize}
|
||||
total={totalResults / pageSize}
|
||||
showLessItems
|
||||
onChange={_onChangePage}
|
||||
onChange={onChangePage}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@ -1,25 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import 'antd/dist/antd.css';
|
||||
import { Layout } from 'antd';
|
||||
import { useHistory } from 'react-router';
|
||||
import { SearchHeader } from './SearchHeader';
|
||||
import { EntityType, fromCollectionName, toCollectionName } from '../shared/EntityTypeUtil';
|
||||
import { SearchCfg } from '../../conf';
|
||||
import { useEntityRegistry } from '../useEntityRegistry';
|
||||
import { useGetAutoCompleteResultsLazyQuery } from '../../graphql/search.generated';
|
||||
import { navigateToSearchUrl } from './utils/navigateToSearchUrl';
|
||||
|
||||
const { SEARCHABLE_ENTITY_TYPES, SEARCH_BAR_PLACEHOLDER_TEXT, SHOW_ALL_ENTITIES_SEARCH_TYPE } = SearchCfg;
|
||||
import { EntityType } from '../../types.generated';
|
||||
|
||||
const ALL_ENTITIES_SEARCH_TYPE_NAME = 'All Entities';
|
||||
const EMPTY_STRING = '';
|
||||
|
||||
const DEFAULT_SELECTED_ENTITY_TYPE_NAME = SHOW_ALL_ENTITIES_SEARCH_TYPE
|
||||
? ALL_ENTITIES_SEARCH_TYPE_NAME
|
||||
: toCollectionName(SEARCHABLE_ENTITY_TYPES[0]);
|
||||
|
||||
const SUPPORTED_SEARCH_TYPE_NAMES = SHOW_ALL_ENTITIES_SEARCH_TYPE
|
||||
? [ALL_ENTITIES_SEARCH_TYPE_NAME, ...SEARCHABLE_ENTITY_TYPES.map((entityType) => toCollectionName(entityType))]
|
||||
: [...SEARCHABLE_ENTITY_TYPES.map((entityType) => toCollectionName(entityType))];
|
||||
|
||||
interface Props extends React.PropsWithChildren<any> {
|
||||
initialType?: EntityType;
|
||||
@ -28,60 +18,58 @@ interface Props extends React.PropsWithChildren<any> {
|
||||
|
||||
const defaultProps = {
|
||||
initialType: undefined,
|
||||
initialQuery: EMPTY_STRING,
|
||||
initialQuery: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* A page that includes a sticky search header (nav bar)
|
||||
*/
|
||||
export const SearchablePage = ({
|
||||
initialType: _initialType,
|
||||
initialQuery: _initialQuery,
|
||||
children: _children,
|
||||
}: Props) => {
|
||||
export const SearchablePage = ({ initialType, initialQuery, children }: Props) => {
|
||||
const history = useHistory();
|
||||
|
||||
const initialSearchTypeName = _initialType ? toCollectionName(_initialType) : DEFAULT_SELECTED_ENTITY_TYPE_NAME;
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const searchTypes = entityRegistry.getSearchEntityTypes();
|
||||
const searchTypeNames = searchTypes.map((entityType) => entityRegistry.getCollectionName(entityType));
|
||||
|
||||
if (!SUPPORTED_SEARCH_TYPE_NAMES.includes(initialSearchTypeName)) {
|
||||
throw new Error(`Unsupported search EntityType ${_initialType} provided!`);
|
||||
}
|
||||
const initialSearchTypeName =
|
||||
initialType && searchTypes.includes(initialType)
|
||||
? entityRegistry.getCollectionName(initialType)
|
||||
: ALL_ENTITIES_SEARCH_TYPE_NAME;
|
||||
|
||||
const [getAutoCompleteResults, { data: suggestionsData }] = useGetAutoCompleteResultsLazyQuery();
|
||||
|
||||
const search = (type: string, query: string) => {
|
||||
navigateToSearchUrl({
|
||||
type: ALL_ENTITIES_SEARCH_TYPE_NAME === type ? SEARCHABLE_ENTITY_TYPES[0] : fromCollectionName(type),
|
||||
type:
|
||||
ALL_ENTITIES_SEARCH_TYPE_NAME === type
|
||||
? searchTypes[0]
|
||||
: entityRegistry.getTypeFromCollectionName(type),
|
||||
query,
|
||||
history,
|
||||
entityRegistry,
|
||||
});
|
||||
};
|
||||
|
||||
const autoComplete = (type: string, query: string) => {
|
||||
const entityType =
|
||||
ALL_ENTITIES_SEARCH_TYPE_NAME === type ? SEARCHABLE_ENTITY_TYPES[0] : fromCollectionName(type);
|
||||
const autoCompleteField = SearchCfg.getAutoCompleteFieldName(entityType);
|
||||
|
||||
if (autoCompleteField) {
|
||||
getAutoCompleteResults({
|
||||
variables: {
|
||||
input: {
|
||||
type: entityType,
|
||||
query,
|
||||
field: autoCompleteField,
|
||||
},
|
||||
ALL_ENTITIES_SEARCH_TYPE_NAME === type ? searchTypes[0] : entityRegistry.getTypeFromCollectionName(type);
|
||||
getAutoCompleteResults({
|
||||
variables: {
|
||||
input: {
|
||||
type: entityType,
|
||||
query,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<SearchHeader
|
||||
types={SUPPORTED_SEARCH_TYPE_NAMES}
|
||||
types={searchTypeNames}
|
||||
initialType={initialSearchTypeName}
|
||||
initialQuery={_initialQuery as string}
|
||||
placeholderText={SEARCH_BAR_PLACEHOLDER_TEXT}
|
||||
initialQuery={initialQuery as string}
|
||||
placeholderText={SearchCfg.SEARCH_BAR_PLACEHOLDER_TEXT}
|
||||
suggestions={
|
||||
(suggestionsData && suggestionsData?.autoComplete && suggestionsData.autoComplete.suggestions) || []
|
||||
}
|
||||
@ -89,7 +77,7 @@ export const SearchablePage = ({
|
||||
onQueryChange={autoComplete}
|
||||
authenticatedUserUrn="urn:li:corpuser:0"
|
||||
/>
|
||||
<div style={{ marginTop: 64 }}>{_children}</div>
|
||||
<div style={{ marginTop: 64 }}>{children}</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
@ -3,8 +3,8 @@ import { RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import filtersToQueryStringParams from './filtersToQueryStringParams';
|
||||
import { EntityType, FacetFilterInput } from '../../../types.generated';
|
||||
import { toPathName } from '../../shared/EntityTypeUtil';
|
||||
import { PageRoutes } from '../../../conf/Global';
|
||||
import EntityRegistry from '../../entity/EntityRegistry';
|
||||
|
||||
export const navigateToSearchUrl = ({
|
||||
type: newType,
|
||||
@ -12,12 +12,14 @@ export const navigateToSearchUrl = ({
|
||||
page: newPage = 1,
|
||||
filters: newFilters,
|
||||
history,
|
||||
entityRegistry,
|
||||
}: {
|
||||
type: EntityType;
|
||||
query?: string;
|
||||
page?: number;
|
||||
filters?: Array<FacetFilterInput>;
|
||||
history: RouteComponentProps['history'];
|
||||
entityRegistry: EntityRegistry;
|
||||
}) => {
|
||||
const search = QueryString.stringify(
|
||||
{
|
||||
@ -29,7 +31,7 @@ export const navigateToSearchUrl = ({
|
||||
);
|
||||
|
||||
history.push({
|
||||
pathname: `${PageRoutes.SEARCH}/${toPathName(newType)}`,
|
||||
pathname: `${PageRoutes.SEARCH}/${entityRegistry.getPathName(newType)}`,
|
||||
search,
|
||||
});
|
||||
};
|
||||
|
||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { Col, Row, Tag, Divider, Layout } from 'antd';
|
||||
import { RoutedTabs } from './RoutedTabs';
|
||||
|
||||
interface Props {
|
||||
export interface EntityProfileProps {
|
||||
title: string;
|
||||
tags?: Array<string>;
|
||||
body: React.ReactNode;
|
||||
@ -19,9 +19,9 @@ const defaultProps = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A generic container view for presenting Entity details.
|
||||
* A default container view for presenting Entity details.
|
||||
*/
|
||||
export const GenericEntityDetails = ({ title: _title, tags: _tags, body: _body, tabs: _tabs }: Props) => {
|
||||
export const EntityProfile = ({ title: _title, tags: _tags, body: _body, tabs: _tabs }: EntityProfileProps) => {
|
||||
const defaultTabPath = _tabs && _tabs?.length > 0 ? _tabs[0].path : '';
|
||||
|
||||
/* eslint-disable spaced-comment */
|
||||
@ -48,4 +48,4 @@ export const GenericEntityDetails = ({ title: _title, tags: _tags, body: _body,
|
||||
);
|
||||
};
|
||||
|
||||
GenericEntityDetails.defaultProps = defaultProps;
|
||||
EntityProfile.defaultProps = defaultProps;
|
||||
@ -1,52 +0,0 @@
|
||||
import { EntityType } from '../../types.generated';
|
||||
|
||||
export { EntityType };
|
||||
|
||||
export const fromPathName = (pathName: string): EntityType => {
|
||||
switch (pathName) {
|
||||
case 'dataset':
|
||||
return EntityType.Dataset;
|
||||
case 'user':
|
||||
return EntityType.User;
|
||||
default:
|
||||
throw new Error(`Unrecognized pathName ${pathName} provided.`);
|
||||
}
|
||||
};
|
||||
|
||||
export const fromPathnameOrEmptyString = (pathName: string): EntityType | null => {
|
||||
if (pathName === '') return null;
|
||||
return fromPathName(pathName);
|
||||
};
|
||||
|
||||
export const toPathName = (type: EntityType): string => {
|
||||
switch (type) {
|
||||
case EntityType.Dataset:
|
||||
return 'dataset';
|
||||
case EntityType.User:
|
||||
return 'user';
|
||||
default:
|
||||
throw new Error(`Unrecognized type ${type} provided.`);
|
||||
}
|
||||
};
|
||||
|
||||
export const fromCollectionName = (name: string): EntityType => {
|
||||
switch (name) {
|
||||
case 'Datasets':
|
||||
return EntityType.Dataset;
|
||||
case 'Users':
|
||||
return EntityType.User;
|
||||
default:
|
||||
throw new Error(`Unrecognized name ${name} provided.`);
|
||||
}
|
||||
};
|
||||
|
||||
export const toCollectionName = (type: EntityType): string => {
|
||||
switch (type) {
|
||||
case EntityType.Dataset:
|
||||
return 'Datasets';
|
||||
case EntityType.User:
|
||||
return 'Users';
|
||||
default:
|
||||
throw new Error(`Unrecognized type ${type} provided.`);
|
||||
}
|
||||
};
|
||||
@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Avatar } from 'antd';
|
||||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { PageRoutes } from '../../conf/Global';
|
||||
import defaultAvatar from '../../images/default_avatar.png';
|
||||
import { EntityType } from '../../types.generated';
|
||||
import { useEntityRegistry } from '../useEntityRegistry';
|
||||
|
||||
interface Props {
|
||||
urn: string;
|
||||
@ -14,8 +15,9 @@ const defaultProps = {
|
||||
};
|
||||
|
||||
export const ManageAccount = ({ urn: _urn, pictureLink: _pictureLink }: Props) => {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
return (
|
||||
<Link to={`${PageRoutes.USERS}/${_urn}`}>
|
||||
<Link to={`${entityRegistry.getPathName(EntityType.User)}/${_urn}`}>
|
||||
<Avatar
|
||||
style={{
|
||||
marginRight: '15px',
|
||||
|
||||
9
datahub-web-react/src/components/useEntityRegistry.tsx
Normal file
9
datahub-web-react/src/components/useEntityRegistry.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { useContext } from 'react';
|
||||
import { EntityRegistryContext } from '../entityRegistryContext';
|
||||
|
||||
/**
|
||||
* Fetch an instance of EntityRegistry from the React context.
|
||||
*/
|
||||
export function useEntityRegistry() {
|
||||
return useContext(EntityRegistryContext);
|
||||
}
|
||||
@ -1,10 +1,3 @@
|
||||
import { EntityType } from '../components/shared/EntityTypeUtil';
|
||||
|
||||
/*
|
||||
Browsable Entity Types
|
||||
*/
|
||||
export const BROWSABLE_ENTITY_TYPES = [EntityType.Dataset];
|
||||
|
||||
/*
|
||||
Number of results shown per browse results page
|
||||
*/
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import DataHubLogo from '../images/landing-logo.png';
|
||||
|
||||
/*
|
||||
Reference to the Logo Image used in Log in page and in search header
|
||||
*/
|
||||
export const LOGO_IMAGE = DataHubLogo;
|
||||
|
||||
/*
|
||||
Top-level page route names
|
||||
Default top-level page route names (excludes entity pages)
|
||||
*/
|
||||
export enum PageRoutes {
|
||||
LOG_IN = '/login',
|
||||
|
||||
@ -1,32 +1,3 @@
|
||||
import { EntityType } from '../components/shared/EntityTypeUtil';
|
||||
|
||||
/*
|
||||
Searchable Entity Types
|
||||
*/
|
||||
export const SEARCHABLE_ENTITY_TYPES = [EntityType.Dataset, EntityType.User];
|
||||
|
||||
/*
|
||||
Default AutoComplete field by entity. Required if autocomplete is desired on a SEARCHABLE_ENTITY_TYPE.
|
||||
Note that we need to consider this further, because in some cases fields may differ: eg. ldap vs name search for Users.
|
||||
*/
|
||||
export const getAutoCompleteFieldName = (type: EntityType) => {
|
||||
switch (type) {
|
||||
case EntityType.Dataset:
|
||||
case EntityType.User:
|
||||
return 'name';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Whether to enable the 'All Entities' search type as the default entity type.
|
||||
|
||||
If false, the default search entity will be the first entry in
|
||||
SEARCH_ENTITY_TYPES.
|
||||
*/
|
||||
export const SHOW_ALL_ENTITIES_SEARCH_TYPE = true;
|
||||
|
||||
/*
|
||||
Placeholder text appearing in the search bar
|
||||
*/
|
||||
|
||||
4
datahub-web-react/src/entityRegistryContext.tsx
Normal file
4
datahub-web-react/src/entityRegistryContext.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import React from 'react';
|
||||
import EntityRegistry from './components/entity/EntityRegistry';
|
||||
|
||||
export const EntityRegistryContext = React.createContext(new EntityRegistry());
|
||||
Loading…
x
Reference in New Issue
Block a user