Merge pull request #545 from datopian/ref/portal-catalogs

Ref/portal catalogs
This commit is contained in:
Anuar Ustayev 2021-05-04 11:13:15 +06:00 committed by GitHub
commit 71f75142ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 449 additions and 58 deletions

View File

@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'prop-types';
import parse from 'html-react-parser';
/**
* Displays a blog post page
* @param {object} props
* {
* title: <The title of the blog post>
* content: <The body of the blog post>
* modified: <The utc date when the post was last modified.
* featured_image: <Url/relative url to post cover image
* }
* @returns
*/
const Post = ({ page }) => {
const { title, content, modified, featured_image } = page;
return (
<>
<h1 className="text-3xl font-semibold text-primary my-6 inline-block">
{title}
</h1>
<p className="mb-6">Edited: {modified}</p>
<img src={featured_image} className="mb-6" alt="featured_img" />
<div>{parse(content)}</div>
</>
);
};
Post.propTypes = {
page: PropTypes.shape({
title: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
modified: PropTypes.string,
featured_image: PropTypes.string,
})
}
export default Post;

View File

@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'prop-types';
import parse from 'html-react-parser';
/**
* Displays a list of blog posts with the title and a short excerp from the content.
* @param {object} props
* {
* posts: {
* title: <The title of the blog post>
* excerpt: <A short excerpt from the post content>
* }
* }
* @returns
*/
const PostList = ({ posts }) => {
return (
<>
<h1 className="text-3xl font-semibold text-primary my-6 inline-block">
{posts.length} posts found
</h1>
{posts.map((post, index) => (
<div key={index}>
<a
href={`/blog/${post.slug}`}
className="text-2xl font-semibold text-primary my-6 inline-block"
>
{parse(post.title)}
</a>
<p>{parse(post.excerpt)}</p>
</div>
))}
</>
);
};
PostList.propTypes = {
posts: PropTypes.object.isRequired
}
export default PostList;

View File

@ -0,0 +1,19 @@
import React from 'react'
import PropTypes from 'prop-types';
/**
* Opens a frictionless resource in data explorer. Data explorer gives you
* an interface to interact with a resource. That means you can do things like
* data filtering, sorting, e.t.c
* @param {object} resource A frictionless Data resource
* @returns React component
*/
const DataExplorer = ({ resource }) => {
// TODO: Add data explorer code
return <>{JSON.stringify(resource)}</>;
};
DataExplorer.propTypes = {
resource: PropTypes.object.isRequired
}
export default DataExplorer;

View File

@ -1,5 +1,6 @@
import React from 'react';
import filesize from 'filesize'
import PropTypes from 'prop-types';
/**
* KeyInfo component receives two arguments.
@ -91,4 +92,8 @@ const KeyInfo = ({ descriptor, resources }) => {
)
}
KeyInfo.propTypes = {
descriptor: PropTypes.object.isRequired,
resources: PropTypes.array.isRequired
}
export default KeyInfo

View File

@ -0,0 +1,45 @@
import Link from 'next/link';
import React from 'react'
import PropTypes from 'prop-types';
/**
* Displays information about an organization in a dataset page
* @param {Object} props object describing the dataset organization.
* organization: {
* image_url: The image url of the organization
* name: The name of the organization
* title: The title of the organization
* }
* @returns
*/
const Org = ({ organization }) => {
return (
<>
{organization ? (
<>
<img
src={
organization.image_url ||
'https://datahub.io/static/img/datahub-cube-edited.svg'
}
className="h-5 w-5 mr-2 inline-block"
alt="org_img"
/>
<Link href={`/@${organization.name}`}>
<a className="font-semibold text-primary underline">
{organization.title || organization.name}
</a>
</Link>
</>
) : (
''
)}
</>
);
};
Org.propTypes = {
organization: PropTypes.object.isRequired
}
export default Org;

View File

@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
/**
* ReadMe component displays the markdown description of a datapackage
@ -18,4 +19,8 @@ const ReadMe = ({ readmeHtml }) => {
)
}
ReadMe.propTypes = {
readmeHtml: PropTypes.string.isRequired
}
export default ReadMe

View File

@ -1,5 +1,6 @@
import React from 'react';
import filesize from 'filesize'
import PropTypes from 'prop-types';
/**
* ResourceInfo component displays all resources in a data package
@ -59,4 +60,8 @@ const ResourcesInfo = ({ resources }) => {
)
}
ResourcesInfo.propTypes = {
resources: PropTypes.array.isRequired
}
export default ResourcesInfo

View File

@ -0,0 +1,27 @@
import React from 'react'
import PropTypes from 'prop-types';
/**
* Creates a custom link with title
* @param {object} props
* {
* url: The url of the custom link
* title: The title for the custom link
* }
* @returns React Component
*/
const CustomLink = ({ url, title }) => (
<a
href={url}
className="bg-white hover:bg-gray-200 border text-black font-semibold py-2 px-4 rounded"
>
{title}
</a>
);
CustomLink.propTypes = {
url: PropTypes.string.isRequired,
title: PropTypes.string.isRequired
}
export default CustomLink;

