[#812,package][xl]: initial versioning of the package

This commit is contained in:
deme
2023-05-01 15:53:42 -03:00
parent cc43597130
commit 169a92d313
49 changed files with 11254 additions and 0 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
* post = {
* title: <The title of the blog post>
* content: <The body of the blog post. Can be plain text or html>
* createdAt: <The utc date when the post was last modified>.
* featuredImage: <Url/relative url to post cover image>
* }
* @returns
*/
const Post = ({ post }) => {
const { title, content, createdAt, featuredImage } = post;
return (
<>
<h1 className="text-3xl font-semibold text-primary my-6 inline-block">
{title}
</h1>
<p className="mb-6">Posted: {createdAt}</p>
<img src={featuredImage} className="mb-6" alt="featured_img" />
<div>{parse(content)}</div>
</>
);
};
Post.propTypes = {
page: PropTypes.shape({
title: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
createdAt: PropTypes.number,
featuredImage: PropTypes.string,
})
}
export default Post;

View File

@@ -0,0 +1,38 @@
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 (
<>
{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,227 @@
import { DataGrid } from '@mui/x-data-grid';
import PropTypes from 'prop-types';
import { useState } from 'react';
import FileDownloadIcon from '@mui/icons-material/FileDownload';
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
import FolderIcon from '@mui/icons-material/Folder';
/**
* 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 resources: A array of frictionless datapackage resource
*/
const DataExplorer = ({ resources, columnHeaderStyle }) => {
const [activeTable, setActiveTable] = useState(0);
const [previewMode, setPreviewMode] = useState(true);
const handleTableNameClick = (index) => {
setActiveTable(index);
}
const getDataGridTable = (resource, columnHeaderStyle) => {
return (
<DataGrid
sx={{
'& .table-column-header-style-class': {
backgroundColor: '#f5f5f5',
color: 'black',
...columnHeaderStyle,
},
}}
key={resource.name}
columns={generateColumns(resource)}
rows={prepareRows(resource)}
pageSize={5}
rowsPerPageOptions={[5]}
/>
)
}
const getDataGridSchema = (resource, columnHeaderStyle) => {
return (
<DataGrid
sx={{
'& .table-column-header-style-class': {
backgroundColor: '#f5f5f5',
color: 'black',
...columnHeaderStyle,
},
}}
key={resource.name}
columns={generateSchemaColumns(resource)}
rows={prepareSchemaRows(resource)}
pageSize={5}
rowsPerPageOptions={[5]}
/>
)
}
return (
<div className='grid grid-cols-12' >
<div className='col-span-3'>
<div className='flex'>
<FolderIcon />
<h1 className="font-bold ml-3">
Files
</h1>
</div>
<div className='flex-col'>
{
resources.map((resource, i) => {
return (
<div key={`res@${i}`} className='flex'>
<InsertDriveFileIcon className='ml-2' />
<button className='ml-3 focus:outline-none' id={i} onClick={() => handleTableNameClick(i)}>
{
i === activeTable ? (
<h3>{resource.name}.{resource.format}</h3>
) : (
<h3 className='text-gray-400'>{resource.name}.{resource.format}</h3>
)
}
</button>
</div>
)
})
}
</div>
</div>
<div className='col-span-9 border-2'>
<h1 className='font-bold ml-3 mb-2 capitalize'>{resources[activeTable].name}</h1>
<div className='flex'>
<div className='flex mr-3'>
<a href={resources[activeTable].path} >
<FileDownloadIcon className='ml-2' />
</a>
<span>
{resources[activeTable].size ? (formatResourceSize(resources[activeTable].size)) : 'N/A'}
</span>
</div>
<div className='mr-3 text-gray-500'>
|
</div>
<div className='flex mr-3'>
<span>
{resources[activeTable].sample.length} rows
</span>
</div>
<div className='mr-3 text-gray-500'>
|
</div>
<div className='flex mr-3'>
<span>
{resources[activeTable].schema.fields.length} columns
</span>
</div>
</div>
<div className='flex mt-5 mb-4'>
<button
className={`${previewMode && 'font-bold underline'} ml-3 mr-5 focus:outline-none`}
onClick={() => setPreviewMode(!previewMode)}
>
Preview
</button>
<button
className={`${!previewMode && 'font-bold underline'} ml-3 mr-5 focus:outline-none`}
onClick={() => setPreviewMode(!previewMode)}
>
Table Schema
</button>
</div>
{
previewMode && (
<div className='ml-3' style={{ height: "370px" }}>
{getDataGridTable(resources[activeTable], columnHeaderStyle)}
</div>
)
}
{
!previewMode && (
<div className='ml-3' style={{ height: "370px" }}>
{getDataGridSchema(resources[activeTable], columnHeaderStyle)}
</div>
)
}
</div>
</div>
);
}
const generateColumns = (resource) => {
return resource.schema?.fields.map((field) => {
return {
field: field.name,
headerName: field.name,
width: 150,
description: field.description,
headerClassName: 'table-column-header-style-class',
}
});
}
const prepareRows = (resource) => {
return resource.sample.map((row, i) => {
row['id'] = i
return row
})
}
const generateSchemaColumns = () => {
return [
{
field: "name",
headerName: "Field",
flex: 0.5,
description: "Field name",
headerClassName: 'table-column-header-style-class',
},
{
field: "type",
headerName: "Type",
width: 150,
description: "Field type",
headerClassName: 'table-column-header-style-class',
},
{
field: "description",
headerName: "Description",
flex: 1,
description: "Field description",
headerClassName: 'table-column-header-style-class',
}
]
}
const prepareSchemaRows = (resource) => {
return resource.schema?.fields.map((field, i) => {
field['id'] = i
return field
});
}
const formatResourceSize = (bytes) => {
if (bytes < 1024) {
return bytes + ' b';
} else if (bytes < 1048576) {
return (bytes / 1024).toFixed(2) + ' kb';
} else if (bytes < 1073741824) {
return (bytes / 1048576).toFixed(2) + ' mb';
} else {
return bytes
}
}
DataExplorer.propTypes = {
resources: PropTypes.array.isRequired,
}
export default DataExplorer

View File

@@ -0,0 +1,89 @@
import React from 'react';
import filesize from 'filesize'
import * as timeago from 'timeago.js';
import PropTypes from 'prop-types';
/**
* KeyInfo component receives two arguments.
* @param {Object} descriptor A Frictionless datapackage descriptor object with the following fields
* @param {Array} resources A Frictionless datapackage resource array
* @returns React Component
*/
const KeyInfo = ({ descriptor, resources }) => {
const formats = resources.map(item => item.format).join(', ');
return (
<>
<section className="m-8" name="key-info" id="key-info">
<h2 className="text-xl font-bold mb-4">Key info</h2>
<div className="grid grid-cols-7 gap-4">
<div>
<h3 className="text-1xl font-bold mb-2">Files</h3>
</div>
<div>
<h3 className="text-1xl font-bold mb-2">Size</h3>
</div>
<div>
<h3 className="text-1xl font-bold mb-2">Format</h3>
</div>
<div>
<h3 className="text-1xl font-bold mb-2">Created</h3>
</div>
<div>
<h3 className="text-1xl font-bold mb-2">Updated</h3>
</div>
<div>
<h3 className="text-1xl font-bold mb-2">Licenses</h3>
</div>
<div>
<h3 className="text-1xl font-bold mb-2">Sources</h3>
</div>
</div>
<div className="grid grid-cols-7 gap-4">
<div>
<h3 className="text-1xl">{resources.length}</h3>
</div>
<div>
<h3 className="text-1xl">{descriptor.size || 'N/A'}</h3>
</div>
<div>
<h3 className="text-1xl">{formats}</h3>
</div>
<div>
<h3 className="text-1xl">{descriptor.created && timeago.format(descriptor.created)}</h3>
</div>
<div>
<h3 className="text-1xl">{descriptor.updated && timeago.format(descriptor.updated)}</h3>
</div>
<div>
<h3 className="text-1xl">{
descriptor.licenses?.length && (descriptor.licenses.map((item, index) => (
<a className="text-yellow-600"
href={item.path || '#'} title={item.title || ''}
key={index}>
{item.name}
</a>
)))
}</h3>
</div>
<div>
<h3 className="text-1xl">{
descriptor.sources?.length && (descriptor.sources.map((item, index) => (
<a className="text-yellow-600" href={item.path} key={index}>
{item.title}
</a>
)))
}</h3>
</div>
</div>
</section>
</>
)
}
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

@@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
/**
* ReadMe component displays the markdown description of a datapackage
* @param {string} readme parsed html of data package readme
* @returns React Component
*/
const ReadMe = ({ readme }) => {
return (
<>
<section className="m-8" name="sample-table">
<div className="prose">
<div dangerouslySetInnerHTML={{ __html: readme }} />
</div>
</section>
</>
)
}
ReadMe.propTypes = {
readme: PropTypes.string.isRequired
}
export default ReadMe

View File

@@ -0,0 +1,74 @@
import React from 'react';
import filesize from 'filesize'
import * as timeago from 'timeago.js';
import PropTypes from 'prop-types';
/**
* ResourceInfo component displays all resources in a data package
* @param {Array} resources A Frictionless datapackage resource object
* @returns React Component
*/
const ResourcesInfo = ({ resources }) => {
return (
<>
<section className="m-8" name="file-list">
<div className="grid grid-cols-7 gap-4">
<div>
<h3 className="text-1xl font-bold mb-2">File</h3>
</div>
<div>
<h3 className="text-1xl font-bold mb-2">Description</h3>
</div>
<div>
<h3 className="text-1xl font-bold mb-2">Size</h3>
</div>
<div>
<h3 className="text-1xl font-bold mb-2">Created</h3>
</div>
<div>
<h3 className="text-1xl font-bold mb-2">Updated</h3>
</div>
<div>
<h3 className="text-1xl font-bold mb-2">Download</h3>
</div>
</div>
{resources.map((resource, index) => {
return (
<div key={`${index}_${resource.name}`} className="grid grid-cols-7 gap-4">
<div>
<h3 className="text-1xl">{resource.title || resource.name}</h3>
</div>
<div>
<h3 className="text-1xl">{resource.description || "No description"}</h3>
</div>
<div>
<h3 className="text-1xl">{resource.size ? filesize(resource.size, { bits: true }) : 0}</h3>
</div>
<div>
<h3 className="text-1xl">{resource.created && timeago.format(resource.created)}</h3>
</div>
<div>
<h3 className="text-1xl">{resource.updated && timeago.format(resource.updated)}</h3>
</div>
<div>
<h3 className="text-1xl">
{/* We assume that resource.path is a URL but not relative path. */}
<a className="text-yellow-600" href={resource.path}>
{resource.format}
</a>
</h3>
</div>
</div>
)
})}
</section>
</>
)
}
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,45 @@
import React, { useState } 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 = ({ handleSubmit }) => {
const [searchQuery, setSearchQuery] = useState("")
return (
<form onSubmit={(e) => e.preventDefault()} className="items-center">
<div className="flex">
<input
type="text"
name="search#q"
value={searchQuery}
onChange={(e) => { setSearchQuery(e.target.value) }}
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(searchQuery)}
type="button"
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>
</form>
);
};
Form.propTypes = {
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

@@ -0,0 +1,59 @@
import React from 'react'
import Link from 'next/link';
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) => {
event.preventDefault();
setOpen(!open);
};
return (
<nav className="flex items-center justify-between flex-wrap bg-white p-4 border-b border-gray-200">
<div className="flex items-center flex-shrink-0 text-gray-700 mr-6">
<Link href="/"><img src={logo} alt="portal logo" width="40" /></Link>
</div>
<div className="block lg:hidden mx-4">
<button
onClick={handleClick}
className="flex items-center px-3 py-2 border rounded text-gray-700 border-orange-400 hover:text-black hover:border-black"
>
<svg
className="fill-current h-3 w-3"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<title>Menu</title>
<path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z" />
</svg>
</button>
</div>
<div className={`${open ? `block` : `hidden`} lg:block`}>
{navMenu.map((menu, 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>)
})}
</div>
</nav>
);
};
Nav.propTypes = {
logo: PropTypes.string.isRequired,
navMenu: PropTypes.array.isRequired
}
export default Nav;

