[examples][sm] - renaming ckan to ckan-2021

This commit is contained in:
Luccas Mateus de Medeiros Gomes
2023-04-13 21:24:25 -03:00
parent 64e97d3884
commit afc2101da0
76 changed files with 12 additions and 12 deletions

View File

@@ -1 +0,0 @@
DMS=https://demo.dev.datopian.com/

View File

@@ -1,31 +0,0 @@
{
"extends": [
"plugin:@nrwl/nx/react-typescript",
"next",
"next/core-web-vitals",
"../../.eslintrc.json"
],
"ignorePatterns": ["!**/*", ".next/**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
"@next/next/no-html-link-for-pages": ["error", "packages/ckan/pages"]
}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
],
"rules": {
"@next/next/no-html-link-for-pages": "off"
},
"env": {
"jest": true
}
}

View File

@@ -1,306 +0,0 @@
<h1 align="center">
🌀 Portal.JS<br/>
The javascript framework for<br/>
data portals
</h1>
🌀 `Portal` is a framework for rapidly building rich data portal frontends using a modern frontend approach (javascript, React, SSR).
`Portal` assumes a "decoupled" approach where the frontend is a separate service from the backend and interacts with backend(s) via an API. It can be used with any backend and has out of the box support for [CKAN][]. `portal` is built in Javascript and React on top of the popular [Next.js][] framework.
[ckan]: https://ckan.org/
[next.js]: https://nextjs.com/
Live DEMO: https://catalog-portal-js.vercel.app
## Features
- 🗺️ Unified sites: present data and content in one seamless site, pulling datasets from a DMS (e.g. CKAN) and content from a CMS (e.g. wordpress) with a common internal API.
- 👩‍💻 Developer friendly: built with familiar frontend tech Javascript, React etc
- 🔋 Batteries included: Full set of portal components out of the box e.g. catalog search, dataset showcase, blog etc.
- 🎨 Easy to theme and customize: installable themes, use standard CSS and React+CSS tooling. Add new routes quickly.
- 🧱 Extensible: quickly extend and develop/import your own React components
- 📝 Well documented: full set of documentation plus the documentation of NextJS and Apollo.
### For developers
- 🏗 Build with modern, familiar frontend tech such as Javascript and React.
- 🚀 NextJS framework: so everything in NextJS for free React, SSR, static site generation, huge number of examples and integrations etc.
- SSR => unlimited number of pages, SEO etc whilst still using React.
- Static Site Generation (SSG) (good for small sites) => ultra-simple deployment, great performance and lighthouse scores etc
- 📋 Typescript support
## Getting Started
### Setup
Install a recent version of Node. You'll need Node 10.13 or later.
### Create a Portal app
To create a Portal app, open your terminal, cd into the directory you'd like to create the app in, and run the following command:
```console
npm init portal-app my-data-portal
```
> NB: Under the hood, this uses the tool called create-next-app, which bootstraps a Next.js app for you. It uses this template through the --example flag.
>
> If it doesnt work, please open an issue.
## Guide
### Styling 🎨
We use Tailwind as a CSS framework. Take a look at `/styles/index.css` to see what we're importing from Tailwind bundle. You can also configure Tailwind using `tailwind.config.js` file.
Have a look at Next.js support of CSS and ways of writing CSS:
https://nextjs.org/docs/basic-features/built-in-css-support
### Backend
So far the app is running with mocked data behind. You can connect CMS and DMS backends easily via environment variables:
```console
$ export DMS=http://ckan:5000
$ export CMS=http://myblog.wordpress.com
```
> Note that we don't yet have implementations for the following CKAN features:
>
> - Activities
> - Auth
> - Groups
> - Facets
### Routes
These are the default routes set up in the "starter" app.
- Home `/`
- Search `/search`
- Dataset `/@org/dataset`
- Resource `/@org/dataset/r/resource`
- Organization `/@org`
- Collection (aka group in CKAN) (?) - suggest to merge into org
- Static pages, eg, `/about` etc. from CMS or can do it without external CMS, e.g., in Next.js
### New Routes
TODO
### Data fetching
We use Apollo client which allows us to query data with GraphQL. We have setup CKAN API for the demo (it uses demo.ckan.org as DMS):
http://portal.datopian1.now.sh/
Note that we don't have Apollo Server but we connect CKAN API using [`apollo-link-rest`](https://www.apollographql.com/docs/link/links/rest/) module. You can see how it works in [lib/apolloClient.ts](https://github.com/datopian/portal/blob/master/lib/apolloClient.ts) and then have a look at [pages/\_app.tsx](https://github.com/datopian/portal/blob/master/pages/_app.tsx).
For development/debugging purposes, we suggest installing the Chrome extension - https://chrome.google.com/webstore/detail/apollo-client-developer-t/jdkknkkbebbapilgoeccciglkfbmbnfm.
#### i18n configuration
Portal.js is configured by default to support both `English` and `French` subpath for language translation. But for subsequent users, this following steps can be used to configure i18n for other languages;
1. Update `next.config.js`, to add more languages to the i18n locales
```js
i18n: {
locales: ['en', 'fr', 'nl-NL'], // add more language to the list
defaultLocale: 'en', // set the default language to use
},
```
2. Create a folder for the language in `locales` --> `locales/en-Us`
3. In the language folder, different namespace files (json) can be created for each translation. For the `index.js` use-case, I named it `common.json`
```json
// locales/en/common.json
{
"title" : "Portal js in English",
}
// locales/fr/common.json
{
"title" : "Portal js in French",
}
```
4. To use on pages using Server-side Props.
```js
import { loadNamespaces } from './_app';
import useTranslation from 'next-translate/useTranslation';
const Home: React.FC = ()=> {
const { t } = useTranslation();
return (
<div>{t(`common:title`)}</div> // we use common and title base on the common.json data
);
};
export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
........ ........
return {
props : {
_ns: await loadNamespaces(['common'], locale),
}
};
};
```
5. Go to the browser and view the changes using language subpath like this `http://localhost:3000` and `http://localhost:3000/fr`. **Note** The subpath also activate chrome language Translator
#### Pre-fetch data in the server-side
When visiting a dataset page, you may want to fetch the dataset metadata in the server-side. To do so, you can use `getServerSideProps` function from NextJS:
```javascript
import { GetServerSideProps } from 'next';
import { initializeApollo } from '../lib/apolloClient';
import gql from 'graphql-tag';
const QUERY = gql`
query dataset($id: String) {
dataset(id: $id) @rest(type: "Response", path: "package_show?{args}") {
result
}
}
`;
...
export const getServerSideProps: GetServerSideProps = async (context) => {
const apolloClient = initializeApollo();
await apolloClient.query({
query: QUERY,
variables: {
id: 'my-dataset'
},
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
},
};
};
```
This would fetch the data from DMS and save it in the Apollo cache so that we can query it again from the components.
#### Access data from a component
Consider situation when rendering a component for org info on the dataset page. We already have pre-fetched dataset metadata that includes `organization` property with attributes such as `name`, `title` etc. We can now query only organization part for our `Org` component:
```javascript
import { useQuery } from '@apollo/react-hooks';
import gql from 'graphql-tag';
export const GET_ORG_QUERY = gql`
query dataset($id: String) {
dataset(id: $id) @rest(type: "Response", path: "package_show?{args}") {
result {
organization {
name
title
image_url
}
}
}
}
`;
export default function Org({ variables }) {
const { loading, error, data } = useQuery(
GET_ORG_QUERY,
{
variables: { id: 'my-dataset' }
}
);
...
const { organization } = data.dataset.result;
return (
<>
{organization ? (
<>
<img
src={
organization.image_url
}
className="h-5 w-5 mr-2 inline-block"
/>
<Link href={`/@${organization.name}`}>
<a className="font-semibold text-primary underline">
{organization.title || organization.name}
</a>
</Link>
</>
) : (
''
)}
</>
);
}
```
#### Add a new data source
TODO
## Developers
### Boot the local instance
Install the dependencies:
```bash
yarn # or npm i
```
Boot the demo portal:
```console
$ yarn dev # or npm run dev
```
Open [http://localhost:3000](http://localhost:3000) to see the home page 🎉
You can start editing the page by modifying `/pages/index.tsx`. The page auto-updates as you edit the file.
### Tests
We use Jest for running tests:
```bash
yarn test # or npm run test
# turn on watching
yarn test --watch
```
We use Cypress tests as well
```
yarn run e2e
```
### Architecture
- Language: Javascript
- Framework: NextJS - https://nextjs.org/
- Data layer API: GraphQL using Apollo. So controllers access data using GraphQL “gatsby like”
### Key Pages
See https://tech.datopian.com/frontend/

View File

@@ -1,23 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react';
import Form from '../../../components/search/Form';
const useRouter = jest.spyOn(require('next/router'), 'useRouter');
test('📸 of Form component with empty', () => {
useRouter.mockImplementationOnce(() => ({
query: { search: '', sort: '' },
}));
const { container } = render(<Form />);
expect(container).toMatchSnapshot();
});
test('📸 of Form component with query', () => {
useRouter.mockImplementationOnce(() => ({
query: { search: 'gdp', sort: '' },
}));
const { container } = render(<Form />);
expect(container).toMatchSnapshot();
});

View File

@@ -1,47 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`📸 of Form component with empty 1`] = `
<div>
<form
class="items-center"
>
<input
aria-label="Search"
class="inline-block w-1/2 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
id="search2"
name="search"
placeholder="GDP data..."
type="search"
/>
<button
class="inline-block text-sm px-4 py-3 mx-3 leading-none border rounded text-white bg-black border-black lg:mt-0"
type="button"
>
Search
</button>
</form>
</div>
`;
exports[`📸 of Form component with query 1`] = `
<div>
<form
class="items-center"
>
<input
aria-label="Search"
class="inline-block w-1/2 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
id="search2"
name="search"
placeholder="GDP data..."
type="search"
/>
<button
class="inline-block text-sm px-4 py-3 mx-3 leading-none border rounded text-white bg-black border-black lg:mt-0"
type="button"
>
Search
</button>
</form>
</div>
`;

View File

@@ -1,15 +0,0 @@
type LinkProps = {
url: string;
format: any;
};
const CustomLink: React.FC<LinkProps> = ({ url, format }: LinkProps) => (
<a
href={url}
className="bg-white hover:bg-gray-200 border text-black font-semibold py-2 px-4 rounded"
>
{format}
</a>
);
export default CustomLink;

View File

@@ -1,17 +0,0 @@
const ErrorMessage: React.FC<{ message: any }> = ({ message }) => {
return (
<aside>
{message}
<style jsx>{`
aside {
padding: 1.5em;
font-size: 14px;
color: white;
background-color: red;
}
`}</style>
</aside>
);
};
export default ErrorMessage;

View File

