mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-12 18:47:45 +00:00
feat(React Browse): Adding Browse Logic and misc refactorings (#2060)
Co-authored-by: John Joyce <jjoyce0510@gmail.com> Co-authored-by: Gabe Lyons <gabe@Gabes-MacBook-Pro.local>
This commit is contained in:
parent
50cec65f57
commit
bee6ba43f8
@ -27,9 +27,15 @@ module.exports = {
|
||||
'require-await': 'warn',
|
||||
'import/prefer-default-export': 'off', // TODO: remove this lint rule
|
||||
'react/jsx-props-no-spreading': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error', {
|
||||
varsIgnorePattern: '^_',
|
||||
argsIgnorePattern: '^_' }],
|
||||
'no-plusplus': 'off',
|
||||
'react/require-default-props': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
varsIgnorePattern: '^_',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
||||
@ -6,7 +6,8 @@ schema {
|
||||
}
|
||||
|
||||
type Query {
|
||||
dataset(urn: String): Dataset
|
||||
dataset(urn: String!): Dataset
|
||||
user(urn: String!): CorpUser
|
||||
search(input: SearchInput!): SearchResults
|
||||
autoComplete(input: AutoCompleteInput!): AutoCompleteResults
|
||||
browse(input: BrowseInput!): BrowseResults
|
||||
@ -303,7 +304,7 @@ input BrowseInput {
|
||||
|
||||
# Browse Output
|
||||
type BrowseResults {
|
||||
entities: [BrowseResultEntity]!
|
||||
entities: [BrowseResultEntity!]!
|
||||
start: Int!
|
||||
count: Int!
|
||||
total: Int!
|
||||
|
||||
@ -2,6 +2,7 @@ import { GetDatasetDocument } from './graphql/dataset.generated';
|
||||
import { GetBrowsePathsDocument, GetBrowseResultsDocument } from './graphql/browse.generated';
|
||||
import { GetAutoCompleteResultsDocument, GetSearchResultsDocument } from './graphql/search.generated';
|
||||
import { LoginDocument } from './graphql/auth.generated';
|
||||
import { GetUserDocument } from './graphql/user.generated';
|
||||
|
||||
const user1 = {
|
||||
username: 'sdas',
|
||||
@ -163,6 +164,21 @@ export const mocks = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GetUserDocument,
|
||||
variables: {
|
||||
urn: 'urn:li:corpuser:1',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
dataset: {
|
||||
...user1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GetBrowsePathsDocument,
|
||||
@ -179,6 +195,74 @@ export const mocks = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GetBrowseResultsDocument,
|
||||
variables: {
|
||||
input: {
|
||||
type: 'DATASET',
|
||||
path: [],
|
||||
start: 0,
|
||||
count: 20,
|
||||
filters: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
browse: {
|
||||
entities: [],
|
||||
start: 0,
|
||||
count: 0,
|
||||
total: 0,
|
||||
metadata: {
|
||||
path: [],
|
||||
groups: [
|
||||
{
|
||||
name: 'prod',
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
totalNumEntities: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GetBrowseResultsDocument,
|
||||
variables: {
|
||||
input: {
|
||||
type: 'DATASET',
|
||||
path: ['prod'],
|
||||
start: 0,
|
||||
count: 20,
|
||||
filters: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
browse: {
|
||||
entities: [],
|
||||
start: 0,
|
||||
count: 0,
|
||||
total: 0,
|
||||
metadata: {
|
||||
path: ['prod'],
|
||||
groups: [
|
||||
{
|
||||
name: 'hdfs',
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
totalNumEntities: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GetBrowseResultsDocument,
|
||||
@ -187,7 +271,8 @@ export const mocks = [
|
||||
type: 'DATASET',
|
||||
path: ['prod', 'hdfs'],
|
||||
start: 0,
|
||||
count: 10,
|
||||
count: 20,
|
||||
filters: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -196,8 +281,8 @@ export const mocks = [
|
||||
browse: {
|
||||
entities: [
|
||||
{
|
||||
name: 'The Great Test Dataset',
|
||||
urn: 'urn:li:dataset:1',
|
||||
name: 'Test Dataset',
|
||||
},
|
||||
],
|
||||
start: 0,
|
||||
@ -232,6 +317,26 @@ export const mocks = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GetAutoCompleteResultsDocument,
|
||||
variables: {
|
||||
input: {
|
||||
type: 'USER',
|
||||
query: 'j',
|
||||
field: 'ldap',
|
||||
},
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
autoComplete: {
|
||||
query: 'j',
|
||||
suggestions: ['jjoyce'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GetSearchResultsDocument,
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Switch, Route, RouteProps, Redirect } from 'react-router-dom';
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
import { BrowsePage } from './browse/BrowsePage';
|
||||
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';
|
||||
@ -27,10 +28,10 @@ const ProtectedRoute = ({
|
||||
*/
|
||||
export const Routes = (): JSX.Element => {
|
||||
const isLoggedIn = useReactiveVar(isLoggedInVar);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Switch>
|
||||
<ProtectedRoute isLoggedIn={isLoggedIn} exact path="/" render={() => <BrowseTypesPage />} />
|
||||
<Route path={PageRoutes.LOG_IN} component={LogIn} />
|
||||
<ProtectedRoute
|
||||
isLoggedIn={isLoggedIn}
|
||||
@ -38,7 +39,17 @@ export const Routes = (): JSX.Element => {
|
||||
render={() => <DatasetPage />}
|
||||
/>
|
||||
<ProtectedRoute isLoggedIn={isLoggedIn} path={PageRoutes.SEARCH} render={() => <SearchPage />} />
|
||||
<ProtectedRoute isLoggedIn={isLoggedIn} path={PageRoutes.BROWSE} render={() => BrowsePage} />
|
||||
<ProtectedRoute
|
||||
isLoggedIn={isLoggedIn}
|
||||
exact
|
||||
path={PageRoutes.BROWSE_TYPES}
|
||||
render={() => <BrowseTypesPage />}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
isLoggedIn={isLoggedIn}
|
||||
path={PageRoutes.BROWSE_RESULTS}
|
||||
render={() => <BrowseResultsPage />}
|
||||
/>
|
||||
<ProtectedRoute isLoggedIn={isLoggedIn} path={PageRoutes.USERS} render={() => <UserPage />} />
|
||||
<Route component={NoPageFound} />
|
||||
</Switch>
|
||||
|
||||
@ -32,6 +32,7 @@ export const LogIn: React.VFC<LogInProps> = () => {
|
||||
})
|
||||
.then(() => {
|
||||
Cookies.set('PLAY_SESSION', 'DUMMY_VALUE');
|
||||
Cookies.set('IS_LOGGED_IN', 'true');
|
||||
isLoggedInVar(true);
|
||||
})
|
||||
.catch((e: ApolloError) => {
|
||||
|
||||
@ -6,7 +6,7 @@ export const checkAuthStatus = (): boolean => {
|
||||
// TODO: perhaps there's a more robust way to detect this?
|
||||
// e.g. what happens if the PLAY_SESSION cookie is stuck but the session is
|
||||
// invalid or expired?
|
||||
return !!Cookies.get('PLAY_SESSION');
|
||||
return !!Cookies.get('IS_LOGGED_IN');
|
||||
};
|
||||
|
||||
export const isLoggedInVar = makeVar(checkAuthStatus());
|
||||
|
||||
@ -12,7 +12,7 @@ interface Props {
|
||||
}
|
||||
|
||||
/**
|
||||
* A page that includes a search header & entity browse path view
|
||||
* A entity-details page that includes a search header & entity browse path view
|
||||
*/
|
||||
export const BrowsableEntityPage = ({ urn: _urn, type: _type, children: _children }: Props) => {
|
||||
const { data } = useGetBrowsePathsQuery({ variables: { input: { urn: _urn, type: _type } } });
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* Needs Implemented!
|
||||
*/
|
||||
export const BrowsePage: React.FC = () => {
|
||||
// TODO: /browse?path=<entity-type>/sub/list
|
||||
return <div>Needs Implemented!</div>;
|
||||
};
|
||||
@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Breadcrumb, Row } from 'antd';
|
||||
import { EntityType, toCollectionName, toPathName } from '../shared/EntityTypeUtil';
|
||||
import { PageRoutes } from '../../conf/Global';
|
||||
import { EntityType, toCollectionName, toPathName } from '../shared/EntityTypeUtil';
|
||||
|
||||
interface Props {
|
||||
type: EntityType;
|
||||
@ -17,13 +17,11 @@ export const BrowsePath = ({ type, path }: Props) => {
|
||||
return parts.join('/');
|
||||
};
|
||||
|
||||
const baseBrowsePath = `${PageRoutes.BROWSE}?type=${toPathName(type)}`;
|
||||
const baseBrowsePath = `${PageRoutes.BROWSE}/${toPathName(type)}`;
|
||||
|
||||
const pathCrumbs = path.map((part, index) => (
|
||||
<Breadcrumb.Item>
|
||||
<Link to={`${baseBrowsePath}&path=${encodeURIComponent(createPartialPath(path.slice(0, index + 1)))}`}>
|
||||
{part}
|
||||
</Link>
|
||||
<Link to={`${baseBrowsePath}/${createPartialPath(path.slice(0, index + 1))}`}>{part}</Link>
|
||||
</Breadcrumb.Item>
|
||||
));
|
||||
|
||||
@ -31,7 +29,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}>{toCollectionName(type)}</Link>
|
||||
</Breadcrumb.Item>
|
||||
{pathCrumbs}
|
||||
</Breadcrumb>
|
||||
|
||||
22
datahub-web-react/src/components/browse/BrowseResultCard.tsx
Normal file
22
datahub-web-react/src/components/browse/BrowseResultCard.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { Card } from 'antd';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
type Props = {
|
||||
url: string;
|
||||
name: string;
|
||||
count?: number | undefined;
|
||||
};
|
||||
|
||||
export default function BrowseResultCard({ url, count, name }: Props) {
|
||||
return (
|
||||
<Link to={url}>
|
||||
<Card hoverable>
|
||||
<div style={{ display: 'flex', width: '100%' }}>
|
||||
<div style={{ fontSize: '12px', fontWeight: 'bold', color: '#0073b1' }}>{name}</div>
|
||||
{count && <div style={{ marginLeft: 'auto' }}>{count}</div>}
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
61
datahub-web-react/src/components/browse/BrowseResults.tsx
Normal file
61
datahub-web-react/src/components/browse/BrowseResults.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { Col, Pagination, Row } from 'antd';
|
||||
import { Content } from 'antd/lib/layout/layout';
|
||||
import React from 'react';
|
||||
import { BrowseResultEntity, BrowseResultGroup } from '../../types.generated';
|
||||
import BrowseResultCard from './BrowseResultCard';
|
||||
import { browseEntityResultToUrl } from './util/entityToUrl';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
rootPath: string;
|
||||
pageStart: number;
|
||||
pageSize: number;
|
||||
totalResults: number;
|
||||
groups: Array<BrowseResultGroup>;
|
||||
entities: Array<BrowseResultEntity>;
|
||||
onChangePage: (page: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display browse groups + entities.
|
||||
*/
|
||||
export const BrowseResults = ({
|
||||
title,
|
||||
rootPath,
|
||||
pageStart,
|
||||
pageSize,
|
||||
totalResults,
|
||||
entities,
|
||||
groups,
|
||||
onChangePage,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div>
|
||||
<Content style={{ backgroundColor: 'white', padding: '25px 100px' }}>
|
||||
<h1 className="ant-typography">{title}</h1>
|
||||
<Row gutter={[4, 8]}>
|
||||
{groups.map((group) => (
|
||||
<Col span={24}>
|
||||
<BrowseResultCard name={group.name} count={group.count} url={`${rootPath}/${group.name}`} />
|
||||
</Col>
|
||||
))}
|
||||
{entities.map((entity) => (
|
||||
<Col span={24}>
|
||||
<BrowseResultCard name={entity.name} url={browseEntityResultToUrl(entity)} />
|
||||
</Col>
|
||||
))}
|
||||
<Col span={24}>
|
||||
<Pagination
|
||||
style={{ width: '100%', display: 'flex', justifyContent: 'center', paddingTop: 16 }}
|
||||
current={pageStart}
|
||||
pageSize={pageSize}
|
||||
total={totalResults / pageSize}
|
||||
showLessItems
|
||||
onChange={onChangePage}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Content>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,74 @@
|
||||
import * as 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;
|
||||
|
||||
type BrowseResultsPageParams = {
|
||||
type: string;
|
||||
};
|
||||
|
||||
export const BrowseResultsPage = () => {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const { type } = useParams<BrowseResultsPageParams>();
|
||||
|
||||
const rootPath = location.pathname;
|
||||
const params = QueryString.parse(location.search);
|
||||
const entityType = fromPathName(type);
|
||||
const path = rootPath.split('/').slice(3);
|
||||
const page = Number(params.page) || 1;
|
||||
|
||||
const { data, loading, error } = useGetBrowseResultsQuery({
|
||||
variables: {
|
||||
input: {
|
||||
type: entityType,
|
||||
path,
|
||||
start: (page - 1) * RESULTS_PER_PAGE,
|
||||
count: RESULTS_PER_PAGE,
|
||||
filters: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const onChangePage = (newPage: number) => {
|
||||
history.push({
|
||||
pathname: rootPath,
|
||||
search: `&page=${newPage}`,
|
||||
});
|
||||
};
|
||||
|
||||
if (page < 0 || page === undefined || Number.isNaN(page)) {
|
||||
return <Redirect to={`${PageRoutes.BROWSE}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchablePage>
|
||||
<Affix offsetTop={64}>
|
||||
<BrowsePath type={entityType} path={path} />
|
||||
</Affix>
|
||||
{error && <p>Error fetching browse results!</p>}
|
||||
{loading && <p>Loading browse results...</p>}
|
||||
{data && data.browse && (
|
||||
<BrowseResults
|
||||
rootPath={rootPath}
|
||||
title={path.length > 0 ? path[path.length - 1] : toCollectionName(entityType)}
|
||||
pageSize={RESULTS_PER_PAGE}
|
||||
pageStart={page * RESULTS_PER_PAGE}
|
||||
groups={data.browse.metadata.groups}
|
||||
entities={data.browse.entities}
|
||||
totalResults={data.browse.total}
|
||||
onChangePage={onChangePage}
|
||||
/>
|
||||
)}
|
||||
</SearchablePage>
|
||||
);
|
||||
};
|
||||
42
datahub-web-react/src/components/browse/BrowseTypesPage.tsx
Normal file
42
datahub-web-react/src/components/browse/BrowseTypesPage.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import * as 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;
|
||||
|
||||
export const BrowseTypesPage = () => {
|
||||
return (
|
||||
<SearchablePage>
|
||||
<Content style={{ backgroundColor: 'white', padding: '25px 100px' }}>
|
||||
<h1 className="ant-typography">Browse</h1>
|
||||
<Row gutter={[16, 16]}>
|
||||
{BROWSABLE_ENTITY_TYPES.map((entityType) => (
|
||||
<Col xs={24} sm={24} md={8}>
|
||||
<Link to={`${PageRoutes.BROWSE}/${toPathName(entityType)}`}>
|
||||
<Card
|
||||
style={{
|
||||
padding: '30px 0px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
hoverable
|
||||
>
|
||||
<div style={{ fontSize: '18px', fontWeight: 'bold', color: '#0073b1' }}>
|
||||
{toCollectionName(entityType)}
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Content>
|
||||
</SearchablePage>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
// 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}`;
|
||||
}
|
||||
@ -22,6 +22,8 @@ interface RouteParams {
|
||||
urn: string;
|
||||
}
|
||||
|
||||
const EMPTY_OWNER_ARR: never[] = [];
|
||||
|
||||
/**
|
||||
* Responsible for display the Dataset Page
|
||||
*/
|
||||
@ -71,7 +73,7 @@ export const DatasetPage: React.VFC = () => {
|
||||
path: TabType.Ownership.toLowerCase(),
|
||||
content: (
|
||||
<OwnershipView
|
||||
owners={(ownership && ownership.owners) || []}
|
||||
initialOwners={(ownership && ownership.owners) || EMPTY_OWNER_ARR}
|
||||
lastModifiedAt={ownership && ownership.lastModified}
|
||||
/>
|
||||
),
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import { AutoComplete, Avatar, Button, Col, Row, Select, Table } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Owner, OwnershipType } from '../../../types.generated';
|
||||
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';
|
||||
|
||||
const OWNER_SEARCH_PLACEHOLDER = 'Enter an LDAP...';
|
||||
const NUMBER_OWNERS_REQUIRED = 2;
|
||||
|
||||
interface Props {
|
||||
owners: Array<Owner>;
|
||||
initialOwners: Array<Owner>;
|
||||
lastModifiedAt: number;
|
||||
}
|
||||
|
||||
@ -18,8 +19,11 @@ interface Props {
|
||||
*
|
||||
* TODO: Add mutations to change ownership on explicit save.
|
||||
*/
|
||||
export const Ownership: React.FC<Props> = ({ owners, lastModifiedAt }: Props): JSX.Element => {
|
||||
console.log(lastModifiedAt);
|
||||
export const Ownership: React.FC<Props> = ({
|
||||
initialOwners: _initialOwners,
|
||||
lastModifiedAt: _lastModifiedAt,
|
||||
}: Props): JSX.Element => {
|
||||
console.log(_lastModifiedAt);
|
||||
|
||||
const getOwnerTableData = (ownerArr: Array<Owner>) => {
|
||||
const rows = ownerArr.map((owner) => ({
|
||||
@ -33,45 +37,55 @@ export const Ownership: React.FC<Props> = ({ owners, lastModifiedAt }: Props): J
|
||||
return rows;
|
||||
};
|
||||
|
||||
const [ownerTableData, setOwnerTableData] = useState(getOwnerTableData(owners));
|
||||
const [owners, setOwners] = useState(_initialOwners);
|
||||
const [showAddOwner, setShowAddOwner] = useState(false);
|
||||
const [ownerSuggestions, setOwnerSuggestions] = useState(new Array<string>());
|
||||
const [getOwnerAutoCompleteResults, { data: searchOwnerSuggestionsData }] = useGetAutoCompleteResultsLazyQuery();
|
||||
|
||||
const onShowAddAnOwner = () => {
|
||||
setShowAddOwner(true);
|
||||
};
|
||||
|
||||
const onDeleteOwner = (urn: string) => {
|
||||
const newOwnerTableData = ownerTableData.filter((ownerRow) => !(ownerRow.urn === urn));
|
||||
setOwnerTableData(newOwnerTableData);
|
||||
const newOwners = owners.filter((owner: Owner) => !(owner.owner.urn === urn));
|
||||
setOwners(newOwners);
|
||||
};
|
||||
|
||||
const onOwnerQueryChange = (_: string) => {
|
||||
// TODO: Fetch real suggestions!
|
||||
setOwnerSuggestions(['jjoyce']);
|
||||
const onOwnerQueryChange = (query: string) => {
|
||||
getOwnerAutoCompleteResults({
|
||||
variables: {
|
||||
input: {
|
||||
type: EntityType.User,
|
||||
query,
|
||||
field: 'ldap',
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onSelectOwner = (ldap: string) => {
|
||||
// TODO: Remove sample suggestions.
|
||||
const newOwnerTableData = [
|
||||
...ownerTableData,
|
||||
// TODO: Remove this sample code.
|
||||
const newOwners = [
|
||||
...owners,
|
||||
{
|
||||
urn: 'urn:li:corpuser:ldap',
|
||||
ldap,
|
||||
fullName: 'John Joyce',
|
||||
type: 'USER',
|
||||
role: OwnershipType.Delegate,
|
||||
pictureLink: null,
|
||||
},
|
||||
owner: {
|
||||
urn: `urn:li:corpuser:${ldap}`,
|
||||
username: ldap,
|
||||
info: {
|
||||
fullName: 'John Joyce',
|
||||
},
|
||||
editableInfo: {
|
||||
pictureLink: null,
|
||||
},
|
||||
},
|
||||
type: OwnershipType.Delegate,
|
||||
} as Owner,
|
||||
];
|
||||
setOwnerTableData(newOwnerTableData);
|
||||
setOwners(newOwners);
|
||||
};
|
||||
|
||||
const onOwnershipTypeChange = (urn: string, type: string) => {
|
||||
const newOwnerTableData = ownerTableData.map((ownerRow) =>
|
||||
ownerRow.urn === urn ? { ...ownerRow, type } : ownerRow,
|
||||
);
|
||||
setOwnerTableData(newOwnerTableData);
|
||||
const onOwnershipTypeChange = (urn: string, type: OwnershipType) => {
|
||||
const newOwners = owners.map((owner: Owner) => (owner.owner.urn === urn ? { ...owner, type } : owner));
|
||||
setOwners(newOwners);
|
||||
};
|
||||
|
||||
const ownerTableColumns = [
|
||||
@ -137,7 +151,7 @@ export const Ownership: React.FC<Props> = ({ owners, lastModifiedAt }: Props): J
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Table pagination={false} columns={ownerTableColumns} dataSource={ownerTableData} />
|
||||
<Table pagination={false} columns={ownerTableColumns} dataSource={getOwnerTableData(owners)} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
@ -149,7 +163,14 @@ export const Ownership: React.FC<Props> = ({ owners, lastModifiedAt }: Props): J
|
||||
)}
|
||||
{showAddOwner && (
|
||||
<AutoComplete
|
||||
options={ownerSuggestions.map((result: string) => ({ value: result }))}
|
||||
options={
|
||||
(searchOwnerSuggestionsData &&
|
||||
searchOwnerSuggestionsData.autoComplete &&
|
||||
searchOwnerSuggestionsData.autoComplete.suggestions.map((result: string) => ({
|
||||
value: result,
|
||||
}))) ||
|
||||
[]
|
||||
}
|
||||
style={{
|
||||
width: 150,
|
||||
}}
|
||||
|
||||
@ -7,7 +7,7 @@ import { SearchablePage } from './SearchablePage';
|
||||
import { fromCollectionName, fromPathName, toCollectionName, toPathName } from '../shared/EntityTypeUtil';
|
||||
import { useGetSearchResultsQuery } from '../../graphql/search.generated';
|
||||
import { SearchResults } from './SearchResults';
|
||||
import { PlatformNativeType } from '../../types.generated';
|
||||
import { EntityType, PlatformNativeType } from '../../types.generated';
|
||||
import { SearchFilters } from './SearchFilters';
|
||||
import { SearchCfg } from '../../conf';
|
||||
import { PageRoutes } from '../../conf/Global';
|
||||
@ -24,21 +24,19 @@ export const SearchPage = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const params = QueryString.parse(location.search);
|
||||
const typeParam = params.type ? fromPathName(params.type as string) : SEARCHABLE_ENTITY_TYPES[0];
|
||||
const queryParam = params.query ? (params.query as string) : '';
|
||||
const pageParam = params.page && Number(params.page as string) > 0 ? Number(params.page as string) : 1;
|
||||
const filtersParam = location.state
|
||||
? ((location.state as any).filters as Array<{ field: string; value: string }>)
|
||||
: [];
|
||||
const type = params.type ? fromPathName(params.type as string) : SEARCHABLE_ENTITY_TYPES[0];
|
||||
const query = params.query ? (params.query as string) : '';
|
||||
const page = params.page && Number(params.page as string) > 0 ? Number(params.page as string) : 1;
|
||||
const filters = location.state ? ((location.state as any).filters as Array<{ field: string; value: string }>) : [];
|
||||
|
||||
const { loading, error, data } = useGetSearchResultsQuery({
|
||||
variables: {
|
||||
input: {
|
||||
type: typeParam,
|
||||
query: queryParam,
|
||||
start: (pageParam - 1) * RESULTS_PER_PAGE,
|
||||
type,
|
||||
query,
|
||||
start: (page - 1) * RESULTS_PER_PAGE,
|
||||
count: RESULTS_PER_PAGE,
|
||||
filters: filtersParam,
|
||||
filters,
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -47,30 +45,30 @@ export const SearchPage = () => {
|
||||
const entityType = fromCollectionName(newType);
|
||||
history.push({
|
||||
pathname: PageRoutes.SEARCH,
|
||||
search: `?type=${toPathName(entityType)}&query=${queryParam}&page=1`,
|
||||
search: `?type=${toPathName(entityType)}&query=${query}&page=1`,
|
||||
});
|
||||
};
|
||||
|
||||
const onFilterSelect = (selected: boolean, field: string, value: string) => {
|
||||
const newFilters = selected
|
||||
? [...filtersParam, { field, value }]
|
||||
: filtersParam.filter((filter) => filter.field !== field || filter.value !== value);
|
||||
? [...filters, { field, value }]
|
||||
: filters.filter((filter) => filter.field !== field || filter.value !== value);
|
||||
|
||||
history.push({
|
||||
pathname: PageRoutes.SEARCH,
|
||||
search: `?type=${toPathName(typeParam)}&query=${queryParam}&page=1`,
|
||||
search: `?type=${toPathName(type)}&query=${query}&page=1`,
|
||||
state: {
|
||||
filters: newFilters,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onResultsPageChange = (page: number) => {
|
||||
const onResultsPageChange = (newPage: number) => {
|
||||
return history.push({
|
||||
pathname: PageRoutes.SEARCH,
|
||||
search: `?type=${toPathName(typeParam)}&query=${queryParam}&page=${page}`,
|
||||
search: `?type=${toPathName(type)}&query=${query}&page=${newPage}`,
|
||||
state: {
|
||||
filters: [...filtersParam],
|
||||
filters: [...filters],
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -78,9 +76,6 @@ export const SearchPage = () => {
|
||||
const navigateToDataset = (urn: string) => {
|
||||
return history.push({
|
||||
pathname: `${PageRoutes.DATASETS}/${urn}`,
|
||||
state: {
|
||||
filters: [...filtersParam],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -126,32 +121,29 @@ export const SearchPage = () => {
|
||||
};
|
||||
};
|
||||
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
const toSearchResults = (elements: { __typename: string }[]) => {
|
||||
return elements.map((element) => {
|
||||
switch (element.__typename) {
|
||||
case 'Dataset':
|
||||
return toDatasetSearchResult(element as any);
|
||||
default:
|
||||
throw new Error('Non-dataset search type currently not supported!');
|
||||
}
|
||||
});
|
||||
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!`);
|
||||
}
|
||||
};
|
||||
|
||||
const searchResults = (data && data?.search && toSearchResults(data.search.elements)) || [];
|
||||
|
||||
return (
|
||||
<SearchablePage initialQuery={queryParam} initialType={typeParam}>
|
||||
<SearchablePage initialQuery={query} initialType={type}>
|
||||
<Layout.Content style={{ backgroundColor: 'white' }}>
|
||||
<Affix offsetTop={64}>
|
||||
<Tabs
|
||||
tabBarStyle={{ backgroundColor: 'white', padding: '0px 165px', marginBottom: '0px' }}
|
||||
defaultActiveKey={toCollectionName(typeParam)}
|
||||
defaultActiveKey={toCollectionName(type)}
|
||||
size="large"
|
||||
onChange={(newPath) => onSearchTypeChange(newPath)}
|
||||
>
|
||||
{SEARCHABLE_ENTITY_TYPES.map((type) => (
|
||||
<Tabs.TabPane tab={toCollectionName(type)} key={toCollectionName(type)} />
|
||||
{SEARCHABLE_ENTITY_TYPES.map((t) => (
|
||||
<Tabs.TabPane tab={toCollectionName(t)} key={toCollectionName(t)} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Affix>
|
||||
@ -159,7 +151,7 @@ export const SearchPage = () => {
|
||||
<Col style={{ margin: '24px 0px 0px 0px', padding: '0px 15px' }} span={6}>
|
||||
<SearchFilters
|
||||
facets={(data && data?.search && data.search.facets) || []}
|
||||
selectedFilters={filtersParam}
|
||||
selectedFilters={filters}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</Col>
|
||||
@ -168,7 +160,7 @@ export const SearchPage = () => {
|
||||
{error && !data && <p>Search error!</p>}
|
||||
{data && data?.search && (
|
||||
<SearchResults
|
||||
typeName={toCollectionName(typeParam)}
|
||||
typeName={toCollectionName(type)}
|
||||
results={searchResults}
|
||||
pageStart={data.search?.start}
|
||||
pageSize={data.search?.count}
|
||||
|
||||
@ -22,7 +22,7 @@ export const SearchResults = ({
|
||||
pageSize: _pageSize,
|
||||
totalResults: _totalResults,
|
||||
results: _results,
|
||||
onChangePage,
|
||||
onChangePage: _onChangePage,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Card
|
||||
@ -52,7 +52,7 @@ export const SearchResults = ({
|
||||
pageSize={_pageSize}
|
||||
total={_totalResults / _pageSize}
|
||||
showLessItems
|
||||
onChange={onChangePage}
|
||||
onChange={_onChangePage}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@ -4,3 +4,8 @@ import { EntityType } from '../components/shared/EntityTypeUtil';
|
||||
Browsable Entity Types
|
||||
*/
|
||||
export const BROWSABLE_ENTITY_TYPES = [EntityType.Dataset];
|
||||
|
||||
/*
|
||||
Number of results shown per browse results page
|
||||
*/
|
||||
export const RESULTS_PER_PAGE = 20;
|
||||
|
||||
@ -11,7 +11,8 @@ export const LOGO_IMAGE = DataHubLogo;
|
||||
export enum PageRoutes {
|
||||
LOG_IN = '/login',
|
||||
SEARCH = '/search',
|
||||
BROWSE = '/browse',
|
||||
BROWSE_TYPES = '/browse',
|
||||
BROWSE_RESULTS = '/browse/:type',
|
||||
DATASETS = '/datasets',
|
||||
USERS = '/users',
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ 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) {
|
||||
|
||||
@ -3,5 +3,6 @@ import * as Search from './Search';
|
||||
import * as Browse from './Browse';
|
||||
|
||||
// TODO: A way to populate configs without code changes?
|
||||
// TODO: Introduce Theme configs
|
||||
// TOOD: Entity-oriented configurations?
|
||||
// TODO: Theme configs
|
||||
export { Global as GlobalCfg, Search as SearchCfg, Browse as BrowseCfg };
|
||||
|
||||
17
datahub-web-react/src/graphql/user.graphql
Normal file
17
datahub-web-react/src/graphql/user.graphql
Normal file
@ -0,0 +1,17 @@
|
||||
query getUser($urn: String!) {
|
||||
user(urn: $urn) {
|
||||
urn
|
||||
username
|
||||
info {
|
||||
active
|
||||
displayName
|
||||
title
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
}
|
||||
editableInfo {
|
||||
pictureLink
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user