View File

@ -0,0 +1,32 @@
import React from 'react'
import PropTypes from 'prop-types';
/**
* Error message component with consistent portal style
* @param {object} props
* {
* message: The error message to display
* }
* @returns
*/
const ErrorMessage = ({ message }) => {
return (
<aside>
{message}
<style jsx>{`
aside {
padding: 1.5em;
font-size: 14px;
color: white;
background-color: red;
}
`}</style>
</aside>
);
};
ErrorMessage.propTypes = {
message: PropTypes.string.isRequired
}
export default ErrorMessage;

View File

@ -0,0 +1,62 @@
import React from 'react'
import PropTypes from 'prop-types';
/**
* Search component form that can be customized with change and submit handlers
* @param {object} props
* {
* handleChange: A form input change event handler. This function is executed when the
* search input or order by input changes.
* handleSubmit: A form submit event handler. This function is executed when the
* search form is submitted.
* }
* @returns
*/
const Form = ({ handleChange, handleSubmit }) => {
return (
<form onSubmit={handleSubmit} className="items-center">
<div className="flex">
<input
type="text"
name="q"
value={q}
onChange={handleChange}
placeholder="Search"
aria-label="Search"
className="bg-white focus:outline-none focus:shadow-outline border border-gray-300 w-1/2 rounded-lg py-2 px-4 block appearance-none leading-normal"
/>
<button
onClick={handleSubmit}
className="inline-block text-sm px-4 py-3 mx-3 leading-none border rounded text-white bg-black border-black lg:mt-0"
>
Search
</button>
</div>
<div className="inline-block my-6 float-right">
<label htmlFor="field-order-by">Order by:</label>
<select
className="bg-white"
id="field-order-by"
name="sort"
onChange={handleChange}
onBlur={handleChange}
value={sort}
>
<option value="score:desc">Relevance</option>
<option value="title_string:asc">Name Ascending</option>
<option value="title_string:desc">Name Descending</option>
<option value="metadata_modified:desc">Last Modified</option>
<option value="views_recent:desc">Popular</option>
</select>
</div>
</form>
);
};
Form.propTypes = {
handleChange: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired
}
export default Form;

View File

@ -0,0 +1,55 @@
import React from 'react'
import Link from 'next/link';
import PropTypes from 'prop-types';
/**
* Single item from a search result showing info about a dataset.
* @param {object} props data package with the following format:
* {
* organization: {name: <some name>, title: <some title> },
* title: <Data package title>
* name: <Data package name>
* description: <description of data package>
* notes: <Notes associated with the data package>
* }
* @returns React Component
*/
const Item = ({ dataset }) => {
return (
<div className="mb-6">
<h3 className="text-xl font-semibold">
<Link
href={`/@${
dataset.organization
? dataset.organization.name
: 'dataset'
}/${dataset.name}`}
>
<a className="text-primary">
{dataset.title || dataset.name}
</a>
</Link>
</h3>
<Link
href={`/@${
dataset.organization ? dataset.organization.name : 'dataset'
}`}
>
<a className="text-gray-500 block mt-1">
{dataset.organization
? dataset.organization.title
: 'dataset'}
</a>
</Link>
<div className="leading-relaxed mt-2">
{dataset.description || dataset.notes}
</div>
</div>
);
};
Item.propTypes = {
dataset: PropTypes.object.isRequired
}
export default Item;

View File

@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
/**
* Displays the total search result
* @param {object} props
* {
* count: The total number of search results
* }
* @returns React Component
*/
const Total = ({ count }) => {
return (
<h1 className="text-3xl font-semibold text-primary my-6 inline-block">
{count} results found
</h1>
);
};
Total.propTypes = {
count: PropTypes.number.isRequired
}
export default Total;

View File