@@ -1,53 +0,0 @@
interface TableProps {
columns: Array<any>;
data: Array<any>;
className?: string;
}
const Table: React.FC<TableProps> = ({ columns, data, className }) => {
return (
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
<table
className={`min-w-full divide-y divide-gray-200 ${className}`}
>
<thead className="bg-gray-50">
<tr>
{columns.map(({ key, name }) => (
<th
key={key}
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
{name}
</th>
))}
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{data.map((item) => (
<tr key={item.id}>
{columns.map(({ key, render }) => (
<td
key={key}
className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"
>
{(render &&
typeof render === 'function' &&
render(item)) ||
item[key] ||
''}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
};
export default Table;

View File

@@ -1,5 +0,0 @@
import Table from './Table';
import ErrorMessage from './Error';
import CustomLink from './CustomLink';
export { Table, ErrorMessage, CustomLink };

View File

@@ -1,83 +0,0 @@
import { useQuery } from '@apollo/react-hooks';
import * as timeago from 'timeago.js';
import { ErrorMessage } from '../_shared';
import { GET_DATASET_QUERY } from '../../graphql/queries';
const About: React.FC<{ variables: any }> = ({ variables }) => {
const { loading, error, data } = useQuery(GET_DATASET_QUERY, {
variables,
// Setting this value to true will make the component rerender when
// the "networkStatus" changes, so we are able to know if it is fetching
// more data
notifyOnNetworkStatusChange: true,
});
if (error) return <ErrorMessage message="Error loading dataset." />;
if (loading) return <div>Loading</div>;
const { result } = data.dataset;
const stats = [
{ name: 'Files', stat: result.resources.length },
{ name: 'Size', stat: result.size || 'N/A' },
{
name: 'Formats',
stat: result.resources.map((item) => item.format).join(', '),
},
{
name: 'Created',
stat: result.created && timeago.format(result.created),
},
{
name: 'Updated',
stat: result.updated && timeago.format(result.updated),
},
{
name: 'Licenses',
stat: result.licenses?.length
? result.licenses.map((item, index) => (
<a
className="text-yellow-600"
href={item.path || '#'}
title={item.title || ''}
key={index}
>
{item.name}
</a>
))
: 'N/A',
},
];
return (
<>
<div className="pb-5 border-b border-gray-200">
<h1 className="text-3xl leading-6 font-medium text-gray-900">
{result.title || result.name}
</h1>
<p className="mt-2 max-w-4xl text-sm text-gray-500">
{result.description || 'This dataset does not have a description.'}
</p>
</div>
<div className="mb-5">
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
{stats.map((item) => (
<div
key={item.name}
className="px-4 py-5 bg-white shadow rounded-lg overflow-hidden sm:p-6"
>
<dt className="text-sm font-medium text-gray-500 truncate">
{item.name}
</dt>
<dd className="mt-1 text-3xl font-semibold text-gray-900">
{item.stat}
</dd>
</div>
))}
</dl>
</div>
</>
);
};
export default About;

View File

@@ -1,23 +0,0 @@
import { useQuery } from '@apollo/react-hooks';
import { ErrorMessage } from '../_shared';
import { GET_DATASET_QUERY } from '../../graphql/queries';
import { Org } from '@portaljs/portaljs-components';
const OrgInfo: React.FC<{ variables: any }> = ({ variables }) => {
const { loading, error, data } = useQuery(GET_DATASET_QUERY, {
variables,
// Setting this value to true will make the component rerender when
// the "networkStatus" changes, so we are able to know if it is fetching
// more data
notifyOnNetworkStatusChange: true,
});
if (error) return <ErrorMessage message="Error loading dataset." />;
if (loading) return <div>Loading</div>;
const { organization } = data.dataset.result;
return <Org organization={organization} />;
};
export default OrgInfo;

View File

@@ -1,76 +0,0 @@
/* eslint-disable jsx-a11y/anchor-is-valid */
/* eslint-disable react/display-name */
import Link from 'next/link';
import { useQuery } from '@apollo/react-hooks';
import * as timeago from 'timeago.js';
import { Table, ErrorMessage } from '../_shared';
import { GET_DATASET_QUERY } from '../../graphql/queries';
const columns = [
{
name: 'File',
key: 'file',
render: ({ name: resName, title, parentName }) => (
<Link className="underline" href={`${parentName}/r/${resName}`}>
{title || resName}
</Link>
),
},
{
name: 'Format',
key: 'format',
},
{
name: 'Created',
key: 'created',
render: ({ created }) => timeago.format(created),
},
{
name: 'Updated',
key: 'updated',
render: ({ updated }) => timeago.format(updated),
},
{
name: 'Link',
key: 'link',
render: ({ name: resName, parentName }) => (
<Link className="underline" href={`${parentName}/r/${resName}`}>
Preview
</Link>
),
},
];
const Resources: React.FC<{ variables: any }> = ({ variables }) => {
const { loading, error, data } = useQuery(GET_DATASET_QUERY, {
variables,
// Setting this value to true will make the component rerender when
// the "networkStatus" changes, so we are able to know if it is fetching
// more data
notifyOnNetworkStatusChange: true,
});
if (error) return <ErrorMessage message="Error loading dataset." />;
if (loading) return <div>Loading</div>;
const { result } = data.dataset;
return (
<div className="mt-12">
<div className="pb-5 border-b border-gray-200">
<h1 className="text-2xl leading-6 font-medium text-gray-900">
Data files
</h1>
</div>
<Table
columns={columns}
data={result.resources.map((resource) => ({
...resource,
parentName: result.name,
}))}
/>
</div>
);
};
export default Resources;

View File

@@ -1,61 +0,0 @@
const Footer: React.FC = () => {
const navigation = {
main: [
{ name: 'Blog', href: '/blog' },
{ name: 'Search', href: '/search' },
{ name: 'Docs', href: '/docs' },
],
social: [
{
name: 'GitHub',
href: 'https://github.com/datopian/portaljs',
// eslint-disable-next-line
icon: (props) => (
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
<path
fillRule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clipRule="evenodd"
/>
</svg>
),
},
],
};
return (
<footer className="bg-white">
<div className="max-w-7xl mx-auto py-12 px-4 overflow-hidden sm:px-6 lg:px-8">
<nav
className="-mx-5 -my-2 flex flex-wrap justify-center"
aria-label="Footer"
>
{navigation.main.map((item) => (
<div key={item.name} className="px-5 py-2">
<a
href={item.href}
className="text-base text-gray-500 hover:text-gray-900"
>
{item.name}
</a>
</div>
))}
</nav>
<div className="mt-8 flex justify-center space-x-6">
{navigation.social.map((item) => (
<a
key={item.name}
href={item.href}
className="text-gray-400 hover:text-gray-500"
>
<span className="sr-only">{item.name}</span>
<item.icon className="h-6 w-6" aria-hidden="true" />
</a>
))}
</div>
</div>
</footer>
);
};
export default Footer;

View File

@@ -1,7 +0,0 @@
import Template from './HeroTemplate';
const Hero: React.FC = () => {
return <Template />;
};
export default Hero;

View File

@@ -1,130 +0,0 @@
import useTranslation from 'next-translate/useTranslation';
import SearchForm from '../search/Form';
export default function Example() {
const { t } = useTranslation();
return (
<div className="relative bg-white overflow-hidden">
<div
className="hidden lg:block lg:absolute lg:inset-0"
aria-hidden="true"
>
<svg
className="absolute top-0 left-1/2 transform translate-x-64 -translate-y-8"
width={640}
height={784}
fill="none"
viewBox="0 0 640 784"
>
<defs>
<pattern
id="9ebea6f4-a1f5-4d96-8c4e-4c2abf658047"
x={118}
y={0}
width={20}
height={20}
patternUnits="userSpaceOnUse"
>
<rect
x={0}
y={0}
width={4}
height={4}
className="text-gray-200"
fill="currentColor"
/>
</pattern>
</defs>
<rect
y={72}
width={640}
height={640}
className="text-gray-50"
fill="currentColor"
/>
<rect
x={118}
width={404}
height={784}
fill="url(#9ebea6f4-a1f5-4d96-8c4e-4c2abf658047)"
/>
</svg>
</div>
<div className="relative pt-6 pb-16 sm:pb-24 lg:pb-32">
<main className="mt-16 mx-auto max-w-7xl px-4 sm:mt-24 sm:px-6 lg:mt-32">
<div className="lg:grid lg:grid-cols-12 lg:gap-8">
<div className="sm:text-center md:max-w-2xl md:mx-auto lg:col-span-6 lg:text-left">
<h1>
<span className="block text-sm font-semibold uppercase tracking-wide text-gray-500 sm:text-base lg:text-sm xl:text-base">
Quality Data ready to Integrate
</span>
<span className="mt-1 block text-4xl tracking-tight font-extrabold sm:text-5xl xl:text-6xl">
<span className="block text-gray-900">Find and Share</span>
<span className="block text-indigo-600">Quality Data</span>
</span>
</h1>
<p className="mt-3 text-base text-gray-500 sm:mt-5 sm:text-xl lg:text-lg xl:text-xl">
{t(`common:description`)}
</p>
<div className="mt-8 sm:max-w-lg sm:mx-auto sm:text-center lg:text-left lg:mx-0">
<SearchForm />
</div>
</div>
<div className="mt-12 relative sm:max-w-lg sm:mx-auto lg:mt-0 lg:max-w-none lg:mx-0 lg:col-span-6 lg:flex lg:items-center">
<svg
className="absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-8 scale-75 origin-top sm:scale-100 lg:hidden"
width={640}
height={784}
fill="none"
viewBox="0 0 640 784"
aria-hidden="true"
>
<defs>
<pattern
id="4f4f415c-a0e9-44c2-9601-6ded5a34a13e"
x={118}
y={0}
width={20}
height={20}
patternUnits="userSpaceOnUse"
>
<rect
x={0}
y={0}
width={4}
height={4}
className="text-gray-200"
fill="currentColor"
/>
</pattern>
</defs>
<rect
y={72}
width={640}
height={640}
className="text-gray-50"
fill="currentColor"
/>
<rect
x={118}
width={404}
height={784}
fill="url(#4f4f415c-a0e9-44c2-9601-6ded5a34a13e)"
/>
</svg>
<div className="relative mx-auto w-full rounded-lg shadow-lg lg:max-w-md">
<img
className="w-full"
src="/images/banner.svg"
alt="banner_img"
/>
</div>
</div>
</div>
</main>
</div>
</div>
);
}

View File

@@ -1,14 +0,0 @@
import Template from './NavTemplate';
const NavBar: React.FC = () => {
const navMenu = [
{ title: 'Blog', path: '/blog' },
{ title: 'Search', path: '/search' },
{ title: 'Docs', path: 'http://tech.datopian.com/frontend/' },
{ title: 'GitHub', path: 'https://github.com/datopian/portaljs' },
];
return <Template menu={navMenu} logo={'/images/logo.svg'} />;
};
export default NavBar;

View File

@@ -1,117 +0,0 @@
import { useState } from 'react';
import { useRouter } from 'next/router';
import { Disclosure } from '@headlessui/react';
import { Bars3Icon, MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/24/solid';
const NavBar: React.FC<{ menu: any; logo: string }> = ({ menu, logo }) => {
const router = useRouter();
const [searchQuery, setSearchQuery] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
router.push({
pathname: '/search',
query: { q: searchQuery },
});
};
return (
<Disclosure as="nav" className="bg-white shadow">
{({ open }) => (
<>
<div className="max-w-7xl mx-auto px-2 sm:px-4 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex px-2 lg:px-0">
<div className="flex-shrink-0 flex items-center">
<a href="/">
<img
className="block lg:hidden h-8 w-auto"
src={logo}
alt="Portal.js"
/>
<img
className="hidden lg:block h-8 w-auto"
src={logo}
alt="Portal.js"
/>
</a>
</div>
<div className="hidden lg:ml-6 lg:flex lg:space-x-8">
{/* Current: "border-indigo-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" */}
{menu.map((item, index) => (
<a
key={'menu-link' + index}
href={item.path}
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
{item.title}
</a>
))}
</div>
</div>
<div className="flex-1 flex items-center justify-center px-2 lg:ml-6 lg:justify-end">
<div className="max-w-lg w-full lg:max-w-xs">
<label htmlFor="search" className="sr-only">
Search
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<MagnifyingGlassIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</div>
<form
onSubmit={(e) => handleSubmit(e)}
className="items-center"
>
<input
id="search"
type="search"
name="search"
onChange={(e) => {
setSearchQuery(e.target.value);
}}
placeholder="Search"
aria-label="Search"
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</form>
</div>
</div>
</div>
<div className="flex items-center lg:hidden">
{/* Mobile menu button */}
<Disclosure.Button className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500">
<span className="sr-only">Open main menu</span>
{open ? (
<XMarkIcon className="block h-6 w-6" aria-hidden="true" />
) : (
<Bars3Icon className="block h-6 w-6" aria-hidden="true" />
)}
</Disclosure.Button>
</div>
</div>
</div>
<Disclosure.Panel className="lg:hidden">
<div className="pt-2 pb-3 space-y-1">
{/* Current: "bg-indigo-50 border-indigo-500 text-indigo-700", Default: "border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800" */}
{menu.map((item, index) => (
<a
key={'mobile-menu-link' + index}
href={item.path}
className="border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800 block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
>
{item.title}
</a>
))}
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};
export default NavBar;

View File

@@ -1,96 +0,0 @@
/* eslint-disable jsx-a11y/anchor-is-valid */
import { useQuery } from '@apollo/react-hooks';
import {
PresentationChartBarIcon,
RectangleStackIcon,
} from '@heroicons/react/24/solid';
import { ErrorMessage } from '../_shared';
import { SEARCH_QUERY } from '../../graphql/queries';
const RecentDataset: React.FC = () => {
const { loading, error, data } = useQuery(SEARCH_QUERY, {
variables: {
sort: 'metadata_created desc',
rows: 3,
},
// Setting this value to true will make the component rerender when
// the "networkStatus" changes, so we are able to know if it is fetching
// more data
notifyOnNetworkStatusChange: true,
});
if (error) return <ErrorMessage message="Error loading search results." />;
if (loading) return <div>Loading</div>;
const { result } = data.search;
return (
<section className="my-10 p-4">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Recent Datasets
</h3>
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{result.results.map((dataset) => (
<li
key={dataset.id}
className="col-span-1 bg-white rounded-lg shadow divide-y divide-gray-200"
>
<div className="w-full flex items-center justify-between p-6 space-x-6">
<div className="flex-1 truncate">
<div className="flex items-center space-x-3">
<h3 className="text-gray-900 text-sm font-medium truncate">
{dataset.title || dataset.name}
</h3>
<span className="flex-shrink-0 inline-block px-2 py-0.5 text-green-800 text-xs font-medium bg-green-100 rounded-full">
dataset
</span>
</div>
<p className="mt-1 text-gray-500 text-sm truncate">
{dataset.organization.title || dataset.organization.name}
</p>
</div>
<img
className="w-10 h-10 bg-gray-300 rounded-full flex-shrink-0"
src={
dataset.organization.image ||
'https://datahub.io/static/img/datahub-cube-edited.svg'
}
alt=""
/>
</div>
<div>
<div className="-mt-px flex divide-x divide-gray-200">
<div className="w-0 flex-1 flex">
<a
href={`/@${dataset.organization.name}`}
className="relative -mr-px w-0 flex-1 inline-flex items-center justify-center py-4 text-sm text-gray-700 font-medium border border-transparent rounded-bl-lg hover:text-gray-500"
>
<RectangleStackIcon
className="w-5 h-5 text-gray-400"
aria-hidden="true"
/>
<span className="ml-3">Organization</span>
</a>
</div>
<div className="-ml-px w-0 flex-1 flex">
<a
href={`/@${dataset.organization.name}/${dataset.name}`}
className="relative w-0 flex-1 inline-flex items-center justify-center py-4 text-sm text-gray-700 font-medium border border-transparent rounded-br-lg hover:text-gray-500"
>
<PresentationChartBarIcon
className="w-5 h-5 text-gray-400"
aria-hidden="true"
/>
<span className="ml-3">Preview</span>
</a>
</div>
</div>
</div>
</li>
))}
</ul>
</section>
);
};
export default RecentDataset;

View File

@@ -1,54 +0,0 @@
/* eslint-disable jsx-a11y/anchor-is-valid */
import { useQuery } from '@apollo/react-hooks';
import { ErrorMessage } from '../_shared';
import { GET_STATS_QUERY } from '../../graphql/queries';
const Stats: React.FC = () => {
const { loading, error, data } = useQuery(GET_STATS_QUERY, {
variables: {},
// Setting this value to true will make the component rerender when
// the "networkStatus" changes, so we are able to know if it is fetching
// more data
notifyOnNetworkStatusChange: true,
});
if (error) return <ErrorMessage message="Error loading search results." />;
if (loading) return <div>Loading</div>;
const stats = [
{ name: 'Datasets', stat: data.datasets.result.count },
{
name: 'Organizations',
stat: data.orgs.result ? data.orgs.result.length : 0,
},
{
name: 'Groups',
stat: data.groups.result ? data.groups.result.length : 0,
},
];
return (
<div className="p-4">
<h3 className="text-lg leading-6 font-medium text-gray-900">
DataHub Stats
</h3>
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
{stats.map((item) => (
<div
key={item.name}
className="px-4 py-5 bg-white shadow rounded-lg overflow-hidden sm:p-6"
>
<dt className="text-sm font-medium text-gray-500 truncate">
{item.name}
</dt>
<dd className="mt-1 text-3xl font-semibold text-gray-900">
{item.stat}
</dd>
</div>
))}
</dl>
</div>
);
};
export default Stats;

View File

@@ -1,154 +0,0 @@
import { useQuery } from '@apollo/react-hooks';
import * as timeago from 'timeago.js';
import { ErrorMessage } from '../_shared';
import { GET_ORG_QUERY } from '../../graphql/queries';
const About: React.FC<{ variables: any }> = ({ variables }) => {
const { loading, error, data } = useQuery(GET_ORG_QUERY, {
variables,
// Setting this value to true will make the component rerender when
// the "networkStatus" changes, so we are able to know if it is fetching
// more data
notifyOnNetworkStatusChange: true,
});
if (error) return <ErrorMessage message="Error loading dataset." />;
if (loading) return <div>Loading</div>;
const { result } = data.org;
return (
<div className="relative bg-white py-16 sm:py-4">
<div className="lg:mx-auto lg:max-w-7xl lg:px-8 lg:grid lg:grid-cols-2 lg:gap-24 lg:items-start">
<div className="relative sm:py-16 lg:py-0">
<div
aria-hidden="true"
className="hidden sm:block lg:absolute lg:inset-y-0 lg:right-0 lg:w-screen"
>
<div className="absolute inset-y-0 right-1/2 w-full bg-gray-50 rounded-r-3xl lg:right-72" />
<svg
className="absolute top-8 left-1/2 -ml-3 lg:-right-8 lg:left-auto lg:top-12"
width={404}
height={392}
fill="none"
viewBox="0 0 404 392"
>
<defs>
<pattern
id="02f20b47-fd69-4224-a62a-4c9de5c763f7"
x={0}
y={0}
width={20}
height={20}
patternUnits="userSpaceOnUse"
>
<rect
x={0}
y={0}
width={4}
height={4}
className="text-gray-200"
fill="currentColor"
/>
</pattern>
</defs>
<rect
width={404}
height={392}
fill="url(#02f20b47-fd69-4224-a62a-4c9de5c763f7)"
/>
</svg>
</div>
<div className="relative mx-auto max-w-md px-4 sm:max-w-3xl sm:px-6 lg:px-0 lg:max-w-none lg:py-20">
{/* Testimonial card*/}
<div className="relative pt-64 pb-10 rounded-2xl shadow-xl overflow-hidden">
<img
className="absolute inset-0 h-full w-full object-cover"
src={
result.image ||
'https://datahub.io/static/img/datahub-cube-edited.svg'
}
alt={result.title || result.name}
/>
<div className="absolute inset-0 bg-indigo-500 mix-blend-multiply" />
<div className="absolute inset-0 bg-gradient-to-t from-indigo-600 via-indigo-600 opacity-90" />
<div className="relative px-8">
<blockquote className="mt-8">
<div className="relative text-lg font-medium text-white md:flex-grow">
<p className="relative">
{result.description ||
"This organization doesn't have a description."}
</p>
</div>
<footer className="mt-4">
<p className="text-base font-semibold text-indigo-200">
{result.title}
</p>
</footer>
</blockquote>
</div>
</div>
</div>
</div>
<div className="relative mx-auto max-w-md px-4 sm:max-w-3xl sm:px-6 lg:px-0">
{/* Content area */}
<div className="pt-12 sm:pt-16 lg:pt-20">
<h1 className="text-3xl text-gray-900 font-extrabold tracking-tight sm:text-4xl">
{result.title || result.name}
</h1>
</div>
{/* Stats section */}
<div className="mt-10">
<dl className="grid grid-cols-2 gap-x-4 gap-y-8">
<div className="border-t-2 border-gray-100 pt-6">
<dt className="text-base font-medium text-gray-500">
Datasets
</dt>
<dd className="text-3xl font-extrabold tracking-tight text-gray-900">
{result.total}
</dd>
</div>
<div className="border-t-2 border-gray-100 pt-6">
<dt className="text-base font-medium text-gray-500">Users</dt>
<dd className="text-3xl font-extrabold tracking-tight text-gray-900">
{result.users && result.users.length}
</dd>
</div>
<div className="border-t-2 border-gray-100 pt-6">
<dt className="text-base font-medium text-gray-500">
Followers
</dt>
<dd className="text-3xl font-extrabold tracking-tight text-gray-900">
{result.followers}
</dd>
</div>
<div className="border-t-2 border-gray-100 pt-6">
<dt className="text-base font-medium text-gray-500">
Created
</dt>
<dd className="text-3xl font-extrabold tracking-tight text-gray-900">
{timeago.format(result.created)}
</dd>
</div>
</dl>
<div className="mt-10">
<a
href={`/search?fq=organization:${result.name}`}
className="text-base font-medium text-indigo-600"
>
{' '}
Datasets by {result.title || result.name}{' '}
<span aria-hidden="true">&rarr;</span>{' '}
</a>
</div>
</div>
</div>
</div>
</div>
);
};
export default About;

View File

@@ -1,78 +0,0 @@
/* eslint-disable react/display-name */
import { useQuery } from '@apollo/react-hooks';
import * as timeago from 'timeago.js';
import { ErrorMessage } from '../_shared';
import { GET_DATASET_QUERY } from '../../graphql/queries';
const About: React.FC<{ variables: any }> = ({ variables }) => {
const { loading, error, data } = useQuery(GET_DATASET_QUERY, {
variables,
// Setting this value to true will make the component rerender when
// the "networkStatus" changes, so we are able to know if it is fetching
// more data
notifyOnNetworkStatusChange: true,
});
if (error) return <ErrorMessage message="Error loading dataset." />;
if (loading) return <div>Loading</div>;
const { result } = data.dataset;
const resource = result.resources.find(
(item) => item.name === variables.resource
);
const stats = [
{ name: 'File', stat: resource.title || resource.name },
{ name: 'Description', stat: resource.description || 'N/A' },
{ name: 'Size', stat: resource.size || 'N/A' },
{
name: 'Created',
stat: resource.created && timeago.format(resource.created),
},
{
name: 'Updated',
stat: resource.updated && timeago.format(resource.updated),
},
{ name: 'Download', stat: resource.path, link: true },
];
return (
<>
<div className="pb-5 border-b border-gray-200">
<h1 className="text-3xl leading-6 font-medium text-gray-900">
{result.title || result.name}
</h1>
</div>
<div className="mb-5">
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
{stats.map((item) => (
<div
key={item.name}
className="px-4 py-5 bg-white shadow rounded-lg overflow-hidden sm:p-6"
>
<dt className="text-sm font-medium text-gray-500 truncate">
{item.name}
</dt>
<dd className="mt-1 text-3xl font-semibold text-gray-900">
{item.link ? (
<a
href={item.stat}
className="underline"
rel="noreferrer"
target="_blank"
>
{resource.format || 'Click'}
</a>
) : (
item.stat
)}
</dd>
</div>
))}
</dl>
</div>
</>
);
};
export default About;

View File

@@ -1,32 +0,0 @@
import { useQuery } from '@apollo/react-hooks';
import { PlotlyChart, Table } from '@portaljs/portaljs-components';
import { ErrorMessage } from '../_shared';
import { GET_DATASTORE_DATA } from '../../graphql/queries';
const Preview: React.FC<{ view: any }> = ({ view }) => {
const variables = {
resource_id: view.resources,
};
const { loading, error, data } = useQuery(GET_DATASTORE_DATA, {
variables,
// Setting this value to true will make the component rerender when
// the "networkStatus" changes, so we are able to know if it is fetching
// more data
notifyOnNetworkStatusChange: true,
});
if (error) return <ErrorMessage message="Error loading dataset." />;
if (loading) return <div>Loading</div>;
const { result } = data.datastore;
// Assuming for now it is always a table view
const columns = result.fields.map((field) => ({
field: field.id,
headerName: field.id,
}));
return <Table columns={columns} data={result.records} height="300px" width="100%"/>;
};
export default Preview;

View File

@@ -1,24 +0,0 @@
import { useQuery } from '@apollo/react-hooks';
import Preview from './Preview';
import { ErrorMessage } from '../_shared';
import { GET_RESOURCE_VIEWS } from '../../graphql/queries';
const View: React.FC<{ variables: any }> = ({ variables }) => {
const { loading, error, data } = useQuery(GET_RESOURCE_VIEWS, {
variables,
// Setting this value to true will make the component rerender when
// the "networkStatus" changes, so we are able to know if it is fetching
// more data
notifyOnNetworkStatusChange: true,
});
if (error) return <ErrorMessage message="Error loading dataset." />;
if (loading) return <div>Loading</div>;
const { result } = data.views;
const previews = result.map((view) => <Preview view={view} key={view.id} />);
return <>{previews}</>;
};
export default View;

View File

@@ -1,42 +0,0 @@
import { useState } from 'react';
import { useRouter } from 'next/router';
const SearchForm: React.FC = () => {
const router = useRouter();
const [searchQuery, setSearchQuery] = useState('');
const handleSubmit = (e) => {
if (e) {
e.preventDefault();
}
router.push({
pathname: '/search',
query: { q: searchQuery },
});
};
return (
<form onSubmit={(e) => handleSubmit(e)} className="items-center">
<input
id="search2"
type="search"
name="search"
onChange={(e) => {
setSearchQuery(e.target.value);
}}
placeholder="GDP data..."
aria-label="Search"
className="inline-block w-1/2 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
<button
onClick={() => handleSubmit(false)}
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>
</form>
);
};
export default SearchForm;

View File

@@ -1,64 +0,0 @@
import { useQuery } from '@apollo/react-hooks';
import Link from 'next/link';
import * as timeago from 'timeago.js';
import { ErrorMessage } from '../_shared';
import { SEARCH_QUERY } from '../../graphql/queries';
const List: React.FC<{ variables: any }> = ({ variables }) => {
const { loading, error, data } = useQuery(SEARCH_QUERY, {
variables,
// Setting this value to true will make the component rerender when
// the "networkStatus" changes, so we are able to know if it is fetching
// more data
notifyOnNetworkStatusChange: true,
});
if (error) return <ErrorMessage message="Error loading search results." />;
if (loading) return <div>Loading</div>;
const { result } = data.search;
return (
<ul className="divide-y divide-gray-200">
{result.results.map((dataset, index) => (
<li
key={index}
className="relative bg-white py-5 px-4 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600"
>
<div className="flex justify-between space-x-3">
<div className="min-w-0 flex-1">
<Link className="block focus:outline-none"
href={`/@${
dataset.organization ? dataset.organization.name : 'dataset'
}/${dataset.name}`}
>
<span className="absolute inset-0" aria-hidden="true" />
<p className="text-sm font-medium text-gray-900">
{dataset.title}
</p>
<p className="text-sm text-gray-500 truncate">
{dataset.organization
? dataset.organization.title
: 'dataset'}{' '}
/ {dataset.name}
</p>
</Link>
</div>
<time
dateTime={dataset.metadata_modified}
className="flex-shrink-0 whitespace-nowrap text-sm text-gray-500"
>
Updated {timeago.format(dataset.metadata_modified)}
</time>
</div>
<div className="mt-1">
<p className="line-clamp-2 text-sm text-gray-600">
{dataset.description ||
"This dataset doesn't have a description."}
</p>
</div>
</li>
))}
</ul>
);
};
export default List;

View File

@@ -1,23 +0,0 @@
import { useQuery } from '@apollo/react-hooks';
import { ErrorMessage } from '../_shared';
import { GET_TOTAL_COUNT_QUERY } from '../../graphql/queries';
import { ItemTotal } from '@portaljs/portaljs-components';
const Total: React.FC<{ variables: any }> = ({ variables }) => {
const { loading, error, data } = useQuery(GET_TOTAL_COUNT_QUERY, {
variables,
// Setting this value to true will make the component rerender when
// the "networkStatus" changes, so we are able to know if it is fetching
// more data
notifyOnNetworkStatusChange: true,
});
if (error) return <ErrorMessage message="Error loading search results." />;
if (loading) return <div>Loading</div>;
const { result } = data.search;
return <ItemTotal count={result.count} />;
};
export default Total;

View File

@@ -1,39 +0,0 @@
import parse from 'html-react-parser';
import { useQuery } from '@apollo/react-hooks';
import { ErrorMessage } from '../_shared';
import { GET_POSTS_QUERY } from '../../graphql/queries';
const List: React.FC = () => {
const { loading, error, data } = useQuery(GET_POSTS_QUERY, {
// Setting this value to true will make the component rerender when
// the "networkStatus" changes, so we are able to know if it is fetching
// more data
notifyOnNetworkStatusChange: true,
});
if (error) return <ErrorMessage message="Error loading search results." />;
if (loading) return <div>Loading</div>;
const { posts, found } = data.posts;
return (
<>
<h1 className="text-3xl font-semibold text-primary my-6 inline-block">
{found} 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>
))}
</>
);
};
export default List;

View File

@@ -1,32 +0,0 @@
import parse from 'html-react-parser';
import { useQuery } from '@apollo/react-hooks';
import { ErrorMessage } from '../_shared';
import { GET_PAGE_QUERY } from '../../graphql/queries';
const Page: React.FC<{ variables: any }> = ({ variables }) => {
const { loading, error, data } = useQuery(GET_PAGE_QUERY, {
variables,
// Setting this value to true will make the component rerender when
// the "networkStatus" changes, so we are able to know if it is fetching
// more data
notifyOnNetworkStatusChange: true,
});
if (error) return <ErrorMessage message="Error loading search results." />;
if (loading) return <div>Loading</div>;
const { title, content, modified, featured_image } = data.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>
</>
);
};
export default Page;

View File

@@ -1,32 +0,0 @@
import parse from 'html-react-parser';
import { useQuery } from '@apollo/react-hooks';
import { ErrorMessage } from '../_shared';
import { GET_PAGE_QUERY } from '../../graphql/queries';
const Post: React.FC<{ variables: any }> = ({ variables }) => {
const { loading, error, data } = useQuery(GET_PAGE_QUERY, {
variables,
// Setting this value to true will make the component rerender when
// the "networkStatus" changes, so we are able to know if it is fetching
// more data
notifyOnNetworkStatusChange: true,
});
if (error) return <ErrorMessage message="Error loading search results." />;
if (loading) return <div>Loading</div>;
const { title, content, modified, featured_image } = data.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>
</>
);
};
export default Post;

View File

@@ -1,8 +0,0 @@
module.exports = {
process() {
return 'module.exports = {};';
},
getCacheKey() {
return 'cssTransform';
},
};

View File

@@ -1,3 +0,0 @@
{
"baseUrl": "http://localhost:3000"
}

View File

@@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -1,32 +0,0 @@
describe('Test Home Page', () => {
beforeEach(() => {
cy.visit('/');
});
it('renders the hero title', () => {
cy.contains('Find and Share');
});
it('checks that a search form exists', () => {
cy.get('form').contains('Search');
});
// it('submits the search form', () => {
// cy.get('form').find('[type="text"]').type('gdp');
// cy.get('form').submit();
// cy.url().should('include', '/search?q=gdp&sort=');
// cy.get('.text-3xl').and('contain.text', '1 results found');
// });
it('shows the recent datasets', () => {
cy.contains('Recent Datasets');
});
it('returns the expected number of recent datasets', () => {
cy.get('.recent')
.find('div')
.should(($div) => {
expect($div).to.have.length.of.at.least(2);
});
});
});

View File

@@ -1,21 +0,0 @@
describe('Test Search Page', () => {
beforeEach(() => {
cy.visit('/search');
});
it('has a search form', () => {
cy.contains('form');
cy.contains('Search');
});
// it('should return a search result', () => {
// cy.get('form').find('[type="text"]').type('gdp');
// cy.get('form').submit();
// cy.url().should('include', 'search?q=gdp&sort=');
// cy.get('.text-3xl').should('have.text', '1 results found');
// cy.get('.text-xl > .text-primary').should(
// 'have.text',
// 'Country, Regional and World GDP (Gross Domestic Product)'
// );
// });
});

View File

@@ -1,21 +0,0 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
};

View File

@@ -1,25 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

View File

@@ -1,20 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@@ -1,12 +0,0 @@
{
"compilerOptions": {
"strict": true,
"baseUrl": "../node_modules",
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
},
"include": [
"**/*.ts"
]
}

View File

@@ -1,166 +0,0 @@
import gql from 'graphql-tag';
export const GET_RESOURCE_VIEWS = gql`
query views($id: String) {
views(id: $id) @rest(type: "Response", path: "resource_view_list?{args}") {
result @type(name: "View") {
id
title
description
resources: resource_id
viewType: view_type
series
group
type: graph_type
}
}
}
`;
export const GET_DATASTORE_DATA = gql`
query datastore($resource_id: String) {
datastore(resource_id: $resource_id)
@rest(type: "Response", path: "datastore_search?{args}") {
result {
count: total
fields
records
}
}
}
`;
export const GET_ORG_QUERY = gql`
query org($id: String) {
org(id: $id) @rest(type: "Response", path: "organization_show?{args}") {
result {
name
title
description
image: image_url
created
total: package_count
users
followers: num_followers
}
}
}
`;
export const GET_DATASET_QUERY = gql`
query dataset($id: String) {
dataset(id: $id) @rest(type: "Response", path: "package_show?{args}") {
result {
name
title
size
created: metadata_created
updated: metadata_modified
resources {
id
name
title
description
path: url
format
created
updated: last_modified
size
}
organization {
name
title
image: image_url
}
}
}
}
`;
export const SEARCH_QUERY = gql`
query search($q: String, $sort: String, $rows: Int, $start: Int) {
search(q: $q, sort: $sort, rows: $rows, start: $start)
@rest(type: "Search", path: "package_search?{args}") {
result {
count
results {
name
title
updated: metadata_modified
organization {
name
title
description
image: image_url
}
}
}
}
}
`;
export const GET_TOTAL_COUNT_QUERY = gql`
query search($q: String, $sort: String) {
search(q: $q, sort: $sort)
@rest(type: "Search", path: "package_search?{args}") {
result {
count
}
}
}
`;
export const GET_STATS_QUERY = gql`
query stats {
datasets @rest(type: "Search", path: "package_search?rows=0") {
result {
count
}
}
orgs @rest(type: "Search", path: "organization_list") {
result
}
groups @rest(type: "Search", path: "group_list") {
result
}
}
`;
export const GET_POSTS_QUERY = gql`
query posts {
posts @rest(type: "Posts", path: "", endpoint: "wordpress-posts") {
found
posts
meta
}
}
`;
export const GET_PAGE_QUERY = gql`
query page($slug: String) {
page(slug: $slug)
@rest(type: "Page", path: "{args.slug}", endpoint: "wordpress") {
title
content
excerpt
slug
date
modified
featured_image
}
}
`;
export const GET_POST_QUERY = gql`
query post($slug: String) {
post(slug: $slug)
@rest(type: "Post", path: "{args.slug}", endpoint: "wordpress") {
title
content
excerpt
slug
date
modified
}
}
`;

View File

@@ -1,6 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
declare module '*.svg' {
const content: any;
export const ReactComponent: any;
export default content;
}

View File

@@ -1,29 +0,0 @@
module.exports = {
collectCoverageFrom: [
'**/*.{js,jsx,ts,tsx}',
'!**/*.d.ts',
'!**/node_modules/**',
'!**/config/**',
'!**/coverage/**',
'!**/**.config.js**',
],
setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
testPathIgnorePatterns: [
'/node_modules/',
'/.next/',
'/jest.config.js/',
'/tailwind.config.js/',
'<rootDir>/postcss.config.js',
],
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
'^.+\\.css$': '<rootDir>/config/jest/cssTransform.js',
},
transformIgnorePatterns: [
'/node_modules/',
'^.+\\.module\\.(css|sass|scss)$',
],
moduleNameMapper: {
'^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy',
},
};

View File

@@ -1,11 +0,0 @@
/* eslint-disable */
export default {
displayName: 'ckan',
preset: '../../jest.preset.js',
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nrwl/next/babel'] }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/examples/ckan',
};

View File

@@ -1,93 +0,0 @@
import { useMemo } from 'react';
import getConfig from 'next/config';
import { ApolloClient } from '@apollo/client';
import {
InMemoryCache,
NormalizedCache,
NormalizedCacheObject,
} from 'apollo-cache-inmemory';
import { RestLink } from 'apollo-link-rest';
import { ApolloLink } from '@apollo/client';
let apolloClient:
| ApolloClient<NormalizedCache>
| ApolloClient<NormalizedCacheObject>;
const restLink = new RestLink({
uri: getConfig().publicRuntimeConfig.DMS + '/api/3/action/',
endpoints: {
wordpress: `https://public-api.wordpress.com/rest/v1.1/sites/${
getConfig().publicRuntimeConfig.CMS
}/posts/slug:`,
'wordpress-posts': `https://public-api.wordpress.com/rest/v1.1/sites/${
getConfig().publicRuntimeConfig.CMS
}/posts/`,
},
typePatcher: {
Search: (data: any): any => {
if (data.result != null) {
data.result.__typename = 'SearchResponse';
if (data.result.results != null) {
data.result.results = data.result.results.map((item) => {
if (item.organization != null) {
item.organization.__typename = 'Organization';
}
return { __typename: 'Package', ...item };
});
}
}
return data;
},
Response: (data: any): any => {
if (data.result != null) {
data.result.__typename = 'Package';
if (data.result.organization != null) {
data.result.organization.__typename = 'Organization';
}
if (data.result.resources != null) {
data.result.resources = data.result.resources.map((item) => {
return { __typename: 'Resource', ...item };
});
}
}
return data;
},
},
});
function createApolloClient() {
return new ApolloClient({
link: restLink as any,
cache: new InMemoryCache(),
});
}
export function initializeApollo(
initialState = null
): ApolloClient<NormalizedCache> | ApolloClient<NormalizedCacheObject> {
const _apolloClient:
| ApolloClient<NormalizedCache>
| ApolloClient<NormalizedCacheObject> =
apolloClient ?? createApolloClient();
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
// gets hydrated here
if (initialState) {
_apolloClient.cache.restore(initialState);
}
// For SSG and SSR always create a new Apollo Client
if (typeof window === 'undefined') return _apolloClient;
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient;
return _apolloClient;
}
export function useApollo(
initialState = null
): ApolloClient<NormalizedCache> | ApolloClient<NormalizedCacheObject> {
const store = useMemo(() => initializeApollo(initialState), [initialState]);
return store;
}

View File

@@ -1,4 +0,0 @@
{
"title": "Portal in English",
"description": "At Datahub, we have over thousands of datasets for free and a Premium Data Service for additional or customised data with guaranteed updates."
}

View File

@@ -1,4 +0,0 @@
{
"title": "Portal in french",
"description": "Chez Datahub, nous avons plus de milliers d'ensembles de données gratuitement et un service de données Premium pour des données supplémentaires ou personnalisées avec des mises à jour garanties."
}

View File

@@ -1,155 +0,0 @@
const nock = require('nock');
const gdp = {
name: 'gdp',
title: 'Country, Regional and World GDP (Gross Domestic Product)',
notes:
'Country, regional and world GDP in current US Dollars ($). Regional means collections of countries e.g. Europe & Central Asia. Data is sourced from the World Bank and turned into a standard normalized CSV.',
resources: [
{
name: 'gdp',
id: 'gdp',
title: 'GDP data',
format: 'csv',
created: '2019-03-07T12:00:36.273495',
last_modified: '2020-05-07T12:00:36.273495',
datastore_active: false,
url: 'http://mock.filestore/gdp.csv',
},
],
organization: {
title: 'World Bank',
name: 'world-bank',
description:
'The World Bank is an international financial institution that provides loans and grants to the governments of poorer countries for the purpose of pursuing capital projects.',
created: '2019-03-07T11:51:13.758844',
image_url:
'https://github.com/datahq/frontend/raw/master/public/img/avatars/world-bank.jpg',
},
metadata_created: '2019-03-07T11:56:19.696257',
metadata_modified: '2019-03-07T12:03:58.817280',
size: '',
};
const population = {
name: 'population',
title: 'World population data',
notes:
'Population figures for countries, regions (e.g. Asia) and the world. Data comes originally from World Bank and has been converted into standard CSV.',
resources: [
{
name: 'population',
id: 'population',
title: 'Population data',
format: 'csv',
created: '2019-03-07T12:00:36.273495',
last_modified: '2020-05-07T12:00:36.273495',
datastore_active: true,
url: 'http://mock.filestore/population.csv',
},
],
organization: {
title: 'World Bank',
name: 'world-bank',
description:
'The World Bank is an international financial institution that provides loans and grants to the governments of poorer countries for the purpose of pursuing capital projects.',
created: '2019-03-07T11:51:13.758844',
image_url:
'https://github.com/datahq/frontend/raw/master/public/img/avatars/world-bank.jpg',
},
};
module.exports.initMocks = function () {
// Uncomment this line if you want to record API calls
// nock.recorder.rec()
// "package_search" mocks
nock('http://mock.ckan/api/3/action', { encodedQueryParams: true })
.persist()
// 1. Call without query.
.get('/package_search?')
.reply(200, {
success: true,
result: {
count: 2,
sort: 'score desc, metadata_modified desc',
facets: {},
results: [gdp, population],
search_facets: {},
},
})
// 2. Call with `q=gdp` query.
.get('/package_search?q=gdp')
.reply(200, {
success: true,
result: {
count: 1,
sort: 'score desc, metadata_modified desc',
facets: {},
results: [gdp],
search_facets: {},
},
})
// 3. Call for recent packages.
.get('/package_search?sort=metadata_created%20desc&rows=3')
.reply(200, {
success: true,
result: {
count: 2,
sort: 'metadata_created desc',
facets: {},
results: [gdp, population],
search_facets: {},
},
});
// "package_show" mocks
nock('http://mock.ckan/api/3/action', { encodedQueryParams: true })
.persist()
.get('/package_show?id=gdp')
.reply(200, {
success: true,
result: gdp,
})
.get('/package_show?id=population')
.reply(200, {
success: true,
result: population,
});
// "datastore_search" mocks
nock('http://mock.ckan/api/3/action', { encodedQueryParams: true })
.persist()
.get('/datastore_search?resource_id=population')
.reply(200, {
success: true,
result: {
records: [
{
'Country Code': 'ARB',
'Country Name': 'Arab World',
Value: 92197753,
Year: 1960,
},
{
'Country Code': 'ARB',
'Country Name': 'Arab World',
Value: 94724510,
Year: 1961,
},
{
'Country Code': 'ARB',
'Country Name': 'Arab World',
Value: 97334442,
Year: 1962,
},
],
},
});
// Filestore mocks
nock('http://mock.filestore', { encodedQueryParams: true })
.persist()
.get('/gdp.csv')
.reply(200, 'a,b,c\n1,2,3\n4,5,6\n');
};

View File

@@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@@ -1,65 +0,0 @@
//@ts-check
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { withNx } = require('@nrwl/next/plugins/with-nx');
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
const _nextConfig = (phase) => {
const dms = process.env.DMS;
const cms = process.env.CMS;
if (phase === PHASE_DEVELOPMENT_SERVER) {
if (dms) {
console.log('\nYou are running the app in dev mode 🌀');
console.log(
'Did you know that you can use mocked CKAN API? Just unset your `DMS` env var.'
);
console.log('Happy coding ☀️\n');
} else {
const mocks = require('./mocks');
mocks.initMocks();
console.log(
'\nYou have not defined any DMS API so we are activating the mocks ⚠️'
);
console.log(
'If you wish to run against your CKAN API, you can set `DMS` env var.'
);
console.log(
'For example, to run against demo ckan site: `DMS=https://demo.ckan.org`\n'
);
}
return {
i18n: {
locales: ['en', 'fr', 'nl-NL'],
defaultLocale: 'en',
},
publicRuntimeConfig: {
DMS: dms ? dms.replace(/\/?$/, '') : 'http://mock.ckan',
CMS: cms ? cms.replace(/\/?$/, '') : 'oddk.home.blog',
},
};
}
return {
i18n: {
locales: ['en', 'fr', 'nl-NL'],
defaultLocale: 'en',
},
publicRuntimeConfig: {
DMS: dms ? dms.replace(/\/?$/, '') : 'https://demo.dev.datopian.com',
CMS: cms ? cms.replace(/\/?$/, '') : 'oddk.home.blog',
},
};
};
/**
* @type {import('@nrwl/next/plugins/with-nx').WithNxOptions}
**/
const nextConfig = {
..._nextConfig(),
nx: {
// Set this to true if you would like to use SVGR
// See: https://github.com/gregberge/svgr
svgr: false,
},
};
module.exports = withNx(nextConfig);

View File

@@ -1,53 +0,0 @@
import { GetServerSideProps } from 'next';
import { useQuery } from '@apollo/react-hooks';
import Head from 'next/head';
import { initializeApollo } from '../../../lib/apolloClient';
import Nav from '../../../components/home/Nav';
import About from '../../../components/dataset/About';
import Org from '../../../components/dataset/Org';
import Resources from '../../../components/dataset/Resources';
import Footer from '../../../components/home/Footer';
import { GET_DATASET_QUERY } from '../../../graphql/queries';
const Dataset: React.FC<{ variables: any }> = ({ variables }) => {
const { data, loading } = useQuery(GET_DATASET_QUERY, { variables });
if (loading) return <div>Loading</div>;
const { result } = data.dataset;
return (
<>
<Head>
<title>Portal | {result.title || result.name}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Nav />
<main className="p-8">
<About variables={variables} />
<Resources variables={variables} />
<Footer />
</main>
</>
);
};
export const getServerSideProps: GetServerSideProps = async (context) => {
const apolloClient = initializeApollo();
const variables = {
id: context.query.dataset,
};
await apolloClient.query({
query: GET_DATASET_QUERY,
variables,
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
variables,
},
};
};
export default Dataset;

View File

@@ -1,56 +0,0 @@
import { GetServerSideProps } from 'next';
import { useQuery } from '@apollo/react-hooks';
import Head from 'next/head';
import { initializeApollo } from '../../../../../lib/apolloClient';
import Nav from '../../../../../components/home/Nav';
import About from '../../../../../components/resource/About';
import View from '../../../../../components/resource/View';
import Footer from '../../../../../components/home/Footer';
import { GET_DATASET_QUERY } from '../../../../../graphql/queries';
const Resource: React.FC<{ variables: any }> = ({ variables }) => {
const { data, loading } = useQuery(GET_DATASET_QUERY, { variables });
if (loading) return <div>Loading</div>;
const result = data.dataset.result;
// Find right resource
const resource = result.resources.find(
(item) => item.name === variables.resource
);
return (
<>
<Head>
<title>Portal | {resource.title || resource.name}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Nav />
<main className="p-6">
<About variables={variables} />
<View variables={{ id: resource.id }} />
<Footer />
</main>
</>
);
};
export const getServerSideProps: GetServerSideProps = async (context) => {
const apolloClient = initializeApollo();
const variables = {
id: context.query.dataset,
resource: context.query.resource,
};
await apolloClient.query({
query: GET_DATASET_QUERY,
variables,
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
variables,
},
};
};
export default Resource;

View File

@@ -1,49 +0,0 @@
import { GetServerSideProps } from 'next';
import { useQuery } from '@apollo/react-hooks';
import Head from 'next/head';
import { initializeApollo } from '../../lib/apolloClient';
import Nav from '../../components/home/Nav';
import About from '../../components/org/About';
import Footer from '../../components/home/Footer';
import { GET_ORG_QUERY } from '../../graphql/queries';
const Org: React.FC<{ variables: any }> = ({ variables }) => {
const { data, loading } = useQuery(GET_ORG_QUERY, { variables });
if (loading) return <div>Loading</div>;
const { result } = data.org;
return (
<>
<Head>
<title>Portal | {result.title || result.name}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Nav />
<About variables={variables} />
<Footer />
</>
);
};
export const getServerSideProps: GetServerSideProps = async (context) => {
const apolloClient = initializeApollo();
const variables = {
id: (context.query.org as string).replace('@', ''),
};
await apolloClient.query({
query: GET_ORG_QUERY,
variables,
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
variables,
},
};
};
export default Org;

View File

@@ -1,57 +0,0 @@
import { useEffect, useState } from 'react';
import { ApolloProvider } from '@apollo/react-hooks';
import { useApollo } from '../lib/apolloClient';
import { DEFAULT_THEME } from '../themes';
import { applyTheme } from '../themes/utils';
import I18nProvider from 'next-translate/I18nProvider';
import { useRouter } from 'next/router';
import '../styles/globals.css';
import ApolloClient from '@apollo/client';
import { NormalizedCache, NormalizedCacheObject } from 'apollo-cache-inmemory';
interface I8nObject {
[property: string]: any;
}
export async function loadNamespaces(
namespaces: string[],
lang: string
): Promise<I8nObject> {
const res = {};
for (const ns of namespaces) {
res[ns] = await import(`../locales/${lang}/${ns}.json`).then(
(m) => m.default
);
}
return res;
}
type Props = {
Component: any;
pageProps: any;
};
const MyApp: React.FC<Props> = ({ Component, pageProps }) => {
const apolloClient = useApollo(pageProps.initialApolloState);
const [theme] = useState(DEFAULT_THEME); // setTheme
const router = useRouter();
useEffect(() => {
/**
* We can switch theme.
* e.g. setTheme('primary');
* */
applyTheme(theme);
}, [theme]);
return (
<I18nProvider lang={router.locale} namespaces={pageProps._ns}>
<ApolloProvider client={apolloClient as any}>
<Component {...pageProps} />
</ApolloProvider>
</I18nProvider>
);
};
export default MyApp;

View File

@@ -1,34 +0,0 @@
import Document, { Html, Head, Main, NextScript } from 'next/document';
const GA_TRACKING_ID = 'G-NX72GYFHFS';
export default class CustomDocument extends Document {
render() {
return (
<Html>
<Head>
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-NX72GYFHFS"
/>
<script
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_TRACKING_ID}',{
page_path: window.location.pathname,
});
`,
}}
/>
</Head>
<body>
<Main />
</body>
<NextScript />
</Html>
);
}
}

View File

@@ -1,45 +0,0 @@
import { GetServerSideProps } from 'next';
import Head from 'next/head';
import { initializeApollo } from '../../../lib/apolloClient';
import Nav from '../../../components/home/Nav';
import Post from '../../../components/static/Post';
import { GET_POST_QUERY } from '../../../graphql/queries';
type Props = {
variables: any;
};
const PostItem: React.FC<Props> = ({ variables }) => (
<>
<Head>
<title>Portal | {variables.slug}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Nav />
<main className="p-6">
<Post variables={variables} />
</main>
</>
);
export const getServerSideProps: GetServerSideProps = async (context) => {
const variables = {
slug: context.query.post,
};
const apolloClient = initializeApollo();
await apolloClient.query({
query: GET_POST_QUERY,
variables,
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
variables,
},
};
};
export default PostItem;

View File

@@ -1,35 +0,0 @@
import { GetServerSideProps } from 'next';
import Head from 'next/head';
import { initializeApollo } from '../../lib/apolloClient';
import Nav from '../../components/home/Nav';
import List from '../../components/static/List';
import { GET_POSTS_QUERY } from '../../graphql/queries';
const PostList: React.FC = () => (
<>
<Head>
<title>Portal | Blog</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Nav />
<main className="p-6">
<List />
</main>
</>
);
export const getServerSideProps: GetServerSideProps = async () => {
const apolloClient = initializeApollo();
await apolloClient.query({
query: GET_POSTS_QUERY,
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
},
};
};
export default PostList;

View File

@@ -1,2 +0,0 @@
.page {
}

View File

@@ -1,59 +0,0 @@
import { GetServerSideProps } from 'next';
import Head from 'next/head';
import { initializeApollo } from '../lib/apolloClient';
import RecentDataset from '../components/home/Recent';
import { SEARCH_QUERY } from '../graphql/queries';
import { loadNamespaces } from './_app';
import useTranslation from 'next-translate/useTranslation';
import NavBar from '../components/home/Nav';
import Hero from '../components/home/Hero';
import Footer from '../components/home/Footer';
import Stats from '../components/home/Stats';
const Home: React.FC<{ locale: any; locales: any }> = ({
locale,
locales,
}) => {
const { t } = useTranslation();
return (
<>
<div className="container mx-auto">
<Head>
<title>{t(`common:title`)}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<NavBar />
<Hero />
<Stats />
<RecentDataset />
<Footer />
</div>
</>
);
};
export const getServerSideProps: GetServerSideProps = async ({
locale,
locales,
}) => {
const apolloClient = initializeApollo();
await apolloClient.query({
query: SEARCH_QUERY,
variables: {
sort: 'metadata_created desc',
rows: 3,
},
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
_ns: await loadNamespaces(['common'], locale),
locale,
locales,
},
};
};
export default Home;

View File

@@ -1,45 +0,0 @@
import { GetServerSideProps } from 'next';
import Head from 'next/head';
import { initializeApollo } from '../../../lib/apolloClient';
import Nav from '../../../components/home/Nav';
import Page from '../../../components/static/Page';
import { GET_PAGE_QUERY } from '../../../graphql/queries';
type Props = {
variables: any;
};
const PageItem: React.FC<Props> = ({ variables }) => (
<>
<Head>
<title>Portal | {variables.slug}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Nav />
<main className="p-6">
<Page variables={variables} />
</main>
</>
);
export const getServerSideProps: GetServerSideProps = async (context) => {
const variables = {
slug: context.query.page,
};
const apolloClient = initializeApollo();
await apolloClient.query({
query: GET_PAGE_QUERY,
variables,
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
variables,
},
};
};
export default PageItem;

View File

@@ -1,49 +0,0 @@
import { GetServerSideProps } from 'next';
import { initializeApollo } from '../lib/apolloClient';
import utils from '../utils';
import Head from 'next/head';
import Nav from '../components/home/Nav';
import Form from '../components/search/Form';
import Total from '../components/search/Total';
import List from '../components/search/List';
import { SEARCH_QUERY } from '../graphql/queries';
type Props = {
variables: any;
};
const Search: React.FC<Props> = ({ variables }) => (
<>
<Head>
<title>Portal | Search</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Nav />
<main className="p-6">
<Form />
<Total variables={variables} />
<List variables={variables} />
</main>
</>
);
export const getServerSideProps: GetServerSideProps = async (context) => {
const query = context.query || {};
const variables = utils.convertToCkanSearchQuery(query);
const apolloClient = initializeApollo();
await apolloClient.query({
query: SEARCH_QUERY,
variables,
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
variables,
},
};
};
export default Search;

View File

@@ -1,403 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
-webkit-text-size-adjust: 100%;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif,
Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
line-height: 1.5;
tab-size: 4;
scroll-behavior: smooth;
}
body {
font-family: inherit;
line-height: inherit;
margin: 0;
}
h1,
h2,
p,
pre {
margin: 0;
}
*,
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: currentColor;
}
h1,
h2 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
text-decoration: inherit;
}
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
Liberation Mono, Courier New, monospace;
}
svg {
display: block;
vertical-align: middle;
shape-rendering: auto;
text-rendering: optimizeLegibility;
}
pre {
background-color: rgba(55, 65, 81, 1);
border-radius: 0.25rem;
color: rgba(229, 231, 235, 1);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
Liberation Mono, Courier New, monospace;
overflow: scroll;
padding: 0.5rem 0.75rem;
}
.shadow {
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.rounded {
border-radius: 1.5rem;
}
.wrapper {
width: 100%;
}
.container {
margin-left: auto;
margin-right: auto;
max-width: 768px;
padding-bottom: 3rem;
padding-left: 1rem;
padding-right: 1rem;
color: rgba(55, 65, 81, 1);
width: 100%;
}
#welcome {
margin-top: 2.5rem;
}
#welcome h1 {
font-size: 3rem;
font-weight: 500;
letter-spacing: -0.025em;
line-height: 1;
}
#welcome span {
display: block;
font-size: 1.875rem;
font-weight: 300;
line-height: 2.25rem;
margin-bottom: 0.5rem;
}
#hero {
align-items: center;
background-color: hsla(214, 62%, 21%, 1);
border: none;
box-sizing: border-box;
color: rgba(55, 65, 81, 1);
display: grid;
grid-template-columns: 1fr;
margin-top: 3.5rem;
}
#hero .text-container {
color: rgba(255, 255, 255, 1);
padding: 3rem 2rem;
}
#hero .text-container h2 {
font-size: 1.5rem;
line-height: 2rem;
position: relative;
}
#hero .text-container h2 svg {
color: hsla(162, 47%, 50%, 1);
height: 2rem;
left: -0.25rem;
position: absolute;
top: 0;
width: 2rem;
}
#hero .text-container h2 span {
margin-left: 2.5rem;
}
#hero .text-container a {
background-color: rgba(255, 255, 255, 1);
border-radius: 0.75rem;
color: rgba(55, 65, 81, 1);
display: inline-block;
margin-top: 1.5rem;
padding: 1rem 2rem;
text-decoration: inherit;
}
#hero .logo-container {
display: none;
justify-content: center;
padding-left: 2rem;
padding-right: 2rem;
}
#hero .logo-container svg {
color: rgba(255, 255, 255, 1);
width: 66.666667%;
}
#middle-content {
align-items: flex-start;
display: grid;
gap: 4rem;
grid-template-columns: 1fr;
margin-top: 3.5rem;
}
#learning-materials {
padding: 2.5rem 2rem;
}
#learning-materials h2 {
font-weight: 500;
font-size: 1.25rem;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
.list-item-link {
align-items: center;
border-radius: 0.75rem;
display: flex;
margin-top: 1rem;
padding: 1rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 100%;
}
.list-item-link svg:first-child {
margin-right: 1rem;
height: 1.5rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 1.5rem;
}
.list-item-link > span {
flex-grow: 1;
font-weight: 400;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.list-item-link > span > span {
color: rgba(107, 114, 128, 1);
display: block;
flex-grow: 1;
font-size: 0.75rem;
font-weight: 300;
line-height: 1rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.list-item-link svg:last-child {
height: 1rem;
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 1rem;
}
.list-item-link:hover {
color: rgba(255, 255, 255, 1);
background-color: hsla(162, 47%, 50%, 1);
}
.list-item-link:hover > span {
}
.list-item-link:hover > span > span {
color: rgba(243, 244, 246, 1);
}
.list-item-link:hover svg:last-child {
transform: translateX(0.25rem);
}
#other-links {
}
.button-pill {
padding: 1.5rem 2rem;
transition-duration: 300ms;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
align-items: center;
display: flex;
}
.button-pill svg {
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
flex-shrink: 0;
width: 3rem;
}
.button-pill > span {
letter-spacing: -0.025em;
font-weight: 400;
font-size: 1.125rem;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
.button-pill span span {
display: block;
font-size: 0.875rem;
font-weight: 300;
line-height: 1.25rem;
}
.button-pill:hover svg,
.button-pill:hover {
color: rgba(255, 255, 255, 1) !important;
}
#nx-console:hover {
background-color: rgba(0, 122, 204, 1);
}
#nx-console svg {
color: rgba(0, 122, 204, 1);
}
#nx-repo:hover {
background-color: rgba(24, 23, 23, 1);
}
#nx-repo svg {
color: rgba(24, 23, 23, 1);
}
#nx-cloud {
margin-bottom: 2rem;
margin-top: 2rem;
padding: 2.5rem 2rem;
}
#nx-cloud > div {
align-items: center;
display: flex;
}
#nx-cloud > div svg {
border-radius: 0.375rem;
flex-shrink: 0;
width: 3rem;
}
#nx-cloud > div h2 {
font-size: 1.125rem;
font-weight: 400;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
#nx-cloud > div h2 span {
display: block;
font-size: 0.875rem;
font-weight: 300;
line-height: 1.25rem;
}
#nx-cloud p {
font-size: 1rem;
line-height: 1.5rem;
margin-top: 1rem;
}
#nx-cloud pre {
margin-top: 1rem;
}
#nx-cloud a {
color: rgba(107, 114, 128, 1);
display: block;
font-size: 0.875rem;
line-height: 1.25rem;
margin-top: 1.5rem;
text-align: right;
}
#nx-cloud a:hover {
text-decoration: underline;
}
#commands {
padding: 2.5rem 2rem;
margin-top: 3.5rem;
}
#commands h2 {
font-size: 1.25rem;
font-weight: 400;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
#commands p {
font-size: 1rem;
font-weight: 300;
line-height: 1.5rem;
margin-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
}
details {
align-items: center;
display: flex;
margin-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
width: 100%;
}
details pre > span {
color: rgba(181, 181, 181, 1);
display: block;
}
summary {
border-radius: 0.5rem;
display: flex;
font-weight: 400;
padding: 0.5rem;
cursor: pointer;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
summary:hover {
background-color: rgba(243, 244, 246, 1);
}
summary svg {
height: 1.5rem;
margin-right: 1rem;
width: 1.5rem;
}
#love {
color: rgba(107, 114, 128, 1);
font-size: 0.875rem;
line-height: 1.25rem;
margin-top: 3.5rem;
opacity: 0.6;
text-align: center;
}
#love svg {
color: rgba(252, 165, 165, 1);
width: 1.25rem;
height: 1.25rem;
display: inline;
margin-top: -0.25rem;
}
@media screen and (min-width: 768px) {
#hero {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#hero .logo-container {
display: flex;
}
#middle-content {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

View File

@@ -1,15 +0,0 @@
const { join } = require('path');
// Note: If you use library-specific PostCSS/Tailwind configuration then you should remove the `postcssConfig` build
// option from your application's configuration (i.e. project.json).
//
// See: https://nx.dev/guides/using-tailwind-css-in-react#step-4:-applying-configuration-to-libraries
module.exports = {
plugins: {
tailwindcss: {
config: join(__dirname, 'tailwind.config.js'),
},
autoprefixer: {},
},
};

View File

@@ -1,69 +0,0 @@
{
"name": "ckan",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "examples/ckan",
"projectType": "application",
"targets": {
"build": {
"executor": "@nrwl/next:build",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"root": "examples/ckan",
"outputPath": "dist/examples/ckan"
},
"configurations": {
"development": {
"outputPath": "examples/ckan"
},
"production": {}
}
},
"serve": {
"executor": "@nrwl/next:server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "ckan:build",
"dev": true
},
"configurations": {
"development": {
"buildTarget": "ckan:build:development",
"dev": true
},
"production": {
"buildTarget": "ckan:build:production",
"dev": false
}
}
},
"export": {
"executor": "@nrwl/next:export",
"options": {
"buildTarget": "ckan:build:production"
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "examples/ckan/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["examples/ckan/**/*.{ts,tsx,js,jsx}"]
}
}
},
"tags": []
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><g transform="translate(-395.882 247.118)"><rect width="64" height="64" rx="2" transform="translate(395.882 -247.118)" fill="#030303"/><text transform="translate(408.882 -221.118)" fill="#fff" font-size="20" font-family="AndaleMono, Andale Mono"><tspan x="0" y="0">POR</tspan><tspan x="0" y="22">TAL</tspan></text></g></svg>

Before

Width:  |  Height:  |  Size: 408 B

View File

@@ -1,11 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react';
import Index from '../pages/index';
describe('Index', () => {
it('should render successfully', () => {
const { baseElement } = render(<Index />);
expect(baseElement).toBeTruthy();
});
});

View File

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,17 +0,0 @@
const { createGlobPatternsForDependencies } = require('@nrwl/react/tailwind');
const { join } = require('path');
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
join(
__dirname,
'{src,pages,components}/**/*!(*.stories|*.spec).{ts,tsx,html}'
),
...createGlobPatternsForDependencies(__dirname),
],
theme: {
extend: {},
},
plugins: [],
};

View File

@@ -1,13 +0,0 @@
export default {
primary: '#896A00',
secondary: '#254E70',
black: '#0C0C0C',
positive: '#0C0C0C',
textPrimary: '#896A00',
backgroundPrimary: '#FAEEC5',
// Define font size variables
fontSmall: '18px',
fontMedium: '30px',
fontLarge: '45px',
};

View File

@@ -1,11 +0,0 @@
import base from './base';
import { IThemes } from './utils';
/**
* The default theme to load
*/
export const DEFAULT_THEME = 'base';
export const themes: IThemes = {
base,
};

View File

@@ -1,6 +0,0 @@
import { extend } from './utils';
import base from './base';
export default extend(base, {
// Custom styles for primary theme
});

View File

@@ -1,47 +0,0 @@
import { themes } from './index';
export interface ITheme {
[key: string]: string;
}
export interface IThemes {
[key: string]: ITheme;
}
export interface IMappedTheme {
[key: string]: string | null;
}
export const mapTheme = (variables: ITheme): IMappedTheme => {
return {
'--color-primary': variables.primary || '',
'--color-secondary': variables.secondary || '',
'--color-positive': variables.positive || '',
'--color-negative': variables.negative || '',
'--color-text-primary': variables.textPrimary || '',
'--background-primary': variables.backgroundPrimary || '',
'--background-sec': variables.backgroundSecondary || '',
'--font-size-small': variables.fontSmall || '18px',
'--font-size-medium': variables.fontMedium || '30px',
'--font-size-large': variables.fontLarge || '45px',
};
};
export const applyTheme = (theme: string): void => {
const themeObject: IMappedTheme = mapTheme(themes[theme]);
if (!themeObject) return;
const root = document.documentElement;
Object.keys(themeObject).forEach((property) => {
if (property === 'name') {
return;
}
root.style.setProperty(property, themeObject[property]);
});
};
export const extend = (extending: ITheme, newTheme: ITheme): ITheme => {
return { ...extending, ...newTheme };
};

View File

@@ -1,23 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "preserve",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true,
"types": ["jest", "node"]
},
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"],
"exclude": [
"node_modules",
"jest.config.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts"
]
}

View File

@@ -1,21 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"],
"jsx": "react"
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}

View File

@@ -1,541 +0,0 @@
const { URL } = require('url');
const bytes = require('bytes');
const slugify = require('slugify');
const config = require('../next.config.js');
module.exports.ckanToDataPackage = function (descriptor) {
// Make a copy
const datapackage = JSON.parse(JSON.stringify(descriptor));
// Lowercase name
datapackage.name = datapackage.name.toLowerCase();
// Rename notes => description
if (datapackage.notes) {
datapackage.description = datapackage.notes;
delete datapackage.notes;
}
// Rename ckan_url => homepage
if (datapackage.ckan_url) {
datapackage.homepage = datapackage.ckan_url;
delete datapackage.ckan_url;
}
// Parse license
const license = {};
if (datapackage.license_id) {
license.type = datapackage.license_id;
delete datapackage.license_id;
}
if (datapackage.license_title) {
license.title = datapackage.license_title;
delete datapackage.license_title;
}
if (datapackage.license_url) {
license.url = datapackage.license_url;
delete datapackage.license_url;
}
if (Object.keys(license).length > 0) {
datapackage.license = license;
}
// Parse author and sources
const source = {};
if (datapackage.author) {
source.name = datapackage.author;
delete datapackage.author;
}
if (datapackage.author_email) {
source.email = datapackage.author_email;
delete datapackage.author_email;
}
if (datapackage.url) {
source.web = datapackage.url;
delete datapackage.url;
}
if (Object.keys(source).length > 0) {
datapackage.sources = [source];
}
// Parse maintainer
const author = {};
if (datapackage.maintainer) {
author.name = datapackage.maintainer;
delete datapackage.maintainer;
}
if (datapackage.maintainer_email) {
author.email = datapackage.maintainer_email;
delete datapackage.maintainer_email;
}
if (Object.keys(author).length > 0) {
datapackage.author = author;
}
// Parse tags
if (datapackage.tags) {
datapackage.keywords = [];
datapackage.tags.forEach((tag) => {
datapackage.keywords.push(tag.name);
});
delete datapackage.tags;
}
// Parse extras
// TODO
// Resources
datapackage.resources = datapackage.resources.map((resource) => {
if (resource.name) {
resource.title = resource.title || resource.name;
resource.name = resource.name.toLowerCase().replace(/ /g, '_');
} else {
resource.name = resource.id;
}
if (resource.url) {
resource.path = resource.url;
delete resource.url;
}
if (!resource.schema) {
// If 'fields' property exists use it as schema fields
if (resource.fields) {
if (typeof resource.fields === 'string') {
try {
resource.fields = JSON.parse(resource.fields);
} catch (e) {
console.log('Could not parse resource.fields');
}
}
resource.schema = { fields: resource.fields };
delete resource.fields;
}
}
return resource;
});
return datapackage;
};
/*
At the moment, we're considering only following examples of CKAN view:
1. recline_view => Data Explorer with Table view, Chart Builder, Map Builder
and Query Builder.
2. geojson_view => Leaflet map
3. pdf_view => our PDF viewer
4. recline_grid_view => our Table viewer
5. recline_graph_view => our Simple graph
6. recline_map_view => our Leaflet map
7. image_view => not supported at the moment
8. text_view => not supported at the moment
9. webpage_view => not supported at the moment
*/
module.exports.ckanViewToDataPackageView = (ckanView) => {
const viewTypeToSpecType = {
recline_view: 'dataExplorer', // from datastore data
recline_grid_view: 'table',
recline_graph_view: 'simple',
recline_map_view: 'tabularmap',
geojson_view: 'map',
pdf_view: 'document',
image_view: 'web',
webpage_view: 'web',
};
const dataPackageView = JSON.parse(JSON.stringify(ckanView));
dataPackageView.specType =
viewTypeToSpecType[ckanView.view_type] ||
dataPackageView.specType ||
'unsupported';
if (dataPackageView.specType === 'dataExplorer') {
dataPackageView.spec = {
widgets: [
{ specType: 'table' },
{ specType: 'simple' },
{ specType: 'tabularmap' },
],
};
} else if (dataPackageView.specType === 'simple') {
const graphTypeConvert = {
lines: 'line',
'lines-and-points': 'lines-and-points',
points: 'points',
bars: 'horizontal-bar',
columns: 'bar',
};
dataPackageView.spec = {
group: ckanView.group,
series: Array.isArray(ckanView.series)
? ckanView.series
: [ckanView.series],
type: graphTypeConvert[ckanView.graph_type] || 'line',
};
} else if (dataPackageView.specType === 'tabularmap') {
if (ckanView.map_field_type === 'geojson') {
dataPackageView.spec = {
geomField: ckanView.geojson_field,
};
} else {
dataPackageView.spec = {
lonField: ckanView.longitude_field,
latField: ckanView.latitude_field,
};
}
}
return dataPackageView;
};
/*
Takes single field descriptor from datastore data dictionary and coverts into
tableschema field descriptor.
*/
module.exports.dataStoreDataDictionaryToTableSchema = (dataDictionary) => {
const internalDataStoreFields = ['_id', '_full_text', '_count'];
if (internalDataStoreFields.includes(dataDictionary.id)) {
return null;
}
const dataDictionaryType2TableSchemaType = {
text: 'string',
int: 'integer',
float: 'number',
date: 'date',
time: 'time',
timestamp: 'datetime',
bool: 'boolean',
json: 'object',
};
const field = {
name: dataDictionary.id,
type: dataDictionaryType2TableSchemaType[dataDictionary.type] || 'any',
};
if (dataDictionary.info) {
const constraintsAttributes = [
'required',
'unique',
'minLength',
'maxLength',
'minimum',
'maximum',
'pattern',
'enum',
];
field.constraints = {};
Object.keys(dataDictionary.info).forEach((key) => {
if (constraintsAttributes.includes(key)) {
field.constraints[key] = dataDictionary.info[key];
} else {
field[key] = dataDictionary.info[key];
}
});
}
return field;
};
module.exports.convertToStandardCollection = (descriptor) => {
const standard = {
name: '',
title: '',
summary: '',
image: '',
count: null,
};
standard.name = descriptor.name;
standard.title = descriptor.title || descriptor.display_name;
standard.summary = descriptor.description || '';
standard.image = descriptor.image_display_url || descriptor.image_url;
standard.count = descriptor.package_count || 0;
standard.extras = descriptor.extras || [];
standard.groups = descriptor.groups || [];
return standard;
};
module.exports.convertToCkanSearchQuery = (query) => {
const ckanQuery = {
q: '',
fq: '',
rows: '',
start: '',
sort: '',
'facet.field': [
'organization',
'groups',
'tags',
'res_format',
'license_id',
],
'facet.limit': 5,
'facet.mincount': 0,
};
// Split by space but ignore spaces within double quotes:
if (query.q) {
query.q.match(/(?:[^\s"]+|"[^"]*")+/g).forEach((part) => {
if (part.includes(':')) {
ckanQuery.fq += part + ' ';
} else {
ckanQuery.q += part + ' ';
}
});
ckanQuery.fq = ckanQuery.fq.trim();
ckanQuery.q = ckanQuery.q.trim();
}
if (query.fq) {
ckanQuery.fq = ckanQuery.fq ? ckanQuery.fq + ' ' + query.fq : query.fq;
}
// standard 'size' => ckan 'rows'
ckanQuery.rows = query.size || '';
// standard 'from' => ckan 'start'
ckanQuery.start = query.from || '';
// standard 'sort' => ckan 'sort'
const sortQueries = [];
if (query.sort && query.sort.constructor == Object) {
for (let [key, value] of Object.entries(query.sort)) {
sortQueries.push(`${key} ${value}`);
}
ckanQuery.sort = sortQueries.join(',');
} else if (query.sort && query.sort.constructor == String) {
ckanQuery.sort = query.sort.replace(':', ' ');
} else if (query.sort && query.sort.constructor == Array) {
query.sort.forEach((sort) => {
sortQueries.push(sort.replace(':', ' '));
});
ckanQuery.sort = sortQueries.join(',');
}
// Facets
ckanQuery['facet.field'] = query['facet.field'] || ckanQuery['facet.field'];
ckanQuery['facet.limit'] = query['facet.limit'] || ckanQuery['facet.limit'];
ckanQuery['facet.mincount'] =
query['facet.mincount'] || ckanQuery['facet.mincount'];
ckanQuery['facet.field'] = query['facet.field'] || ckanQuery['facet.field'];
// Remove attributes with empty string, null or undefined values
Object.keys(ckanQuery).forEach(
(key) => !ckanQuery[key] && delete ckanQuery[key]
);
return ckanQuery;
};
module.exports.pagination = (c, m) => {
let current = c,
last = m,
delta = 2,
left = current - delta,
right = current + delta + 1,
range = [],
rangeWithDots = [],
l;
range.push(1);
for (let i = c - delta; i <= c + delta; i++) {
if (i >= left && i < right && i < m && i > 1) {
range.push(i);
}
}
range.push(m);
for (let i of range) {
if (l) {
if (i - l === 2) {
rangeWithDots.push(l + 1);
} else if (i - l !== 1) {
rangeWithDots.push('...');
}
}
rangeWithDots.push(i);
l = i;
}
return rangeWithDots;
};
module.exports.processMarkdown = require('markdown-it')({
html: true,
linkify: true,
typographer: true,
});
/**
* Process data package attributes prior to display to users.
* Process markdown
* Convert bytes to human readable format
* etc.
**/
module.exports.processDataPackage = function (datapackage) {
const newDatapackage = JSON.parse(JSON.stringify(datapackage));
if (newDatapackage.description) {
newDatapackage.descriptionHtml = module.exports.processMarkdown.render(
newDatapackage.description
);
}
if (newDatapackage.readme) {
newDatapackage.readmeHtml = module.exports.processMarkdown.render(
newDatapackage.readme
);
}
newDatapackage.formats = newDatapackage.formats || [];
// Per each resource:
newDatapackage.resources.forEach((resource) => {
if (resource.description) {
resource.descriptionHtml = module.exports.processMarkdown.render(
resource.description
);
}
// Normalize format (lowercase)
if (resource.format) {
resource.format = resource.format.toLowerCase();
newDatapackage.formats.push(resource.format);
}
// Convert bytes into human-readable format:
if (resource.size) {
resource.sizeFormatted = bytes(resource.size, { decimalPlaces: 0 });
}
});
return newDatapackage;
};
/**
* Create 'displayResources' property which has:
* resource: Object containing resource descriptor
* api: API URL for the resource if available, e.g., Datastore
* proxy: path via proxy for the resource if available
* cc_proxy: path via CKAN Classic proxy if available
* slug: slugified name of a resource
**/
module.exports.prepareResourcesForDisplay = function (datapackage) {
const newDatapackage = JSON.parse(JSON.stringify(datapackage));
newDatapackage.displayResources = [];
newDatapackage.resources.forEach((resource, index) => {
const api = resource.datastore_active
? config.get('API_URL') +
'datastore_search?resource_id=' +
resource.id +
'&sort=_id asc'
: null;
// Use proxy path if datastore/filestore proxies are given:
let proxy, cc_proxy;
try {
const resourceUrl = new URL(resource.path);
if (
resourceUrl.host === config.get('PROXY_DATASTORE') &&
resource.format !== 'pdf'
) {
proxy = '/proxy/datastore' + resourceUrl.pathname + resourceUrl.search;
}
if (
resourceUrl.host === config.get('PROXY_FILESTORE') &&
resource.format !== 'pdf'
) {
proxy = '/proxy/filestore' + resourceUrl.pathname + resourceUrl.search;
}
// Store a CKAN Classic proxy path
// https://github.com/ckan/ckan/blob/master/ckanext/resourceproxy/plugin.py#L59
const apiUrlObject = new URL(config.get('API_URL'));
cc_proxy =
apiUrlObject.origin +
`/dataset/${datapackage.id}/resource/${resource.id}/proxy`;
} catch (e) {
console.warn(e);
}
const displayResource = {
resource,
api, // URI for getting the resource via API, e.g., Datastore. Useful when you want to fetch only 100 rows or similar.
proxy, // alternative for path in case there is CORS issue
cc_proxy,
slug: slugify(resource.name) + '-' + index, // Used for anchor links
};
newDatapackage.displayResources.push(displayResource);
});
return newDatapackage;
};
/**
* Prepare 'views' property which is used by 'datapackage-views-js' library to
* render visualizations such as tables, graphs and maps.
**/
module.exports.prepareViews = function (datapackage) {
const newDatapackage = JSON.parse(JSON.stringify(datapackage));
newDatapackage.views = newDatapackage.views || [];
newDatapackage.resources.forEach((resource) => {
const resourceViews =
resource.views &&
resource.views.map((view) => {
view.resources = [resource.name];
return view;
});
newDatapackage.views = newDatapackage.views.concat(resourceViews);
});
return newDatapackage;
};
/**
* Create 'dataExplorers' property which is used by 'data-explorer' library to
* render data explorer widgets.
**/
module.exports.prepareDataExplorers = function (datapackage) {
const newDatapackage = JSON.parse(JSON.stringify(datapackage));
newDatapackage.displayResources.forEach((displayResource, idx) => {
newDatapackage.displayResources[idx].dataExplorers = [];
displayResource.resource.views &&
displayResource.resource.views.forEach((view) => {
const widgets = [];
if (view.specType === 'dataExplorer') {
view.spec.widgets.forEach((widget, index) => {
const widgetNames = {
table: 'Table',
simple: 'Chart',
tabularmap: 'Map',
};
widget = {
name: widgetNames[widget.specType] || 'Widget-' + index,
active: index === 0 ? true : false,
datapackage: {
views: [
{
id: view.id,
specType: widget.specType,
},
],
},
};
widgets.push(widget);
});
} else {
const widget = {
name: view.title || '',
active: true,
datapackage: {
views: [view],
},
};
widgets.push(widget);
}
displayResource.resource.api =
displayResource.resource.api || displayResource.api;
const dataExplorer = JSON.stringify({
widgets,
datapackage: {
resources: [displayResource.resource],
},
}).replace(/'/g, '&#x27;');
newDatapackage.displayResources[idx].dataExplorers.push(dataExplorer);
});
});
return newDatapackage;
};