[@portaljs/ckan][xl] - add ckan package and example using it
This commit is contained in:
parent
eac0a22aa8
commit
c82bfdd847
3
examples/ckan/.eslintrc.json
Normal file
3
examples/ckan/.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
||||||
35
examples/ckan/.gitignore
vendored
Normal file
35
examples/ckan/.gitignore
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
38
examples/ckan/README.md
Normal file
38
examples/ckan/README.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
||||||
|
|
||||||
|
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||||
6
examples/ckan/next.config.js
Normal file
6
examples/ckan/next.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
4287
examples/ckan/package-lock.json
generated
Normal file
4287
examples/ckan/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
examples/ckan/package.json
Normal file
27
examples/ckan/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "ckan",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^2.0.18",
|
||||||
|
"@portaljs/ckan": "*",
|
||||||
|
"@types/node": "20.2.3",
|
||||||
|
"@types/react": "18.2.6",
|
||||||
|
"@types/react-dom": "18.2.4",
|
||||||
|
"autoprefixer": "10.4.14",
|
||||||
|
"eslint": "8.41.0",
|
||||||
|
"eslint-config-next": "13.4.3",
|
||||||
|
"next": "13.4.3",
|
||||||
|
"postcss": "8.4.23",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"tailwindcss": "3.3.2",
|
||||||
|
"typescript": "5.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
176
examples/ckan/pages/[org]/[dataset]/index.tsx
Normal file
176
examples/ckan/pages/[org]/[dataset]/index.tsx
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import Head from "next/head";
|
||||||
|
import { CKAN, Dataset } from "@portaljs/ckan";
|
||||||
|
import {
|
||||||
|
ChevronRightIcon,
|
||||||
|
HomeIcon,
|
||||||
|
PaperClipIcon,
|
||||||
|
} from "@heroicons/react/20/solid";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: any) => {
|
||||||
|
try {
|
||||||
|
const datasetName = context.params?.dataset;
|
||||||
|
if (!datasetName) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const ckan = new CKAN("https://demo.dev.datopian.com");
|
||||||
|
const dataset = await ckan.getDatasetDetails(datasetName as string);
|
||||||
|
if (!dataset) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
props: { dataset },
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DatasetPage({
|
||||||
|
dataset,
|
||||||
|
}: {
|
||||||
|
dataset: Dataset;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{`${dataset.title || dataset.name} - Dataset`}</title>
|
||||||
|
<meta name="description" content="Generated by create next app" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
|
<main className="flex min-h-screen flex-col items-center justify-between p-24 bg-zinc-900">
|
||||||
|
<div className="bg-white p-8 my-4 rounded-lg">
|
||||||
|
<nav className="flex px-4 py-8" aria-label="Breadcrumb">
|
||||||
|
<ol role="list" className="flex items-center space-x-4">
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
<Link href="/" className="text-gray-400 hover:text-gray-500">
|
||||||
|
<HomeIcon
|
||||||
|
className="h-5 w-5 flex-shrink-0"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span className="sr-only">Home</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<ChevronRightIcon
|
||||||
|
className="h-5 w-5 flex-shrink-0 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-700"
|
||||||
|
aria-current={"page"}
|
||||||
|
>
|
||||||
|
{dataset.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
{dataset && (
|
||||||
|
<div>
|
||||||
|
<div className="px-4 sm:px-0">
|
||||||
|
<h3 className="text-base font-semibold leading-7 text-gray-900">
|
||||||
|
{dataset.title || dataset.name}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 max-w-2xl text-sm leading-6 text-gray-500">
|
||||||
|
Dataset details
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 border-t border-gray-100">
|
||||||
|
<dl className="divide-y divide-gray-100">
|
||||||
|
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt className="text-sm font-medium leading-6 text-gray-900">
|
||||||
|
Title
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
||||||
|
{dataset.title}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
{dataset.tags && dataset.tags.length > 0 && (
|
||||||
|
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt className="text-sm font-medium leading-6 text-gray-900">
|
||||||
|
Tags
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
||||||
|
{dataset.tags.map((tag) => tag.display_name).join(", ")}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{dataset.tags && dataset.tags.length > 0 && (
|
||||||
|
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt className="text-sm font-medium leading-6 text-gray-900">
|
||||||
|
URL
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
||||||
|
{dataset.url}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
{dataset.notes && (
|
||||||
|
<>
|
||||||
|
<dt className="text-sm font-medium leading-6 text-gray-900">
|
||||||
|
Description
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
||||||
|
{dataset.notes}
|
||||||
|
</dd>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt className="text-sm font-medium leading-6 text-gray-900">
|
||||||
|
Files
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-2 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||||
|
<ul
|
||||||
|
role="list"
|
||||||
|
className="divide-y divide-gray-100 rounded-md border border-gray-200"
|
||||||
|
>
|
||||||
|
{dataset.resources.map((resource) => (
|
||||||
|
<li key={resource.id} className="flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6">
|
||||||
|
<div className="flex w-0 flex-1 items-center">
|
||||||
|
<PaperClipIcon
|
||||||
|
className="h-5 w-5 flex-shrink-0 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<div className="ml-4 flex min-w-0 flex-1 gap-2">
|
||||||
|
<span className="truncate font-medium">
|
||||||
|
{resource.name || resource.id}
|
||||||
|
</span>
|
||||||
|
<span className="flex-shrink-0 text-gray-400">
|
||||||
|
{resource.size}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 flex-shrink-0">
|
||||||
|
<a
|
||||||
|
href={resource.url}
|
||||||
|
className="font-medium hover:text-indigo-500"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
examples/ckan/pages/_app.tsx
Normal file
7
examples/ckan/pages/_app.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import '@/styles/globals.css'
|
||||||
|
import '@portaljs/ckan/styles.css'
|
||||||
|
import type { AppProps } from 'next/app'
|
||||||
|
|
||||||
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
|
return <Component {...pageProps} />
|
||||||
|
}
|
||||||
13
examples/ckan/pages/_document.tsx
Normal file
13
examples/ckan/pages/_document.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Html, Head, Main, NextScript } from 'next/document'
|
||||||
|
|
||||||
|
export default function Document() {
|
||||||
|
return (
|
||||||
|
<Html lang="en">
|
||||||
|
<Head />
|
||||||
|
<body>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
||||||
13
examples/ckan/pages/api/hello.ts
Normal file
13
examples/ckan/pages/api/hello.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
|
type Data = {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse<Data>
|
||||||
|
) {
|
||||||
|
res.status(200).json({ name: 'John Doe' })
|
||||||
|
}
|
||||||
52
examples/ckan/pages/index.tsx
Normal file
52
examples/ckan/pages/index.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
CKAN,
|
||||||
|
DatasetSearchForm,
|
||||||
|
ListOfDatasets,
|
||||||
|
PackageSearchOptions,
|
||||||
|
Organization,
|
||||||
|
Group,
|
||||||
|
} from "@portaljs/ckan";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export async function getServerSideProps() {
|
||||||
|
const ckan = new CKAN("https://demo.dev.datopian.com");
|
||||||
|
const groups = await ckan.getGroupsWithDetails();
|
||||||
|
const orgs = await ckan.getOrgsWithDetails();
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
groups,
|
||||||
|
orgs,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home({
|
||||||
|
orgs,
|
||||||
|
groups,
|
||||||
|
}: {
|
||||||
|
orgs: Organization[];
|
||||||
|
groups: Group[];
|
||||||
|
}) {
|
||||||
|
const ckan = new CKAN("https://demo.dev.datopian.com");
|
||||||
|
const [options, setOptions] = useState<PackageSearchOptions>({
|
||||||
|
offset: 0,
|
||||||
|
limit: 5,
|
||||||
|
tags: [],
|
||||||
|
groups: [],
|
||||||
|
orgs: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-screen flex-col items-center justify-between p-24 bg-zinc-900">
|
||||||
|
<DatasetSearchForm
|
||||||
|
options={options}
|
||||||
|
setOptions={setOptions}
|
||||||
|
groups={groups}
|
||||||
|
orgs={orgs}
|
||||||
|
/>
|
||||||
|
<div className="bg-white p-8 my-4 rounded-lg">
|
||||||
|
<ListOfDatasets options={options} setOptions={setOptions} ckan={ckan} />{" "}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
examples/ckan/public/favicon.ico
Normal file
BIN
examples/ckan/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
1
examples/ckan/public/next.svg
Normal file
1
examples/ckan/public/next.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
examples/ckan/public/vercel.svg
Normal file
1
examples/ckan/public/vercel.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||||
|
After Width: | Height: | Size: 629 B |
3
examples/ckan/styles/globals.css
Normal file
3
examples/ckan/styles/globals.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
18
examples/ckan/tailwind.config.js
Normal file
18
examples/ckan/tailwind.config.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
backgroundImage: {
|
||||||
|
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||||
|
'gradient-conic':
|
||||||
|
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
23
examples/ckan/tsconfig.json
Normal file
23
examples/ckan/tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
6
examples/learn/postcss.config.js
Normal file
6
examples/learn/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
16
packages/ckan/.eslintrc.cjs
Normal file
16
packages/ckan/.eslintrc.cjs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2020: true
|
||||||
|
},
|
||||||
|
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', 'plugin:storybook/recommended'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module'
|
||||||
|
},
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': 'warn'
|
||||||
|
}
|
||||||
|
};
|
||||||
24
packages/ckan/.gitignore
vendored
Normal file
24
packages/ckan/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
29
packages/ckan/README.md
Normal file
29
packages/ckan/README.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# PortalJS React Components
|
||||||
|
|
||||||
|
**Storybook:** https://storybook.portaljs.org
|
||||||
|
**Docs**: https://portaljs.org/docs
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To install this package on your project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i @portaljs/components
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: React 18 is required.
|
||||||
|
|
||||||
|
You'll also have to import the styles CSS file in your project:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// E.g.: Next.js => pages/_app.tsx
|
||||||
|
import '@portaljs/components/styles.css'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dev
|
||||||
|
|
||||||
|
Use Storybook to work on components by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run storybook
|
||||||
|
```
|
||||||
24959
packages/ckan/package-lock.json
generated
Normal file
24959
packages/ckan/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
packages/ckan/package.json
Normal file
41
packages/ckan/package.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "@portaljs/ckan",
|
||||||
|
"version": "0.1.2",
|
||||||
|
"type": "module",
|
||||||
|
"description": "https://portaljs.org",
|
||||||
|
"keywords": [
|
||||||
|
"data portal",
|
||||||
|
"data catalog",
|
||||||
|
"table",
|
||||||
|
"charts",
|
||||||
|
"visualization"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && vite build && npm run build-tailwind",
|
||||||
|
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"prepack": "json -f package.json -I -e \"delete this.devDependencies; delete this.dependencies\"",
|
||||||
|
"build-tailwind": "NODE_ENV=production npx tailwindcss -o ./dist/styles.css --minify"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"main": "./dist/components.umd.js",
|
||||||
|
"module": "./dist/components.es.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/components.es.js",
|
||||||
|
"require": "./dist/components.umd.js"
|
||||||
|
},
|
||||||
|
"./styles.css": {
|
||||||
|
"import": "./dist/styles.css"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
packages/ckan/portaljs-ckan-0.1.2.tgz
Normal file
BIN
packages/ckan/portaljs-ckan-0.1.2.tgz
Normal file
Binary file not shown.
6
packages/ckan/postcss.config.js
Normal file
6
packages/ckan/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
98
packages/ckan/src/components/DatasetCard.tsx
Normal file
98
packages/ckan/src/components/DatasetCard.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { format } from "timeago.js";
|
||||||
|
import { Dataset } from "../interfaces/dataset.interface";
|
||||||
|
import ResourceCard from "./ResourceCard";
|
||||||
|
|
||||||
|
export default function DatasetCard({
|
||||||
|
dataset,
|
||||||
|
showOrg = true,
|
||||||
|
urlPrefix = ""
|
||||||
|
}: {
|
||||||
|
dataset: Dataset;
|
||||||
|
showOrg: boolean;
|
||||||
|
urlPrefix?: string;
|
||||||
|
}) {
|
||||||
|
const resourceBgColors = {
|
||||||
|
PDF: "bg-cyan-300",
|
||||||
|
CSV: "bg-emerald-300",
|
||||||
|
JSON: "bg-yellow-300",
|
||||||
|
ODS: "bg-amber-400",
|
||||||
|
XLS: "bg-orange-300",
|
||||||
|
DOC: "bg-red-300",
|
||||||
|
SHP: "bg-purple-400",
|
||||||
|
HTML: "bg-pink-300",
|
||||||
|
};
|
||||||
|
|
||||||
|
const resourceBgColorsProxy = new Proxy(resourceBgColors, {
|
||||||
|
get: (obj, prop) => {
|
||||||
|
if (prop in obj) {
|
||||||
|
return obj[prop]
|
||||||
|
}
|
||||||
|
return "bg-amber-400"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function DatasetInformations() {
|
||||||
|
return (
|
||||||
|
<div className="flex align-center gap-2">
|
||||||
|
{(dataset.resources.length > 0 && dataset.resources[0].format && (
|
||||||
|
<>
|
||||||
|
{showOrg !== false && (
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
resourceBgColorsProxy[
|
||||||
|
dataset.resources[0].format as keyof typeof resourceBgColors
|
||||||
|
]
|
||||||
|
} px-4 py-1 rounded-full text-xs`}
|
||||||
|
>
|
||||||
|
{dataset.organization
|
||||||
|
? dataset.organization.title
|
||||||
|
: "No organization"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
resourceBgColorsProxy[
|
||||||
|
dataset.resources[0].format as keyof typeof resourceBgColors
|
||||||
|
]
|
||||||
|
} px-4 py-1 rounded-full text-xs`}
|
||||||
|
>
|
||||||
|
{dataset.metadata_created && format(dataset.metadata_created)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)) || (
|
||||||
|
<>
|
||||||
|
{showOrg !== false && (
|
||||||
|
<span className="bg-gray-200 px-4 py-1 rounded-full text-xs">
|
||||||
|
{dataset.organization
|
||||||
|
? dataset.organization.title
|
||||||
|
: "No organization"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="bg-gray-200 px-4 py-1 rounded-full text-xs">
|
||||||
|
{dataset.metadata_created && format(dataset.metadata_created)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<article className="grid grid-cols-1 md:grid-cols-7 gap-x-2">
|
||||||
|
<ResourceCard
|
||||||
|
resource={dataset?.resources.find((resource) => resource.format)}
|
||||||
|
/>
|
||||||
|
<div className="col-span-6 place-content-start flex flex-col gap-1">
|
||||||
|
<Link href={`${urlPrefix}/@${dataset.organization?.name}/${dataset.name}`}>
|
||||||
|
<h1 className="m-auto md:m-0 font-semibold text-lg text-zinc-900">
|
||||||
|
{dataset.title || "No title"}
|
||||||
|
</h1>
|
||||||
|
</Link>
|
||||||
|
<p className="text-sm font-normal text-stone-500 line-clamp-2 h-[44px] overflow-y-hidden ">
|
||||||
|
{dataset.notes?.replace(/<\/?[^>]+(>|$)/g, "") || "No description"}
|
||||||
|
</p>
|
||||||
|
<DatasetInformations />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
146
packages/ckan/src/components/DatasetSearchFilters.tsx
Normal file
146
packages/ckan/src/components/DatasetSearchFilters.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { Field, Form, Formik, useFormikContext } from "formik";
|
||||||
|
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||||
|
import { PackageSearchOptions, Tag, Group, Organization, FilterObj } from "../interfaces";
|
||||||
|
|
||||||
|
function AutoSubmit({
|
||||||
|
setOptions,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
options: PackageSearchOptions;
|
||||||
|
setOptions: Dispatch<SetStateAction<PackageSearchOptions>>;
|
||||||
|
}) {
|
||||||
|
const { values } = useFormikContext<{
|
||||||
|
tags: string[];
|
||||||
|
orgs: string[];
|
||||||
|
groups: string[];
|
||||||
|
}>();
|
||||||
|
useEffect(() => {
|
||||||
|
setOptions({
|
||||||
|
...options,
|
||||||
|
groups: values.groups,
|
||||||
|
tags: values.tags,
|
||||||
|
orgs: values.orgs,
|
||||||
|
});
|
||||||
|
}, [values]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DatasetSearchFilters({
|
||||||
|
tags,
|
||||||
|
orgs,
|
||||||
|
groups,
|
||||||
|
setOptions,
|
||||||
|
options,
|
||||||
|
filtersName,
|
||||||
|
}: {
|
||||||
|
tags: Array<Tag>;
|
||||||
|
orgs: Array<Organization>;
|
||||||
|
groups: Array<Group>;
|
||||||
|
options: PackageSearchOptions;
|
||||||
|
setOptions: Dispatch<SetStateAction<PackageSearchOptions>>;
|
||||||
|
filtersName?: FilterObj | undefined;
|
||||||
|
}) {
|
||||||
|
const [seeMoreOrgs, setSeeMoreOrgs] = useState(false);
|
||||||
|
const [seeMoreTags, setSeeMoreTags] = useState(false);
|
||||||
|
const [seeMoreGroups, setSeeMoreGroups] = useState(false);
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
tags: [],
|
||||||
|
orgs: [],
|
||||||
|
groups: [],
|
||||||
|
}}
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
alert(JSON.stringify(values, null, 2));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<section className="bg-white rounded-lg xl:p-8 p-4 mb-4 max-h-[400px] overflow-y-auto">
|
||||||
|
<h1 className="font-bold pb-4">Refine by {`${filtersName?.org || "Organization"}`}</h1>
|
||||||
|
{orgs
|
||||||
|
.filter((org) => org.title || org.id)
|
||||||
|
.slice(0, seeMoreOrgs ? orgs.length : 5)
|
||||||
|
.map((org) => (
|
||||||
|
<div key={org.id}>
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id={org.id}
|
||||||
|
name="orgs"
|
||||||
|
value={org.name}
|
||||||
|
></Field>
|
||||||
|
<label className="ml-1.5" htmlFor={org.id}>
|
||||||
|
{org.title || org.display_name}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{orgs.length > 5 && (
|
||||||
|
<button
|
||||||
|
className="bg-gray-300 px-2 rounded text-gray-600 mt-2"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSeeMoreOrgs(!seeMoreOrgs)}
|
||||||
|
>
|
||||||
|
Show {seeMoreOrgs ? "less" : "more"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
<section className="bg-white rounded-lg xl:p-8 p-4 mb-4 max-h-[400px] overflow-y-auto">
|
||||||
|
<h1 className="font-bold pb-4">Refine by {`${filtersName?.group || "Theme"}`}</h1>
|
||||||
|
{groups.slice(0, seeMoreGroups ? groups.length : 5).map((group) => (
|
||||||
|
<div key={group.id}>
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id={group.id}
|
||||||
|
name="groups"
|
||||||
|
value={group.name}
|
||||||
|
></Field>
|
||||||
|
<label className="ml-1.5" htmlFor={group.id}>
|
||||||
|
{group.display_name}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{groups.length > 5 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSeeMoreGroups(!seeMoreGroups)}
|
||||||
|
type="button"
|
||||||
|
className="bg-gray-300 px-2 rounded text-gray-600 mt-2"
|
||||||
|
>
|
||||||
|
See {seeMoreGroups ? "less" : "more..."}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
<section className="bg-white rounded-lg xl:p-8 p-4 mb-4 max-h-[400px] overflow-y-auto">
|
||||||
|
<h1 className="font-bold pb-4">Refine by Keyword</h1>
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
{tags.slice(0, seeMoreTags ? tags.length : 5).map((tag) => (
|
||||||
|
<div key={tag.id}>
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
className="hidden tag-checkbox"
|
||||||
|
id={tag.id}
|
||||||
|
name="tags"
|
||||||
|
value={tag.name}
|
||||||
|
></Field>
|
||||||
|
<label
|
||||||
|
className="bg-gray-200 px-4 py-1 rounded-full text-xs block"
|
||||||
|
htmlFor={tag.id}
|
||||||
|
>
|
||||||
|
{tag.display_name}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{tags.length > 5 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSeeMoreTags(!seeMoreTags)}
|
||||||
|
type="button"
|
||||||
|
className="bg-gray-300 px-2 rounded text-gray-600 mt-2"
|
||||||
|
>
|
||||||
|
See {seeMoreTags ? "less" : "more..."}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
<AutoSubmit options={options} setOptions={setOptions} />
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
||||||
91
packages/ckan/src/components/DatasetSearchForm.tsx
Normal file
91
packages/ckan/src/components/DatasetSearchForm.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { Field, Form, Formik } from 'formik';
|
||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import {
|
||||||
|
PackageSearchOptions,
|
||||||
|
Group,
|
||||||
|
Organization,
|
||||||
|
FilterObj,
|
||||||
|
} from '../interfaces';
|
||||||
|
|
||||||
|
export default function DatasetSearchForm({
|
||||||
|
orgs,
|
||||||
|
groups,
|
||||||
|
setOptions,
|
||||||
|
options,
|
||||||
|
filtersName,
|
||||||
|
}: {
|
||||||
|
orgs: Array<Organization>;
|
||||||
|
groups: Array<Group>;
|
||||||
|
options: PackageSearchOptions;
|
||||||
|
setOptions: Dispatch<SetStateAction<PackageSearchOptions>>;
|
||||||
|
filtersName?: FilterObj | undefined;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
org: '',
|
||||||
|
group: '',
|
||||||
|
query: '',
|
||||||
|
}}
|
||||||
|
enableReinitialize={true}
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
const org = orgs.find(
|
||||||
|
(org) => (org.title || org.display_name) === values.org
|
||||||
|
);
|
||||||
|
const group = groups.find(
|
||||||
|
(group) => group.display_name === values.group
|
||||||
|
);
|
||||||
|
setOptions({
|
||||||
|
...options,
|
||||||
|
groups: group ? [group.name] : [],
|
||||||
|
orgs: org ? [org.name] : [],
|
||||||
|
query: values.query,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="mx-auto" style={{ width: 'min(1100px, 95vw)' }}>
|
||||||
|
<Form className="min-h-[80px] flex flex-col lg:flex-row bg-white inline-block px-5 py-3 rounded-xl">
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
placeholder="Search Datasets"
|
||||||
|
className="mx-4 grow py-4 border-0 placeholder:text-neutral-400"
|
||||||
|
name="query"
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
list="groups"
|
||||||
|
name="group"
|
||||||
|
placeholder={`${filtersName?.group || 'Theme'}`}
|
||||||
|
className="lg:border-l p-4 mx-2 placeholder:text-neutral-400"
|
||||||
|
></Field>
|
||||||
|
|
||||||
|
<datalist aria-label="Formats" id="groups">
|
||||||
|
<option value="">{`${filtersName?.group || 'Theme'}`}</option>
|
||||||
|
{groups.map((group, index) => (
|
||||||
|
<option key={index}>{group.display_name}</option>
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
<Field
|
||||||
|
list="orgs"
|
||||||
|
name="org"
|
||||||
|
placeholder="Organization"
|
||||||
|
className="lg:border-l p-4 mx-2 placeholder:text-neutral-400"
|
||||||
|
autoComplete="off"
|
||||||
|
></Field>
|
||||||
|
|
||||||
|
<datalist aria-label="Formats" id="orgs">
|
||||||
|
<option value="">Organization</option>
|
||||||
|
{orgs.map((org, index) => (
|
||||||
|
<option key={index}>{org.title || org.display_name}</option>
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
<button
|
||||||
|
className="font-bold text-black px-12 py-4 rounded-lg bg-accent hover:bg-cyan-500 duration-150"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
SEARCH
|
||||||
|
</button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
||||||
103
packages/ckan/src/components/ListOfDatasets.tsx
Normal file
103
packages/ckan/src/components/ListOfDatasets.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { Dispatch, SetStateAction, useState } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { SWRConfig } from 'swr';
|
||||||
|
import { PackageSearchOptions } from '../interfaces';
|
||||||
|
import DatasetCard from './DatasetCard';
|
||||||
|
import Pagination from './Pagination';
|
||||||
|
import CKAN from '../lib/ckanapi';
|
||||||
|
|
||||||
|
export default function ListOfDatasets({
|
||||||
|
ckan,
|
||||||
|
options,
|
||||||
|
setOptions,
|
||||||
|
urlPrefix = '',
|
||||||
|
}: {
|
||||||
|
ckan: CKAN;
|
||||||
|
options: PackageSearchOptions;
|
||||||
|
setOptions: Dispatch<SetStateAction<PackageSearchOptions>>;
|
||||||
|
urlPrefix?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SWRConfig>
|
||||||
|
<ListOfDatasetsInner
|
||||||
|
ckan={ckan}
|
||||||
|
options={options}
|
||||||
|
setOptions={setOptions}
|
||||||
|
urlPrefix={urlPrefix}
|
||||||
|
/>
|
||||||
|
</SWRConfig>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListOfDatasetsInner({
|
||||||
|
ckan,
|
||||||
|
options,
|
||||||
|
setOptions,
|
||||||
|
urlPrefix = '',
|
||||||
|
}: {
|
||||||
|
ckan: CKAN;
|
||||||
|
options: PackageSearchOptions;
|
||||||
|
setOptions: Dispatch<SetStateAction<PackageSearchOptions>>;
|
||||||
|
urlPrefix?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-4 homepage-padding">
|
||||||
|
<ListItems
|
||||||
|
ckan={ckan}
|
||||||
|
setOptions={setOptions}
|
||||||
|
options={options}
|
||||||
|
urlPrefix={urlPrefix}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'none' }}>
|
||||||
|
<ListItems
|
||||||
|
ckan={ckan}
|
||||||
|
setOptions={setOptions}
|
||||||
|
options={{ ...options, offset: options.offset + 5 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListItems({
|
||||||
|
ckan,
|
||||||
|
options,
|
||||||
|
setOptions,
|
||||||
|
urlPrefix = '',
|
||||||
|
}: {
|
||||||
|
ckan: CKAN;
|
||||||
|
options: PackageSearchOptions;
|
||||||
|
setOptions: Dispatch<SetStateAction<PackageSearchOptions>>;
|
||||||
|
urlPrefix?: string;
|
||||||
|
}) {
|
||||||
|
const { data } = useSWR(['package_search', options], async () =>
|
||||||
|
ckan.packageSearch({ ...options })
|
||||||
|
);
|
||||||
|
//Define which page buttons are going to be displayed in the pagination list
|
||||||
|
const [subsetOfPages, setSubsetOfPages] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2 className="text-4xl capitalize font-bold text-zinc-900">
|
||||||
|
{data?.count} Datasets
|
||||||
|
</h2>
|
||||||
|
{data?.datasets?.map((dataset) => (
|
||||||
|
<DatasetCard
|
||||||
|
key={dataset.id}
|
||||||
|
dataset={dataset}
|
||||||
|
showOrg={true}
|
||||||
|
urlPrefix={urlPrefix}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{data?.count && (
|
||||||
|
<Pagination
|
||||||
|
options={options}
|
||||||
|
subsetOfPages={subsetOfPages}
|
||||||
|
setSubsetOfPages={setSubsetOfPages}
|
||||||
|
setOptions={setOptions}
|
||||||
|
count={data.count}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
packages/ckan/src/components/Pagination.tsx
Normal file
80
packages/ckan/src/components/Pagination.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
import { PackageSearchOptions } from "../interfaces";
|
||||||
|
|
||||||
|
export default function Pagination({
|
||||||
|
options,
|
||||||
|
setOptions,
|
||||||
|
subsetOfPages,
|
||||||
|
setSubsetOfPages,
|
||||||
|
count,
|
||||||
|
}: {
|
||||||
|
options: PackageSearchOptions;
|
||||||
|
setOptions: Dispatch<SetStateAction<PackageSearchOptions>>;
|
||||||
|
subsetOfPages: number;
|
||||||
|
setSubsetOfPages: Dispatch<SetStateAction<number>>;
|
||||||
|
count: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 align-center">
|
||||||
|
{subsetOfPages !== 0 && (
|
||||||
|
<button
|
||||||
|
className="font-semibold flex items-center gap-2"
|
||||||
|
onClick={() => setSubsetOfPages(subsetOfPages - 5)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Prev
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{Array.from(Array(Math.ceil(count / 5)).keys()).map((x) => (
|
||||||
|
<button
|
||||||
|
key={x}
|
||||||
|
className={`${
|
||||||
|
x == options.offset / 5 ? "bg-orange-500 text-white" : ""
|
||||||
|
} px-2 rounded font-semibold text-zinc-900`}
|
||||||
|
onClick={() => setOptions({ ...options, offset: x * 5 })}
|
||||||
|
style={{
|
||||||
|
display:
|
||||||
|
x >= subsetOfPages && x < subsetOfPages + 5 ? "block" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{x + 1}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{subsetOfPages !== Math.ceil(count / 5) && count > 25 && (
|
||||||
|
<button
|
||||||
|
className="font-semibold flex items-center gap-2"
|
||||||
|
onClick={() => setSubsetOfPages(subsetOfPages + 5)}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
packages/ckan/src/components/ResourceCard.tsx
Normal file
47
packages/ckan/src/components/ResourceCard.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Resource } from "../interfaces";
|
||||||
|
|
||||||
|
export default function ResourceCard({
|
||||||
|
resource,
|
||||||
|
small,
|
||||||
|
}: {
|
||||||
|
resource?: Resource;
|
||||||
|
small?: boolean;
|
||||||
|
}) {
|
||||||
|
const resourceTextColors = {
|
||||||
|
PDF: "text-cyan-300",
|
||||||
|
CSV: "text-emerald-300",
|
||||||
|
JSON: "text-yellow-300",
|
||||||
|
XLS: "text-orange-300",
|
||||||
|
ODS: "text-amber-400",
|
||||||
|
DOC: "text-red-300",
|
||||||
|
SHP: "text-purple-400",
|
||||||
|
HTML: "text-pink-300",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="col-span-1 md:pt-1.5 place-content-center md:place-content-start">
|
||||||
|
<div
|
||||||
|
className="bg-slate-900 rounded-lg max-w-[90px] min-w-[60px] mx-auto md:mx-0 flex place-content-center my-auto"
|
||||||
|
style={{ minHeight: small ? "60px" : "90px" }}
|
||||||
|
>
|
||||||
|
{(resource && resource.format && (
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
resourceTextColors[
|
||||||
|
resource.format as keyof typeof resourceTextColors
|
||||||
|
]
|
||||||
|
? resourceTextColors[
|
||||||
|
resource.format as keyof typeof resourceTextColors
|
||||||
|
]
|
||||||
|
: "text-gray-200"
|
||||||
|
} font-bold ${small ? "text-lg" : "text-2xl"} my-auto`}
|
||||||
|
>
|
||||||
|
{resource.format}
|
||||||
|
</span>
|
||||||
|
)) || (
|
||||||
|
<span className="font-bold text-2xl text-gray-200 my-auto">NONE</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
packages/ckan/src/components/index.tsx
Normal file
6
packages/ckan/src/components/index.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import DatasetCard from "./DatasetCard";
|
||||||
|
import ListOfDatasets from "./ListOfDatasets";
|
||||||
|
import DatasetSearchForm from "./DatasetSearchForm";
|
||||||
|
import DatasetSearchFilters from "./DatasetSearchFilters";
|
||||||
|
|
||||||
|
export { DatasetCard, ListOfDatasets, DatasetSearchForm, DatasetSearchFilters };
|
||||||
3
packages/ckan/src/index.css
Normal file
3
packages/ckan/src/index.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
3
packages/ckan/src/index.ts
Normal file
3
packages/ckan/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./components";
|
||||||
|
export * from "./lib";
|
||||||
|
export * from "./interfaces";
|
||||||
15
packages/ckan/src/interfaces/activity.interface.ts
Normal file
15
packages/ckan/src/interfaces/activity.interface.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { User } from "./user.interface";
|
||||||
|
|
||||||
|
export interface Activity {
|
||||||
|
id: string;
|
||||||
|
timestamp: string;
|
||||||
|
user_id: string;
|
||||||
|
object_id?: string;
|
||||||
|
activity_type?: string;
|
||||||
|
user_data?: User;
|
||||||
|
data?: {
|
||||||
|
package?: {
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
80
packages/ckan/src/interfaces/dataset.interface.ts
Normal file
80
packages/ckan/src/interfaces/dataset.interface.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { Activity } from "./activity.interface";
|
||||||
|
import { Group } from "./group.interface";
|
||||||
|
import { Organization } from "./organization.interface";
|
||||||
|
|
||||||
|
export interface Dataset {
|
||||||
|
author?: string;
|
||||||
|
author_email?: string;
|
||||||
|
creator_user_id?: string;
|
||||||
|
id: string;
|
||||||
|
isopen?: boolean;
|
||||||
|
license_id?: string;
|
||||||
|
license_title?: string;
|
||||||
|
maintainer?: string;
|
||||||
|
maintainer_email?: string;
|
||||||
|
metadata_created?: string;
|
||||||
|
metadata_modified?: string;
|
||||||
|
name: string;
|
||||||
|
notes?: string;
|
||||||
|
num_resources: number;
|
||||||
|
num_tags: number;
|
||||||
|
owner_org?: string;
|
||||||
|
private?: boolean;
|
||||||
|
state?: "active" | "inactive" | "deleted";
|
||||||
|
title?: string;
|
||||||
|
type?: "dataset";
|
||||||
|
url?: string;
|
||||||
|
version?: string;
|
||||||
|
activity_stream?: Array<Activity>;
|
||||||
|
resources: Array<Resource>;
|
||||||
|
organization?: Organization;
|
||||||
|
groups?: Array<Group>;
|
||||||
|
tags?: Array<Tag>;
|
||||||
|
total_downloads?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Resource {
|
||||||
|
cache_last_updated?: string;
|
||||||
|
cache_url?: string;
|
||||||
|
created?: string;
|
||||||
|
datastore_active?: boolean;
|
||||||
|
description?: string;
|
||||||
|
format?: string;
|
||||||
|
hash?: string;
|
||||||
|
id?: string;
|
||||||
|
last_modified?: string;
|
||||||
|
metadata_modified?: string;
|
||||||
|
mimetype?: string;
|
||||||
|
mimetype_inner?: string;
|
||||||
|
name?: string;
|
||||||
|
package_id?: string;
|
||||||
|
position?: number;
|
||||||
|
resource_type?: null;
|
||||||
|
size?: number;
|
||||||
|
state?: "active" | "inactive" | "deleted";
|
||||||
|
url?: string;
|
||||||
|
url_type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DatasetListQueryOptions {
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
export interface PackageSearchOptions {
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
groups: Array<string>;
|
||||||
|
orgs: Array<string>;
|
||||||
|
tags: Array<string>;
|
||||||
|
query?: string;
|
||||||
|
resFormat?: Array<string>;
|
||||||
|
sort?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Tag {
|
||||||
|
display_name?: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
state: "active";
|
||||||
|
vocabulary_id?: string;
|
||||||
|
}
|
||||||
12
packages/ckan/src/interfaces/datastore.interface.ts
Normal file
12
packages/ckan/src/interfaces/datastore.interface.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export interface TableMetadata {
|
||||||
|
_id: string;
|
||||||
|
name?: string;
|
||||||
|
oid: number;
|
||||||
|
alias_of?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceInfo {
|
||||||
|
schema: Record<string, string | boolean | number>;
|
||||||
|
meta: Record<string, string | boolean | number>;
|
||||||
|
alias?: string;
|
||||||
|
}
|
||||||
25
packages/ckan/src/interfaces/group.interface.ts
Normal file
25
packages/ckan/src/interfaces/group.interface.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Activity } from "./activity.interface";
|
||||||
|
import { Dataset, Tag } from "./dataset.interface";
|
||||||
|
import { User } from "./user.interface";
|
||||||
|
|
||||||
|
export interface Group {
|
||||||
|
display_name: string;
|
||||||
|
description: string;
|
||||||
|
image_display_url: string;
|
||||||
|
package_count: number;
|
||||||
|
created: string;
|
||||||
|
name: string;
|
||||||
|
is_organization: false;
|
||||||
|
state: "active" | "deleted" | "inactive";
|
||||||
|
image_url: string;
|
||||||
|
type: "group";
|
||||||
|
title: string;
|
||||||
|
revision_id: string;
|
||||||
|
num_followers: number;
|
||||||
|
id: string;
|
||||||
|
approval_status: string;
|
||||||
|
packages?: Array<Dataset>;
|
||||||
|
activity_stream?: Array<Activity>;
|
||||||
|
tags?: Array<Tag>;
|
||||||
|
users?: Array<User>;
|
||||||
|
}
|
||||||
7
packages/ckan/src/interfaces/index.tsx
Normal file
7
packages/ckan/src/interfaces/index.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export * from './activity.interface'
|
||||||
|
export * from './dataset.interface'
|
||||||
|
export * from './datastore.interface'
|
||||||
|
export * from './group.interface'
|
||||||
|
export * from './organization.interface'
|
||||||
|
export * from './user.interface'
|
||||||
|
export * from './misc.interface'
|
||||||
5
packages/ckan/src/interfaces/misc.interface.ts
Normal file
5
packages/ckan/src/interfaces/misc.interface.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface FilterObj {
|
||||||
|
org?: string;
|
||||||
|
group?: string;
|
||||||
|
format?: string;
|
||||||
|
}
|
||||||
22
packages/ckan/src/interfaces/organization.interface.ts
Normal file
22
packages/ckan/src/interfaces/organization.interface.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Activity } from "./activity.interface";
|
||||||
|
import { Dataset, Tag } from "./dataset.interface";
|
||||||
|
import { User } from "./user.interface";
|
||||||
|
|
||||||
|
export interface Organization {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
display_name: string;
|
||||||
|
type: string;
|
||||||
|
description?: string;
|
||||||
|
image_url?: string;
|
||||||
|
image_display_url?: string;
|
||||||
|
created?: string;
|
||||||
|
is_organization: boolean;
|
||||||
|
approval_status?: "approved";
|
||||||
|
state: "active";
|
||||||
|
packages?: Array<Dataset>;
|
||||||
|
activity_stream?: Array<Activity>;
|
||||||
|
users?: Array<User>;
|
||||||
|
tags?: Array<Tag>;
|
||||||
|
}
|
||||||
17
packages/ckan/src/interfaces/user.interface.ts
Normal file
17
packages/ckan/src/interfaces/user.interface.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export interface User {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
fullname?: string;
|
||||||
|
created?: string;
|
||||||
|
about?: null;
|
||||||
|
activity_streams_email_notifications?: boolean;
|
||||||
|
sysadmin?: boolean;
|
||||||
|
state?: "active" | "inactive" | "deleted";
|
||||||
|
image_url?: string;
|
||||||
|
display_name?: string;
|
||||||
|
email_hash?: string;
|
||||||
|
number_created_packages?: number;
|
||||||
|
apikey?: string;
|
||||||
|
email?: string;
|
||||||
|
image_display_url?: string;
|
||||||
|
}
|
||||||
389
packages/ckan/src/lib/ckanapi.tsx
Normal file
389
packages/ckan/src/lib/ckanapi.tsx
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import { Activity } from '../interfaces/activity.interface';
|
||||||
|
import {
|
||||||
|
Dataset,
|
||||||
|
DatasetListQueryOptions,
|
||||||
|
PackageSearchOptions,
|
||||||
|
Resource,
|
||||||
|
Tag,
|
||||||
|
} from '../interfaces/dataset.interface';
|
||||||
|
import { ResourceInfo, TableMetadata } from '../interfaces/datastore.interface';
|
||||||
|
import { Group } from '../interfaces/group.interface';
|
||||||
|
import { Organization } from '../interfaces/organization.interface';
|
||||||
|
import { User } from '../interfaces/user.interface';
|
||||||
|
import fetchRetry from './utils';
|
||||||
|
|
||||||
|
export default class CKAN {
|
||||||
|
DMS: string;
|
||||||
|
|
||||||
|
constructor(DMS: string) {
|
||||||
|
this.DMS = DMS;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDatasetsList() {
|
||||||
|
const response = await fetchRetry(
|
||||||
|
`${this.DMS}/api/3/action/package_list`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
return responseData.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDatasetsListWithDetails(options: DatasetListQueryOptions) {
|
||||||
|
const response = await fetchRetry(
|
||||||
|
`${
|
||||||
|
this.DMS
|
||||||
|
}/api/3/action/current_package_list_with_resources?offset=${
|
||||||
|
options.offset
|
||||||
|
}&limit=${options.limit}`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
const datasets: Array<Dataset> = responseData.result;
|
||||||
|
return datasets;
|
||||||
|
}
|
||||||
|
|
||||||
|
async packageSearch(
|
||||||
|
options: PackageSearchOptions
|
||||||
|
): Promise<{ datasets: Dataset[]; count: number }> {
|
||||||
|
function buildGroupsQuery(groups: Array<string>) {
|
||||||
|
if (groups.length > 0) {
|
||||||
|
return `groups:(${groups.join(' OR ')})`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
function buildOrgsQuery(orgs: Array<string>) {
|
||||||
|
if (orgs.length > 0) {
|
||||||
|
return `organization:(${orgs.join(' OR ')})`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
function buildTagsQuery(tags: Array<string>) {
|
||||||
|
if (tags.length > 0) {
|
||||||
|
return `tags:(${tags.join(' OR ')})`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildResFormatQuery(resFormat: Array<string>) {
|
||||||
|
if (resFormat?.length > 0) {
|
||||||
|
return `res_format:(${resFormat.join(' OR ')})`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFq(
|
||||||
|
tags: Array<string>,
|
||||||
|
orgs: Array<string>,
|
||||||
|
groups: Array<string>,
|
||||||
|
resFormat: Array<string>
|
||||||
|
) {
|
||||||
|
//TODO; this query builder is not very robust
|
||||||
|
// convertToCkanSearchQuery function should be
|
||||||
|
//copied over from the old portals utils
|
||||||
|
const fq = [
|
||||||
|
buildGroupsQuery(groups),
|
||||||
|
buildOrgsQuery(orgs),
|
||||||
|
buildTagsQuery(tags),
|
||||||
|
buildResFormatQuery(resFormat),
|
||||||
|
].filter((str) => str !== '');
|
||||||
|
if (fq.length > 0) {
|
||||||
|
return '&fq=' + fq.join('+');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fq = buildFq(
|
||||||
|
options.tags,
|
||||||
|
options.orgs,
|
||||||
|
options.groups,
|
||||||
|
options?.resFormat
|
||||||
|
);
|
||||||
|
const response = await fetchRetry(
|
||||||
|
`${
|
||||||
|
this.DMS
|
||||||
|
}/api/3/action/package_search?start=${options.offset}&rows=${
|
||||||
|
options.limit
|
||||||
|
}${fq ? fq : ''}${options.query ? '&q=' + options.query : ''}${
|
||||||
|
options.sort ? '&sort=' + options.sort : ''
|
||||||
|
}`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
const datasets: Array<Dataset> = responseData.result.results;
|
||||||
|
return { datasets, count: responseData.result.count };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDatasetDetails(datasetName: string) {
|
||||||
|
const response = await fetchRetry(
|
||||||
|
`${
|
||||||
|
this.DMS
|
||||||
|
}/api/3/action/package_show?id=${datasetName}`,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
if (responseData.success === false) {
|
||||||
|
throw 'Could not find dataset';
|
||||||
|
}
|
||||||
|
const dataset: Dataset = responseData.result;
|
||||||
|
return dataset;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDatasetActivityStream(datasetName: string) {
|
||||||
|
const response = await fetchRetry(
|
||||||
|
`${
|
||||||
|
this.DMS
|
||||||
|
}/api/3/action/package_activity_list?id=${datasetName}`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
const activitiesWithoutUserData: Array<Activity> = responseData.result;
|
||||||
|
const activities = await Promise.all(
|
||||||
|
activitiesWithoutUserData.map(async (item) => {
|
||||||
|
let user_data: User | null = await this.getUser(item.user_id);
|
||||||
|
user_data = user_data === undefined ? null : user_data;
|
||||||
|
return { ...item, user_data };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return activities;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser(userId: string) {
|
||||||
|
try {
|
||||||
|
const response = await fetchRetry(
|
||||||
|
`${
|
||||||
|
this.DMS
|
||||||
|
}/api/3/action/user_show?id=${userId}`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
const user: User | null =
|
||||||
|
responseData.success === true ? responseData.result : null;
|
||||||
|
return user;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGroupList() {
|
||||||
|
const response = await fetchRetry(
|
||||||
|
`${this.DMS}/api/3/action/group_list`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
const groups: Array<string> = responseData.result;
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGroupsWithDetails() {
|
||||||
|
const response = await fetchRetry(
|
||||||
|
`${
|
||||||
|
this.DMS
|
||||||
|
}/api/3/action/group_list?all_fields=True`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
const groups: Array<Group> = responseData.result;
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGroupDetails(groupName: string) {
|
||||||
|
const response = await fetchRetry(
|
||||||
|
`${
|
||||||
|
this.DMS
|
||||||
|
}/api/3/action/group_show?id=${groupName}&include_datasets=True`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
const group: Group = responseData.result;
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGroupActivityStream(groupName: string) {
|
||||||
|
const response = await fetchRetry(
|
||||||
|
`${
|
||||||
|
this.DMS
|
||||||
|
}/api/3/action/group_activity_list?id=${groupName}`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
const activitiesWithoutUserData: Array<Activity> = responseData.result;
|
||||||
|
const activities = await Promise.all(
|
||||||
|
activitiesWithoutUserData.map(async (item) => {
|
||||||
|
const user_data = await this.getUser(item.user_id);
|
||||||
|
return { ...item, user_data };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return activities;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrgList() {
|
||||||
|
const response = await fetchRetry(
|
||||||
|
`${this.DMS}/api/3/action/organization_list`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
const organizations: Array<string> = responseData.result;
|
||||||
|
return organizations;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrgsWithDetails(accrossPages?: boolean) {
|
||||||
|
if (!accrossPages) {
|
||||||
|
const response = await fetchRetry(
|
||||||
|
`${
|
||||||
|
this.DMS
|
||||||
|
}/api/3/action/organization_list?all_fields=True`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
const organizations: Array<Organization> = responseData.result;
|
||||||
|
return organizations;
|
||||||
|
}
|
||||||
|
|
||||||
|
const organizations = [];
|
||||||
|
const orgListResponse = await fetchRetry(
|
||||||
|
`${this.DMS}/api/3/action/organization_list`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const orgList = await orgListResponse.json();
|
||||||
|
const orgLen = orgList.result.length;
|
||||||
|
const pages = Math.ceil(orgLen / 25);
|
||||||
|
|
||||||
|
for (let i = 0; i < pages; i++) {
|
||||||
|
let allOrgListResponse = await fetchRetry(
|
||||||
|
`${
|
||||||
|
this.DMS
|
||||||
|
}/api/3/action/organization_list?all_fields=True&offset=${
|
||||||
|
i * 25
|
||||||
|
}&limit=25`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const responseData = await allOrgListResponse.json();
|
||||||
|
const result: Array<Organization> = responseData.result;
|
||||||
|
organizations.push(...result);
|
||||||
|
}
|
||||||
|
return organizations.sort((a, b) => b.package_count - a.package_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrgDetails(orgName: string) {
|
||||||
|
const response = await fetchRetry(
|
||||||
|
`${
|
||||||
|
this.DMS
|
||||||
|
}/api/3/action/organization_show?id=${orgName}&include_datasets=True`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
const organization: Organization = responseData.result;
|
||||||
|
return organization;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrgActivityStream(orgName: string) {
|
||||||
|
const response = await fetchRetry(
|
||||||
|
`${
|
||||||
|
this.DMS
|
||||||
|
}/api/3/action/organization_activity_list?id=${orgName}`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
const activitiesWithoutUserData: Array<Activity> = responseData.result;
|
||||||
|
const activities = await Promise.all(
|
||||||
|
activitiesWithoutUserData.map(async (item) => {
|
||||||
|
const user_data = await this.getUser(item.user_id);
|
||||||
|
return { ...item, user_data };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return activities;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllTags() {
|
||||||
|
const response = await fetchRetry(
|
||||||
|
`${
|
||||||
|
this.DMS
|
||||||
|
}/api/3/action/tag_list?all_fields=True`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
const tags: Array<Tag> = responseData.result;
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getResourcesWithAliasList() {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.DMS}/api/3/action/datastore_search`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: '_table_metadata',
|
||||||
|
limit: '32000',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
const tableMetadata: Array<TableMetadata> = responseData.result.records;
|
||||||
|
return tableMetadata.filter((item) => item.alias_of);
|
||||||
|
}
|
||||||
|
|
||||||
|
async datastoreSearch(resourceId: string) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.DMS}/api/3/action/datastore_search`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: resourceId,
|
||||||
|
limit: '32000',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
return responseData.result.records;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getResourceMetadata(resourceId: string) {
|
||||||
|
const response = await fetchRetry(
|
||||||
|
`${
|
||||||
|
this.DMS
|
||||||
|
}/api/3/action/resource_show?id=${resourceId}`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
const resourceMetadata: Resource = responseData.result;
|
||||||
|
return resourceMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getResourceInfo(resourceId: string) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.DMS}/api/3/action/datastore_info`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ resource_id: resourceId }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
const resourceInfo: Array<ResourceInfo> = responseData.result;
|
||||||
|
return resourceInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFacetFields(field: 'res_format' | 'tags') {
|
||||||
|
const response = await fetchRetry(
|
||||||
|
`${
|
||||||
|
this.DMS
|
||||||
|
}/api/3/action/package_search?facet.field=["${field}"]&rows=0`,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const responseData = await response.json();
|
||||||
|
const result = responseData.result?.facets?.[field];
|
||||||
|
return Object.keys(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/ckan/src/lib/index.tsx
Normal file
3
packages/ckan/src/lib/index.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import CKAN from './ckanapi'
|
||||||
|
export { CKAN }
|
||||||
|
export * from './utils'
|
||||||
54
packages/ckan/src/lib/utils.ts
Normal file
54
packages/ckan/src/lib/utils.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
export function getDaysAgo(date: string) {
|
||||||
|
const today = new Date();
|
||||||
|
const createdOn = new Date(date);
|
||||||
|
const msInDay = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
createdOn.setHours(0, 0, 0, 0);
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
return (+today - +createdOn) / msInDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function fetchRetry(url: string, n: number): Promise<any> {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
const id = setTimeout(() => abortController.abort(), 30000);
|
||||||
|
const res = await fetch(url, { signal: abortController.signal });
|
||||||
|
clearTimeout(id);
|
||||||
|
if (!res.ok && n && n > 0) {
|
||||||
|
return await fetchRetry(url, n - 1);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeTag(tag?: string) {
|
||||||
|
if (tag === "{{description}}" || !tag) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.innerHTML = tag;
|
||||||
|
return div.textContent || div.innerText || "";
|
||||||
|
}
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
//The porpuse of this functoin is converting the schema returned from datastore info which is in the form o a dictionary into an array of key value pairs that can be consumed by the data-explorer
|
||||||
|
export function convertFieldSchema(
|
||||||
|
schema: Record<string, string | boolean | number>
|
||||||
|
) {
|
||||||
|
function convertToGraphqlString(fieldName: string) {
|
||||||
|
return fieldName
|
||||||
|
.replaceAll(" ", "_")
|
||||||
|
.replaceAll("(", "_")
|
||||||
|
.replaceAll(")", "_")
|
||||||
|
.replace(/[^\w\s]|(_)\1/gi, "_");
|
||||||
|
}
|
||||||
|
const entries = Object.entries(schema);
|
||||||
|
return {
|
||||||
|
fields: entries.map((entry) => ({
|
||||||
|
name: convertToGraphqlString(entry[0]),
|
||||||
|
title: entry[0],
|
||||||
|
type: entry[1],
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
1
packages/ckan/src/vite-env.d.ts
vendored
Normal file
1
packages/ckan/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
8
packages/ckan/tailwind.config.js
Normal file
8
packages/ckan/tailwind.config.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
28
packages/ckan/tsconfig.json
Normal file
28
packages/ckan/tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
// "strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }],
|
||||||
|
}
|
||||||
10
packages/ckan/tsconfig.node.json
Normal file
10
packages/ckan/tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
30
packages/ckan/vite.config.ts
Normal file
30
packages/ckan/vite.config.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import dts from 'vite-plugin-dts';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
dts({
|
||||||
|
insertTypesEntry: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: path.resolve(__dirname, 'src/index.ts'),
|
||||||
|
name: 'components',
|
||||||
|
formats: ['es', 'umd'],
|
||||||
|
fileName: (format) => `components.${format}.js`,
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['react', 'react-dom', 'styled-components'],
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
react: 'React',
|
||||||
|
'react-dom': 'ReactDOM'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user