View File

@@ -0,0 +1,49 @@
import React from 'react';
import Link from 'next/link';
import PropTypes from 'prop-types';
/**
* Displays a list of recent datasets
* @param {array} props An array of datasets
* { datasets = [{
* organization: {name: <some name>, title: <some title> },
* title: <Data package title>
* name: <Data package name>
* description: <description of data package>
* }]
* }
* @returns React Component
*/
const Recent = ({datasets}) => {
return (
<section className="my-10 mx-4 lg:my-20">
<div className="recent flex flex-col lg:flex-row">
{datasets.map((dataset, index) => (
<div
key={index}
className="border px-4 mb-4 mr-3 border-gray-100 w-5/6 shadow-sm"
>
<h1 className="text-2xl font-thin">{dataset.title}</h1>
<p className="text-gray-500">
{dataset.organization && dataset.organization.description}
</p>
<Link
href={`/@${dataset.organization ? dataset.organization.name : 'dataset'}/${dataset.name}`}
>
<a className="pt-3 flex justify-end text-orange-500">
View Dataset
</a>
</Link>
</div>
))}
</div>
</section>
);
};
Recent.propTypes = {
datasets: PropTypes.array.isRequired
}
export default Recent;

View File

@@ -0,0 +1,34 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import createPlotlyComponent from "react-plotly.js/factory";
let Plot;
const PlotlyChart = ({spec}) => {
const [plotCreated, setPlotCreated] = useState(0) //0: false, 1: true
useEffect(() => {
import(`plotly.js-basic-dist`).then(Plotly => { //import Plotly dist when Page has been generated
Plot = createPlotlyComponent(Plotly);
setPlotCreated(1)
});
}, [])
if (!plotCreated) {
return (<div>Loading...</div>)
}
return (
<div data-testid="plotlyChart">
<Plot {...spec}
layout={{ autosize: true }}
style={{ width: "100%", height: "100%" }}
useResizeHandler={true}
/>
</div>
)
}
PlotlyChart.propTypes = {
spec: PropTypes.object.isRequired
}
export { PlotlyChart }

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { DataGrid } from '@mui/x-data-grid';
import PropTypes from 'prop-types';
/**
* 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 = ({ columns, data, height, width }) => {
let rows = [...data,]
rows = rows.map((row, i) => {
row['id'] = i
return row
})
return (
<div style={{ height, width }} data-testid="tableGrid">
<DataGrid rows={rows} columns={columns} pageSize={5} />
</div>
);
}
Table.propTypes = {
columns: PropTypes.array.isRequired,
data: PropTypes.array.isRequired
}
export default Table