diff --git a/components/Error.tsx b/components/Error.tsx new file mode 100644 index 00000000..87823668 --- /dev/null +++ b/components/Error.tsx @@ -0,0 +1,15 @@ +export default function ErrorMessage({ message }) { + return ( + + ); +} diff --git a/components/dataset/About.tsx b/components/dataset/About.tsx index 9aac13db..1cb63447 100644 --- a/components/dataset/About.tsx +++ b/components/dataset/About.tsx @@ -1,4 +1,41 @@ -export default function About({ datapackage }) { +import ErrorMessage from '../Error'; +import { NetworkStatus } from 'apollo-client'; +import { useQuery } from '@apollo/react-hooks'; +import gql from 'graphql-tag'; + +export const GET_DATAPACKAGE_QUERY = gql` + query dataset($id: String) { + dataset(id: $id) @rest(type: "Response", path: "package_show?{args}") { + result { + name + title + size + metadata_created + metadata_modified + resources { + name + } + } + } + } +`; + +export default function About({ variables }) { + const { loading, error, data, fetchMore, networkStatus } = useQuery( + GET_DATAPACKAGE_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 ; + if (loading) return
Loading
; + + const { result } = data.dataset; return ( <> @@ -15,11 +52,11 @@ export default function About({ datapackage }) { - - + + - - + + diff --git a/components/dataset/Org.tsx b/components/dataset/Org.tsx index 0971532c..00b7fd9f 100644 --- a/components/dataset/Org.tsx +++ b/components/dataset/Org.tsx @@ -1,20 +1,53 @@ import Link from 'next/link'; +import ErrorMessage from '../Error'; +import { NetworkStatus } from 'apollo-client'; +import { useQuery } from '@apollo/react-hooks'; +import gql from 'graphql-tag'; -export default function Org({ org }) { +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, fetchMore, networkStatus } = 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 ; + if (loading) return
Loading
; + + const { organization } = data.dataset.result; return ( <> - {org ? ( + {organization ? ( <> - + - {org.title || org.name} + {organization.title || organization.name} diff --git a/components/dataset/Resources.tsx b/components/dataset/Resources.tsx index e4753de1..1105a47c 100644 --- a/components/dataset/Resources.tsx +++ b/components/dataset/Resources.tsx @@ -1,6 +1,43 @@ import Link from 'next/link'; +import ErrorMessage from '../Error'; +import { NetworkStatus } from 'apollo-client'; +import { useQuery } from '@apollo/react-hooks'; +import gql from 'graphql-tag'; + +export const GET_DATAPACKAGE_QUERY = gql` + query dataset($id: String) { + dataset(id: $id) @rest(type: "Response", path: "package_show?{args}") { + result { + name + resources { + name + title + format + created + last_modified + } + } + } + } +`; + +export default function Resources({ variables }) { + const { loading, error, data, fetchMore, networkStatus } = useQuery( + GET_DATAPACKAGE_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 ; + if (loading) return
Loading
; + + const { result } = data.dataset; -export default function Resources({ datapackage }) { return ( <>

Data Files

@@ -15,10 +52,10 @@ export default function Resources({ datapackage }) {
- {datapackage.resources.map((resource, index) => ( + {result.resources.map((resource, index) => ( @@ -26,7 +63,7 @@ export default function Resources({ datapackage }) { diff --git a/components/home/Nav.tsx b/components/home/Nav.tsx index ea671b02..96570b30 100644 --- a/components/home/Nav.tsx +++ b/components/home/Nav.tsx @@ -35,19 +35,20 @@ export default function Nav() { Search - - - Docs - - - - - GitHub - - + + Docs + + + GitHub + ); diff --git a/components/resource/About.tsx b/components/resource/About.tsx index e4a28423..7caa9765 100644 --- a/components/resource/About.tsx +++ b/components/resource/About.tsx @@ -1,6 +1,45 @@ import Link from 'next/link'; +import ErrorMessage from '../Error'; +import { NetworkStatus } from 'apollo-client'; +import { useQuery } from '@apollo/react-hooks'; +import gql from 'graphql-tag'; -export default function About({ resource }) { +const QUERY = gql` + query dataset($id: String) { + dataset(id: $id) @rest(type: "Response", path: "package_show?{args}") { + result { + resources { + name + id + title + description + format + size + created + last_modified + url + } + } + } + } +`; + +export default function About({ variables }) { + const { loading, error, data } = useQuery(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 ; + if (loading) return
Loading
; + + const { result } = data.dataset; + const resource = result.resources.find( + (item) => item.name === variables.resource + ); return ( <>
{datapackage.resources.length}{datapackage.size || 'NA'}{result.resources.length}{result.size || 'NA'} {datapackage.created}{datapackage.modified}{result.metadata_created}{result.metadata_modified}
- + {resource.title || resource.name} {resource.created} {resource.last_modified} - + Preview
@@ -26,11 +65,12 @@ export default function About({ resource }) { diff --git a/components/resource/DataExplorer.tsx b/components/resource/DataExplorer.tsx index 6f2fbd8a..5fdf4363 100644 --- a/components/resource/DataExplorer.tsx +++ b/components/resource/DataExplorer.tsx @@ -1,5 +1,45 @@ import Link from 'next/link'; +import ErrorMessage from '../Error'; +import { NetworkStatus } from 'apollo-client'; +import { useQuery } from '@apollo/react-hooks'; +import gql from 'graphql-tag'; + +const QUERY = gql` + query dataset($id: String) { + dataset(id: $id) @rest(type: "Response", path: "package_show?{args}") { + result { + resources { + name + id + title + description + format + size + created + last_modified + url + } + } + } + } +`; + +export default function DataExplorer({ variables }) { + const { loading, error, data } = useQuery(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 ; + if (loading) return
Loading
; + + const { result } = data.dataset; + const resource = result.resources.find( + (item) => item.name === variables.resource + ); -export default function DataExplorer({ resource }) { return <>{JSON.stringify(resource)}; } diff --git a/components/search/Form.tsx b/components/search/Form.tsx new file mode 100644 index 00000000..4d71b26d --- /dev/null +++ b/components/search/Form.tsx @@ -0,0 +1,62 @@ +import { useState } from 'react'; +import { useRouter } from 'next/router'; + +export default function Form() { + const router = useRouter(); + const [q, setQ] = useState(router.query.q); + const [sort, setSort] = useState(router.query.sort); + + const handleChange = (event) => { + if (event.target.name === 'q') { + setQ(event.target.value); + } else if (event.target.name === 'sort') { + setSort(event.target.value); + } + }; + + const handleSubmit = (event) => { + event.preventDefault(); + router.push({ + pathname: '/search', + query: { q, sort }, + }); + }; + + return ( +
+
+ + +
+
+ + +
+ + ); +} diff --git a/components/search/Input.tsx b/components/search/Input.tsx deleted file mode 100644 index 81c53244..00000000 --- a/components/search/Input.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useState } from 'react'; -import { useRouter } from 'next/router'; -import Link from 'next/link'; - -export default function Input({ query }) { - const router = useRouter(); - const [q, setQ] = useState(query.q); - - const handleChange = (event) => { - setQ(event.target.value); - }; - - const handleSubmit = (event) => { - event.preventDefault(); - router.push({ - pathname: '/search', - query: { q }, - }); - }; - - return ( -
- - - - ); -} diff --git a/components/search/Item.tsx b/components/search/Item.tsx index cde01fd0..6a6725e3 100644 --- a/components/search/Item.tsx +++ b/components/search/Item.tsx @@ -25,7 +25,9 @@ export default function Item({ datapackage }) { : 'dataset'} -
{datapackage.description}
+
+ {datapackage.description || datapackage.notes} +
); } diff --git a/components/search/List.tsx b/components/search/List.tsx index e0e33199..c8946722 100644 --- a/components/search/List.tsx +++ b/components/search/List.tsx @@ -1,10 +1,44 @@ import Item from './Item'; +import ErrorMessage from '../Error'; +import { NetworkStatus } from 'apollo-client'; +import { useQuery } from '@apollo/react-hooks'; +import gql from 'graphql-tag'; -export default function List({ datapackages }) { +const QUERY = gql` + query search($q: String, $sort: String) { + search(q: $q, sort: $sort) + @rest(type: "Search", path: "package_search?{args}") { + result { + results { + name + title + organization { + name + title + } + } + } + } + } +`; + +export default function List({ variables }) { + const { loading, error, data, fetchMore, networkStatus } = useQuery(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 ; + if (loading) return
Loading
; + + const { result } = data.search; return (
    - {datapackages.map((datapackage, index) => ( - + {result.results.map((pkg, index) => ( + ))}
); diff --git a/components/search/Sort.tsx b/components/search/Sort.tsx deleted file mode 100644 index d09e476c..00000000 --- a/components/search/Sort.tsx +++ /dev/null @@ -1,14 +0,0 @@ -export default function Sort() { - return ( -
- - -
- ); -} diff --git a/components/search/Total.tsx b/components/search/Total.tsx index e370af1c..0a77a25a 100644 --- a/components/search/Total.tsx +++ b/components/search/Total.tsx @@ -1,7 +1,33 @@ -export default function Total({ total }) { +import { useQuery } from '@apollo/react-hooks'; +import gql from 'graphql-tag'; + +const QUERY = gql` + query search($q: String, $sort: String) { + search(q: $q, sort: $sort) + @rest(type: "Search", path: "package_search?{args}") { + result { + count + } + } + } +`; + +export default function Total({ variables }) { + const { loading, error, data } = useQuery(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 (loading) return
Loading
; + + const { result } = data.search; + return (

- {total} results found + {result.count} results found

); } diff --git a/config/index.js b/config/index.js deleted file mode 100644 index dd7c03c5..00000000 --- a/config/index.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -const nconf = require('nconf'); -require('dotenv').config({ path: process.env.DOTENV_PATH || '.env' }); - -nconf.argv().env(); - -nconf.use('memory'); - -const dms = (process.env.DMS || 'http://mock.ckan').replace(/\/?$/, ''); -const cms = (process.env.CMS || 'http://mock.cms').replace(/\/?$/, ''); - -// This is the object that you want to override in your own local config -nconf.defaults({ - env: process.env.NODE_ENV || 'development', - debug: process.env.DEBUG || false, - DMS: dms, - CMS: cms, -}); - -module.exports = { - get: nconf.get.bind(nconf), - set: nconf.set.bind(nconf), - reset: nconf.reset.bind(nconf), -}; diff --git a/lib/apolloClient.ts b/lib/apolloClient.ts new file mode 100644 index 00000000..c3cdecb6 --- /dev/null +++ b/lib/apolloClient.ts @@ -0,0 +1,79 @@ +import { useMemo } from 'react'; +import getConfig from 'next/config'; +import { ApolloClient } from 'apollo-client'; +import { InMemoryCache } from 'apollo-cache-inmemory'; +import { RestLink } from 'apollo-link-rest'; + +let apolloClient; + +const restLink = new RestLink({ + uri: getConfig().publicRuntimeConfig.DMS + '/api/3/action/', + typePatcher: { + Search: ( + data: any, + outerType: string, + patchDeeper: RestLink.FunctionalTypePatcher + ): 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, + outerType: string, + patchDeeper: RestLink.FunctionalTypePatcher + ): 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, + cache: new InMemoryCache(), + }); +} + +export function initializeApollo(initialState = null) { + const _apolloClient = 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) { + const store = useMemo(() => initializeApollo(initialState), [initialState]); + return store; +} diff --git a/mocks/index.js b/mocks/index.js index 017f19ed..1b4a9a0b 100644 --- a/mocks/index.js +++ b/mocks/index.js @@ -28,6 +28,7 @@ const gdp = { }, metadata_created: '2019-03-07T11:56:19.696257', metadata_modified: '2019-03-07T12:03:58.817280', + size: '', }; const population = { @@ -125,9 +126,7 @@ module.exports.initMocks = function () { nock('http://mock.ckan/api/3/action', { encodedQueryParams: true }) .persist() // 1. Call without query. - .get( - '/package_search?facet.field=organization&facet.field=groups&facet.field=tags&facet.field=res_format&facet.field=license_id&facet.limit=5' - ) + .get('/package_search?') .reply(200, { success: true, result: { @@ -139,9 +138,7 @@ module.exports.initMocks = function () { }, }) // 2. Call with `q=gdp` query. - .get( - '/package_search?q=gdp&facet.field=organization&facet.field=groups&facet.field=tags&facet.field=res_format&facet.field=license_id&facet.limit=5' - ) + .get('/package_search?q=gdp') .reply(200, { success: true, result: { diff --git a/next.config.js b/next.config.js new file mode 100644 index 00000000..577f4853 --- /dev/null +++ b/next.config.js @@ -0,0 +1,37 @@ +const { PHASE_DEVELOPMENT_SERVER } = require('next/constants'); + +module.exports = (phase, { defaultConfig }) => { + const dms = process.env.DMS; + 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 { + publicRuntimeConfig: { + DMS: dms ? dms.replace(/\/?$/, '') : 'http://mock.ckan', + }, + }; + } + return { + publicRuntimeConfig: { + DMS: dms ? dms.replace(/\/?$/, '') : 'https://demo.ckan.org', + }, + }; +}; diff --git a/package.json b/package.json index 4210fc07..f602ed84 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,18 @@ "pre-commit": "echo 'formating your changes.....' && prettier --single-quote --write" }, "dependencies": { + "@apollo/react-hooks": "^3.1.5", + "apollo-cache-inmemory": "^1.6.6", + "apollo-client": "^2.6.10", + "apollo-link": "^1.2.14", + "apollo-link-rest": "0.7.3", "bytes": "^3.1.0", + "graphql": "^15.1.0", + "graphql-anywhere": "^4.2.7", + "graphql-tag": "^2.10.3", "markdown-it": "^11.0.0", "next": "9.4.2", - "querystring": "^0.2.0", + "qs": "^6.9.4", "react": "16.13.1", "react-dom": "16.13.1", "slugify": "^1.4.0" @@ -27,12 +35,12 @@ "@types/jest": "^25.2.3", "@types/react": "^16.9.35", "babel-jest": "^26.0.1", - "dotenv": "^8.2.0", + "babel-plugin-graphql-tag": "^2.5.0", "husky": ">=4", "jest": "^26.0.1", "lint-staged": ">=10", - "nconf": "^0.10.0", "nock": "^12.0.3", + "npm-run-all": "^4.1.5", "postcss-preset-env": "^6.7.0", "prettier": "2.0.5", "react-test-renderer": "^16.13.1", diff --git a/pages/[org]/[dataset]/index.tsx b/pages/[org]/[dataset]/index.tsx index 43210a48..5122fca8 100644 --- a/pages/[org]/[dataset]/index.tsx +++ b/pages/[org]/[dataset]/index.tsx @@ -1,39 +1,82 @@ import { GetServerSideProps } from 'next'; -import config from '../../../config'; +import { useQuery } from '@apollo/react-hooks'; +import { initializeApollo } from '../../../lib/apolloClient'; import utils from '../../../utils'; import Head from 'next/head'; 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 gql from 'graphql-tag'; + +const QUERY = gql` + query dataset($id: String) { + dataset(id: $id) @rest(type: "Response", path: "package_show?{args}") { + result { + name + title + size + metadata_created + metadata_modified + resources { + name + title + format + created + last_modified + } + organization { + name + title + image_url + } + } + } + } +`; + +function Dataset({ variables }) { + const { data, loading } = useQuery(QUERY, { variables }); + + if (loading) return
Loading
; + const { result } = data.dataset; -function Dataset({ datapackage }) { return ( <> - Portal | {datapackage.title || datapackage.name} + Portal | {result.title || result.name}
{resource.created} {resource.last_modified || ''} - - - {resource.format} - - + + {resource.format} +