@ -1,6 +1,17 @@
import React from 'react'
import Link from 'next/link';
import React, { useState } from 'react';
import { useState } from 'react';
import PropTypes from 'prop-types';
/**
* Displays a navigation bar with logo and menu links
* @param {Object} props object with the following properties:
* {
* logo: The relative url to the logo image
* navMenu: An array of objects with menu items. E.g : [{ title: 'Blog', path: '/blog' },{ title: 'Search', path: '/search' }]
* }
* @returns React Component
*/
const Nav = ({ logo, navMenu }) => {
const [open, setOpen] = useState(false);
const handleClick = (event) => {
@ -30,41 +41,19 @@ const Nav = ({ logo, navMenu }) => {
</div>
<div className={`${open ? `block` : `hidden`} lg:block`}>
{navMenu.map((menu, index) => {
<Link href={menu.path} key={index}>
return (<Link href={menu.path} key={index}>
<a className="block mt-4 lg:inline-block lg:mt-0 active:bg-primary-background text-gray-700 hover:text-black mr-6">
{menu.title}
</a>
</Link>
</Link>)
})}
<Link href="/blog">
<a className="block mt-4 lg:inline-block lg:mt-0 active:bg-primary-background text-gray-700 hover:text-black mr-6">
Blog
</a>
</Link>
<Link href="/search">
<a className="block mt-4 lg:inline-block lg:mt-0 text-gray-700 hover:text-black mr-6">
Search
</a>
</Link>
<a
href="http://tech.datopian.com/frontend/"
className="block mt-4 lg:inline-block lg:mt-0 text-gray-700 hover:text-black mr-6"
target="_blank"
rel="noreferrer"
>
Docs
</a>
<a
href="https://github.com/datopian/portal"
className="inline-block text-tiny px-4 py-2 leading-none border rounded text-primary bg-primary-background border-black hover:border-gray-700 hover:text-gray-700 hover:bg-white mt-4 lg:mt-0"
target="_blank"
rel="noreferrer"
>
GitHub
</a>
</div>
</nav>
);
};
Nav.propTypes = {
logo: PropTypes.string.isRequired,
navMenu: PropTypes.string
}
export default Nav;

View File

@ -1,10 +1,20 @@
import React from 'react';
import Link from 'next/link';
import PropTypes from 'prop-types';
const Recent = (datasets) => {
if (!datasets) {
return <></>
}
/**
* Displays a list of recent datasets
* @param {object} props
* datasets = {
* organization: {name: <some name>, title: <some title> },
* title: <Data package title>
* name: <Data package name>
* description: <description of data package>
* notes: <Notes associated with the data package>
* }
* @returns React Component
*/
const Recent = ({datasets}) => {
return (
<section className="my-10 mx-4 lg:my-20">
@ -34,4 +44,8 @@ const Recent = (datasets) => {
);
};
Recent.propTypes = {
datasets: PropTypes.object.isRequired
}
export default Recent;

View File

@ -1,8 +1,9 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import createPlotlyComponent from "react-plotly.js/factory";
let Plot;
const PlotlyChart = (props) => {
const PlotlyChart = ({spec}) => {
const [plotCreated, setPlotCreated] = useState(0) //0: false, 1: true
useEffect(() => {
@ -18,7 +19,7 @@ const PlotlyChart = (props) => {
return (
<div data-testid="plotlyChart">
<Plot {...props.spec}
<Plot {...spec}
layout={{ autosize: true }}
style={{ width: "100%", height: "100%" }}
useResizeHandler={true}
@ -27,4 +28,7 @@ const PlotlyChart = (props) => {
)
}
PlotlyChart.propTypes = {
spec: PropTypes.object.isRequired
}
export { PlotlyChart }

View File

@ -1,24 +1,13 @@
import React from 'react';
import { DataGrid } from '@material-ui/data-grid';
import PropTypes from 'prop-types';
/**
* Displays a table from a Frictionless dataset
* @param schema: Frictionless Table Schema
* @param data: an array of data objects e.g. [ {a: 1, b: 2}, {a: 5, b: 7} ]
* Displays dataset in tabular form using data grid
* @param columns: An array of column names with properties: e.g [{field: "col1", headerName: "col1"}, {field: "col2", headerName: "col2"}]
* @param data: an array of data objects e.g. [ {col1: 1, col2: 2}, {col1: 5, col2: 7} ]
*/
const Table = ({ schema, data }) => {
const columns = schema.fields.map((field) => (
{
field: field.title || field.name,
headerName: field.name,
width: 300
}
))
data = data.map((item, index)=>{
item.id = index //Datagrid requires every row to have an ID
return item
})
const Table = ({ columns, data }) => {
return (
<div data-testid="tableGrid" style={{ height: 400, width: '100%' }}>
<DataGrid rows={data} columns={columns} pageSize={5} />
@ -26,4 +15,9 @@ const Table = ({ schema, data }) => {
);
}
Table.propTypes = {
columns: PropTypes.array.isRequired,
data: PropTypes.array.isRequired
}
export default Table

View File

@ -1,10 +1,38 @@
//Components
//View components
import Table from './components/views/Table'
import { PlotlyChart } from './components/views/Chart'
import KeyInfo from './components/page/KeyInfo'
import ResourceInfo from './components/page/ResourceInfo'
import ReadMe from './components/page/Readme'
import Nav from './components/ui/Home/Nav'
import Recent from './components/ui/Home/Recent'
export { Table, PlotlyChart, KeyInfo, ResourceInfo, ReadMe, Nav, Recent }
//Dataset components
import KeyInfo from './components/dataset/KeyInfo'
import ResourceInfo from './components/dataset/ResourceInfo'
import ReadMe from './components/dataset/Readme'
import DataExplorer from './components/dataset/DataExplorer'
import Org from './components/dataset/Org'
//Blog components
import Post from './components/blog/Post'
import PostList from './components/blog/PostList'
//Misc components
import Error from './components/misc/Error'
import CustomLink from './components/misc/CustomLink'
//UI components
import Nav from './components/ui/Nav'
import Recent from './components/ui/Recent'
export {
Table,
PlotlyChart,
KeyInfo,
ResourceInfo,
ReadMe,
Nav,
Recent,
DataExplorer,
Post,
PostList,
Org,
Error,
CustomLink
}