Compare commits
38 Commits
refactor_s
...
alan-turin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a04d8e4c0 | ||
|
|
014c4c043d | ||
|
|
ed3a26cd6d | ||
|
|
026059184a | ||
|
|
a041d69282 | ||
|
|
14abd5b768 | ||
|
|
4aaabba229 | ||
|
|
cc43597130 | ||
|
|
d9a6ea4ef1 | ||
|
|
f6b94ee254 | ||
|
|
04b05c0896 | ||
|
|
5b4d2d1990 | ||
|
|
b7e2e8e6b8 | ||
|
|
b6100546e3 | ||
|
|
58ca032d3f | ||
|
|
4b5329a93e | ||
|
|
298b59d291 | ||
|
|
41e7f8ad8d | ||
|
|
e354009e79 | ||
|
|
ad209c8f21 | ||
|
|
b49abb3b39 | ||
|
|
6d04e2d8c3 | ||
|
|
8038662160 | ||
|
|
5a70118545 | ||
|
|
8743f0d572 | ||
|
|
48908b0842 | ||
|
|
74a4f9a8ed | ||
|
|
907015461a | ||
|
|
7450302440 | ||
|
|
926ae16c35 | ||
|
|
63ab0c4d3c | ||
|
|
a31b2e8fa3 | ||
|
|
5305cc4c2f | ||
|
|
e8bf4daf5f | ||
|
|
267267ac11 | ||
|
|
1770deb960 | ||
|
|
7002b5669c | ||
|
|
bfc124473d |
4
.gitignore
vendored
@@ -44,3 +44,7 @@ Thumbs.db
|
|||||||
# Env
|
# Env
|
||||||
.env
|
.env
|
||||||
**/.env
|
**/.env
|
||||||
|
|
||||||
|
# MarkdownDB
|
||||||
|
*.db
|
||||||
|
**/*.db
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ https://portaljs.org/docs
|
|||||||
|
|
||||||
# Community
|
# Community
|
||||||
|
|
||||||
If you have questions about anything related to Portal.JS, you're always welcome to ask our community on [GitHub Discussions](https://github.com/datopian/portal.js/discussions) or on our [Discord server](https://discord.gg/An7Bu5x8).
|
If you have questions about anything related to Portal.JS, you're always welcome to ask our community on [GitHub Discussions](https://github.com/datopian/portal.js/discussions) or on our [Discord server](https://discord.gg/EeyfGrGu4U).
|
||||||
|
|
||||||
# Appendix
|
# Appendix
|
||||||
|
|
||||||
|
|||||||
3
examples/alan-turing-portal/.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
||||||
35
examples/alan-turing-portal/.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*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# generated files
|
||||||
|
/public/rss/
|
||||||
129
examples/alan-turing-portal/LICENSE.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Tailwind UI License
|
||||||
|
|
||||||
|
## Personal License
|
||||||
|
|
||||||
|
Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates.
|
||||||
|
|
||||||
|
The license grants permission to **one individual** (the Licensee) to access and use the Components and Templates.
|
||||||
|
|
||||||
|
You **can**:
|
||||||
|
|
||||||
|
- Use the Components and Templates to create unlimited End Products.
|
||||||
|
- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license.
|
||||||
|
- Use the Components and Templates to create unlimited End Products for unlimited Clients.
|
||||||
|
- Use the Components and Templates to create End Products where the End Product is sold to End Users.
|
||||||
|
- Use the Components and Templates to create End Products that are open source and freely available to End Users.
|
||||||
|
|
||||||
|
You **cannot**:
|
||||||
|
|
||||||
|
- Use the Components and Templates to create End Products that are designed to allow an End User to build their own End Products using the Components and Templates or derivatives of the Components and Templates.
|
||||||
|
- Re-distribute the Components and Templates or derivatives of the Components and Templates separately from an End Product, neither in code or as design assets.
|
||||||
|
- Share your access to the Components and Templates with any other individuals.
|
||||||
|
- Use the Components and Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc.
|
||||||
|
|
||||||
|
### Example usage
|
||||||
|
|
||||||
|
Examples of usage **allowed** by the license:
|
||||||
|
|
||||||
|
- Creating a personal website by yourself.
|
||||||
|
- Creating a website or web application for a client that will be owned by that client.
|
||||||
|
- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application.
|
||||||
|
- Creating a commercial self-hosted web application that is sold to end users for a one-time fee.
|
||||||
|
- Creating a web application where the primary purpose is clearly not to simply re-distribute the components (like a conference organization app that uses the components for its UI for example) that is free and open source, where the source code is publicly available.
|
||||||
|
|
||||||
|
Examples of usage **not allowed** by the license:
|
||||||
|
|
||||||
|
- Creating a repository of your favorite Tailwind UI components or templates (or derivatives based on Tailwind UI components or templates) and publishing it publicly.
|
||||||
|
- Creating a React or Vue version of Tailwind UI and making it available either for sale or for free.
|
||||||
|
- Create a Figma or Sketch UI kit based on the Tailwind UI component designs.
|
||||||
|
- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind UI.
|
||||||
|
- Creating a theme, template, or project starter kit using the components or templates and making it available either for sale or for free.
|
||||||
|
- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free.
|
||||||
|
|
||||||
|
In simple terms, use Tailwind UI for anything you like as long as it doesn't compete with Tailwind UI.
|
||||||
|
|
||||||
|
### Personal License Definitions
|
||||||
|
|
||||||
|
Licensee is the individual who has purchased a Personal License.
|
||||||
|
|
||||||
|
Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind UI license.
|
||||||
|
|
||||||
|
End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates.
|
||||||
|
|
||||||
|
End User is a user of an End Product.
|
||||||
|
|
||||||
|
Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document.
|
||||||
|
|
||||||
|
## Team License
|
||||||
|
|
||||||
|
Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates.
|
||||||
|
|
||||||
|
The license grants permission for **up to 25 Employees and Contractors of the Licensee** to access and use the Components and Templates.
|
||||||
|
|
||||||
|
You **can**:
|
||||||
|
|
||||||
|
- Use the Components and Templates to create unlimited End Products.
|
||||||
|
- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license.
|
||||||
|
- Use the Components and Templates to create unlimited End Products for unlimited Clients.
|
||||||
|
- Use the Components and Templates to create End Products where the End Product is sold to End Users.
|
||||||
|
- Use the Components and Templates to create End Products that are open source and freely available to End Users.
|
||||||
|
|
||||||
|
You **cannot**:
|
||||||
|
|
||||||
|
- Use the Components or Templates to create End Products that are designed to allow an End User to build their own End Products using the Components or Templates or derivatives of the Components or Templates.
|
||||||
|
- Re-distribute the Components or Templates or derivatives of the Components or Templates separately from an End Product.
|
||||||
|
- Use the Components or Templates to create End Products that are the property of any individual or entity other than the Licensee or Clients of the Licensee.
|
||||||
|
- Use the Components or Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc.
|
||||||
|
|
||||||
|
### Example usage
|
||||||
|
|
||||||
|
Examples of usage **allowed** by the license:
|
||||||
|
|
||||||
|
- Creating a website for your company.
|
||||||
|
- Creating a website or web application for a client that will be owned by that client.
|
||||||
|
- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application.
|
||||||
|
- Creating a commercial self-hosted web application that is sold to end users for a one-time fee.
|
||||||
|
- Creating a web application where the primary purpose is clearly not to simply re-distribute the components or templates (like a conference organization app that uses the components or a template for its UI for example) that is free and open source, where the source code is publicly available.
|
||||||
|
|
||||||
|
Examples of use **not allowed** by the license:
|
||||||
|
|
||||||
|
- Creating a repository of your favorite Tailwind UI components or template (or derivatives based on Tailwind UI components or templates) and publishing it publicly.
|
||||||
|
- Creating a React or Vue version of Tailwind UI and making it available either for sale or for free.
|
||||||
|
- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind UI.
|
||||||
|
- Creating a theme or template using the components or templates and making it available either for sale or for free.
|
||||||
|
- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free.
|
||||||
|
- Creating any End Product that is not the sole property of either your company or a client of your company. For example your employees/contractors can't use your company Tailwind UI license to build their own websites or side projects.
|
||||||
|
|
||||||
|
### Team License Definitions
|
||||||
|
|
||||||
|
Licensee is the business entity who has purchased a Team License.
|
||||||
|
|
||||||
|
Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind UI license.
|
||||||
|
|
||||||
|
End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates.
|
||||||
|
|
||||||
|
End User is a user of an End Product.
|
||||||
|
|
||||||
|
Employee is a full-time or part-time employee of the Licensee.
|
||||||
|
|
||||||
|
Contractor is an individual or business entity contracted to perform services for the Licensee.
|
||||||
|
|
||||||
|
Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
If you are found to be in violation of the license, access to your Tailwind UI account will be terminated, and a refund may be issued at our discretion. When license violation is blatant and malicious (such as intentionally redistributing the Components or Templates through private warez channels), no refund will be issued.
|
||||||
|
|
||||||
|
The copyright of the Components and Templates is owned by Tailwind Labs Inc. You are granted only the permissions described in this license; all other rights are reserved. Tailwind Labs Inc. reserves the right to pursue legal remedies for any unauthorized use of the Components or Templates outside the scope of this license.
|
||||||
|
|
||||||
|
## Liability
|
||||||
|
|
||||||
|
Tailwind Labs Inc.’s liability to you for costs, damages, or other losses arising from your use of the Components or Templates — including third-party claims against you — is limited to a refund of your license fee. Tailwind Labs Inc. may not be held liable for any consequential damages related to your use of the Components or Templates.
|
||||||
|
|
||||||
|
This Agreement is governed by the laws of the Province of Ontario and the applicable laws of Canada. Legal proceedings related to this Agreement may only be brought in the courts of Ontario. You agree to service of process at the e-mail address on your original order.
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
Unsure which license you need, or unsure if your use case is covered by our licenses?
|
||||||
|
|
||||||
|
Email us at [support@tailwindui.com](mailto:support@tailwindui.com) with your questions.
|
||||||
27
examples/alan-turing-portal/README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
## Intro
|
||||||
|
|
||||||
|
This page catalogues datasets annotated for hate speech, online abuse, and offensive language. They may be useful for e.g. training a natural language processing system to detect this language.
|
||||||
|
|
||||||
|
Its built on top of [PortalJS](https://portaljs.org/), it allows you to publish datasets, lists of offensive keywords and static pages, all of those are stored as markdown files inside the `content` folder.
|
||||||
|
|
||||||
|
- .md files inside `content/datasets/` will appear on the dataset list section of the homepage and be searchable as well as having a individual page in `datasets/<file name>`
|
||||||
|
- .md files inside `content/keywords/` will appear on the list of offensive keywords section of the homepage as well as having a individual page in `keywords/<file name>`
|
||||||
|
- .md files inside `content/` will be converted to static pages in the url `/<file name>` eg: `content/about.md` becomes `/about`
|
||||||
|
|
||||||
|
This is also a Next.JS project so you can use the following steps to run the website locally.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
To get started first install the npm dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website.
|
||||||
94
examples/alan-turing-portal/components/Card.jsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
function ChevronRightIcon(props) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
|
||||||
|
<path
|
||||||
|
d="M6.75 5.75 9.25 8l-2.5 2.25"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Card({ as: Component = 'div', className, children }) {
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
className={clsx(className, 'group relative flex flex-col items-start')}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Component>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Card.Link = function CardLink({ children, ...props }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
|
||||||
|
<Link {...props}>
|
||||||
|
<span className="absolute -inset-x-4 -inset-y-6 z-20 sm:-inset-x-6 sm:rounded-2xl" />
|
||||||
|
<span className="relative z-10">{children}</span>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Card.Title = function CardTitle({ as: Component = 'h2', href, children }) {
|
||||||
|
return (
|
||||||
|
<Component className="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
|
||||||
|
{href ? <Card.Link href={href}>{children}</Card.Link> : children}
|
||||||
|
</Component>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Card.Description = function CardDescription({ children }) {
|
||||||
|
return (
|
||||||
|
<p className="z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Card.Cta = function CardCta({ children }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="relative z-10 mt-4 flex items-center text-sm font-medium text-teal-500"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-1 h-4 w-4 stroke-current" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Card.Eyebrow = function CardEyebrow({
|
||||||
|
as: Component = 'p',
|
||||||
|
decorate = false,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
className={clsx(
|
||||||
|
className,
|
||||||
|
'relative z-10 order-first mb-3 flex items-center text-sm text-zinc-400 dark:text-zinc-500',
|
||||||
|
decorate && 'pl-3.5'
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{decorate && (
|
||||||
|
<span
|
||||||
|
className="absolute inset-y-0 left-0 flex items-center"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<span className="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</Component>
|
||||||
|
)
|
||||||
|
}
|
||||||
42
examples/alan-turing-portal/components/Container.jsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { forwardRef } from 'react'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
const OuterContainer = forwardRef(function OuterContainer(
|
||||||
|
{ className, children, ...props },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={clsx('sm:px-8', className)} {...props}>
|
||||||
|
<div className="mx-auto max-w-7xl lg:px-8">{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const InnerContainer = forwardRef(function InnerContainer(
|
||||||
|
{ className, children, ...props },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={clsx('relative px-4 sm:px-8 lg:px-12', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="mx-auto max-w-2xl lg:max-w-5xl">{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Container = forwardRef(function Container(
|
||||||
|
{ children, ...props },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<OuterContainer ref={ref} {...props}>
|
||||||
|
<InnerContainer>{children}</InnerContainer>
|
||||||
|
</OuterContainer>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
Container.Outer = OuterContainer
|
||||||
|
Container.Inner = InnerContainer
|
||||||
36
examples/alan-turing-portal/components/Footer.jsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
import { Container } from '../components/Container'
|
||||||
|
|
||||||
|
function NavLink({ href, children }) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className="transition hover:text-teal-500 dark:hover:text-teal-400"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="mt-32">
|
||||||
|
<Container.Outer>
|
||||||
|
<div className="border-t border-zinc-100 pb-16 pt-10 dark:border-zinc-700/40">
|
||||||
|
<Container.Inner>
|
||||||
|
<div className="flex flex-col items-center justify-between gap-6 sm:flex-row">
|
||||||
|
<p className="text-sm font-medium text-zinc-800 dark:text-zinc-200">
|
||||||
|
Built with <a href='https://portaljs.org'>PortalJS 🌀</a>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-zinc-400 dark:text-zinc-500">
|
||||||
|
© {new Date().getFullYear()} Leon Derczynski. All rights
|
||||||
|
reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Container.Inner>
|
||||||
|
</div>
|
||||||
|
</Container.Outer>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
109
examples/alan-turing-portal/components/Header.jsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { useRef } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
import { Container } from '../components/Container'
|
||||||
|
|
||||||
|
function SunIcon(props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M8 12.25A4.25 4.25 0 0 1 12.25 8v0a4.25 4.25 0 0 1 4.25 4.25v0a4.25 4.25 0 0 1-4.25 4.25v0A4.25 4.25 0 0 1 8 12.25v0Z" />
|
||||||
|
<path
|
||||||
|
d="M12.25 3v1.5M21.5 12.25H20M18.791 18.791l-1.06-1.06M18.791 5.709l-1.06 1.06M12.25 20v1.5M4.5 12.25H3M6.77 6.77 5.709 5.709M6.77 17.73l-1.061 1.061"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MoonIcon(props) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
|
||||||
|
<path
|
||||||
|
d="M17.25 16.22a6.937 6.937 0 0 1-9.47-9.47 7.451 7.451 0 1 0 9.47 9.47ZM12.75 7C17 7 17 2.75 17 2.75S17 7 21.25 7C17 7 17 11.25 17 11.25S17 7 12.75 7Z"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModeToggle() {
|
||||||
|
function disableTransitionsTemporarily() {
|
||||||
|
document.documentElement.classList.add('[&_*]:!transition-none')
|
||||||
|
window.setTimeout(() => {
|
||||||
|
document.documentElement.classList.remove('[&_*]:!transition-none')
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMode() {
|
||||||
|
disableTransitionsTemporarily()
|
||||||
|
|
||||||
|
let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
let isSystemDarkMode = darkModeMediaQuery.matches
|
||||||
|
let isDarkMode = document.documentElement.classList.toggle('dark')
|
||||||
|
|
||||||
|
if (isDarkMode === isSystemDarkMode) {
|
||||||
|
delete window.localStorage.isDarkMode
|
||||||
|
} else {
|
||||||
|
window.localStorage.isDarkMode = isDarkMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Toggle dark mode"
|
||||||
|
className="group rounded-full bg-white/90 px-3 py-2 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur transition dark:bg-zinc-800/90 dark:ring-white/10 dark:hover:ring-white/20"
|
||||||
|
onClick={toggleMode}
|
||||||
|
>
|
||||||
|
<SunIcon className="h-6 w-6 fill-zinc-100 stroke-zinc-500 transition group-hover:fill-zinc-200 group-hover:stroke-zinc-700 dark:hidden [@media(prefers-color-scheme:dark)]:fill-teal-50 [@media(prefers-color-scheme:dark)]:stroke-teal-500 [@media(prefers-color-scheme:dark)]:group-hover:fill-teal-50 [@media(prefers-color-scheme:dark)]:group-hover:stroke-teal-600" />
|
||||||
|
<MoonIcon className="hidden h-6 w-6 fill-zinc-700 stroke-zinc-500 transition dark:block [@media(prefers-color-scheme:dark)]:group-hover:stroke-zinc-400 [@media_not_(prefers-color-scheme:dark)]:fill-teal-400/10 [@media_not_(prefers-color-scheme:dark)]:stroke-teal-500" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
let isHomePage = useRouter().pathname === '/'
|
||||||
|
|
||||||
|
let headerRef = useRef()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header
|
||||||
|
className="pointer-events-none relative z-50 flex flex-col"
|
||||||
|
style={{
|
||||||
|
height: 'var(--header-height)',
|
||||||
|
marginBottom: 'var(--header-mb)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={headerRef}
|
||||||
|
className="top-0 z-10 h-16 pt-6"
|
||||||
|
style={{ position: 'var(--header-position)' }}
|
||||||
|
>
|
||||||
|
<Container
|
||||||
|
className="top-[var(--header-top,theme(spacing.6))] w-full"
|
||||||
|
style={{ position: 'var(--header-inner-position)' }}
|
||||||
|
>
|
||||||
|
<div className="relative flex gap-4">
|
||||||
|
<div className="flex justify-end md:flex-1">
|
||||||
|
<div className="pointer-events-auto">
|
||||||
|
<ModeToggle />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{isHomePage && <div style={{ height: 'var(--content-offset)' }} />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
5
examples/alan-turing-portal/content/about.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: About
|
||||||
|
---
|
||||||
|
|
||||||
|
This is an about page, left here as an example
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: AbuseEval v1.0
|
||||||
|
link-to-publication: http://www.lrec-conf.org/proceedings/lrec2020/pdf/2020.lrec-1.760.pdf
|
||||||
|
link-to-data: https://github.com/tommasoc80/AbuseEval
|
||||||
|
task-description: Explicitness annotation of offensive and abusive content
|
||||||
|
details-of-task: "Enriched versions of the OffensEval/OLID dataset with the distinction of explicit/implicit offensive messages and the new dimension for abusive messages. Labels for offensive language: EXPLICIT, IMPLICT, NOT; Labels for abusive language: EXPLICIT, IMPLICT, NOTABU"
|
||||||
|
size-of-dataset: 14100
|
||||||
|
percentage-abusive: 20.75
|
||||||
|
language: English
|
||||||
|
level-of-annotation: ["Tweets"]
|
||||||
|
platform: ["Twitter"]
|
||||||
|
medium: ["Text"]
|
||||||
|
reference: "Caselli, T., Basile, V., Jelena, M., Inga, K., and Michael, G. 2020. \"I feel offended, don’t be abusive! implicit/explicit messages in offensive and abusive language\". The 12th Language Resources and Evaluation Conference (pp. 6193-6202). European Language Resources Association."
|
||||||
|
---
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "Abusive Language Detection on Arabic Social Media (Al Jazeera)"
|
||||||
|
link-to-publication: https://www.aclweb.org/anthology/W17-3008
|
||||||
|
link-to-data: http://alt.qcri.org/~hmubarak/offensive/AJCommentsClassification-CF.xlsx
|
||||||
|
task-description: Ternary (Obscene, Offensive but not obscene, Clean)
|
||||||
|
details-of-task: Incivility
|
||||||
|
size-of-dataset: 32000
|
||||||
|
percentage-abusive: 0.81
|
||||||
|
language: Arabic
|
||||||
|
level-of-annotation: ["Posts"]
|
||||||
|
platform: ["AlJazeera"]
|
||||||
|
medium: ["Text"]
|
||||||
|
reference: "Mubarak, H., Darwish, K. and Magdy, W., 2017. Abusive Language Detection on Arabic Social Media. In: Proceedings of the First Workshop on Abusive Language Online. Vancouver, Canada: Association for Computational Linguistics, pp.52-56."
|
||||||
|
---
|
||||||
|
|
||||||
|
SOMETHING TEST
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: "CoRAL: a Context-aware Croatian Abusive Language Dataset"
|
||||||
|
link-to-publication: https://aclanthology.org/2022.findings-aacl.21/
|
||||||
|
link-to-data: https://github.com/shekharRavi/CoRAL-dataset-Findings-of-the-ACL-AACL-IJCNLP-2022
|
||||||
|
task-description: Multi-class based on context dependency categories (CDC)
|
||||||
|
details-of-task: Detectioning CDC from abusive comments
|
||||||
|
size-of-dataset: 2240
|
||||||
|
percentage-abusive: 100
|
||||||
|
language: "Croatian"
|
||||||
|
level-of-annotation: ["Posts"]
|
||||||
|
platform: ["Posts"]
|
||||||
|
medium: ["Newspaper Comments"]
|
||||||
|
reference: "Ravi Shekhar, Mladen Karan and Matthew Purver (2022). CoRAL: a Context-aware Croatian Abusive Language Dataset. Findings of the ACL: AACL-IJCNLP."
|
||||||
|
---
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Detecting Abusive Albanian
|
||||||
|
link-to-publication: https://arxiv.org/abs/2107.13592
|
||||||
|
link-to-data: https://doi.org/10.6084/m9.figshare.19333298.v1
|
||||||
|
task-description: Hierarchical (offensive/not; untargeted/targeted; person/group/other)
|
||||||
|
details-of-task: Detect and categorise abusive language in social media data
|
||||||
|
size-of-dataset: 11874
|
||||||
|
percentage-abusive: 13.2
|
||||||
|
language: Albanian
|
||||||
|
level-of-annotation: ["Posts"]
|
||||||
|
platform: ["Instagram", "Youtube"]
|
||||||
|
medium: ["Text"]
|
||||||
|
reference: "Nurce, E., Keci, J., Derczynski, L., 2021. Detecting Abusive Albanian. arXiv:2107.13592"
|
||||||
|
---
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: "Hate Speech Detection in the Bengali language: A Dataset and its Baseline Evaluation"
|
||||||
|
link-to-publication: https://arxiv.org/pdf/2012.09686.pdf
|
||||||
|
link-to-data: https://www.kaggle.com/naurosromim/bengali-hate-speech-dataset
|
||||||
|
task-description: Binary (hateful, not)
|
||||||
|
details-of-task: "Several categories: sports, entertainment, crime, religion, politics, celebrity and meme"
|
||||||
|
size-of-dataset: 30000
|
||||||
|
percentage-abusive: 0.33
|
||||||
|
language: Bengali
|
||||||
|
level-of-annotation: ["Posts"]
|
||||||
|
platform: ["Youtube", "Facebook"]
|
||||||
|
medium: ["Text"]
|
||||||
|
reference: "Romim, N., Ahmed, M., Talukder, H., & Islam, M. S. (2021). Hate speech detection in the bengali language: A dataset and its baseline evaluation. In Proceedings of International Joint Conference on Advances in Computational Intelligence (pp. 457-468). Springer, Singapore."
|
||||||
|
---
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Large-Scale Hate Speech Detection with Cross-Domain Transfer
|
||||||
|
link-to-publication: https://aclanthology.org/2022.lrec-1.238/
|
||||||
|
link-to-data: https://github.com/avaapm/hatespeech
|
||||||
|
task-description: Three-class (Hate speech, Offensive language, None)
|
||||||
|
details-of-task: Hate speech detection on social media (Twitter) including 5 target groups (gender, race, religion, politics, sports)
|
||||||
|
size-of-dataset: "100k English (27593 hate, 30747 offensive, 41660 none)"
|
||||||
|
percentage-abusive: 58.3
|
||||||
|
language: English
|
||||||
|
level-of-annotation: ["Posts"]
|
||||||
|
platform: ["Twitter"]
|
||||||
|
medium: ["Text", "Image"]
|
||||||
|
reference: "Cagri Toraman, Furkan Şahinuç, Eyup Yilmaz. 2022. Large-Scale Hate Speech Detection with Cross-Domain Transfer. In Proceedings of the Thirteenth Language Resources and Evaluation Conference, pages 2215–2225, Marseille, France. European Language Resources Association."
|
||||||
|
---
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: "Let-Mi: An Arabic Levantine Twitter Dataset for Misogynistic Language"
|
||||||
|
link-to-publication: https://arxiv.org/abs/2103.10195
|
||||||
|
link-to-data: https://drive.google.com/file/d/1mM2vnjsy7QfUmdVUpKqHRJjZyQobhTrW/view
|
||||||
|
task-description: Binary (misogyny/none) and Multi-class (none, discredit, derailing, dominance, stereotyping & objectification, threat of violence, sexual harassment, damning)
|
||||||
|
details-of-task: Introducing an Arabic Levantine Twitter dataset for Misogynistic language
|
||||||
|
size-of-dataset: 6603
|
||||||
|
percentage-abusive: 48.76
|
||||||
|
language: Arabic
|
||||||
|
level-of-annotation: ["Posts"]
|
||||||
|
platform: ["Twitter"]
|
||||||
|
medium: ["Text", "Images"]
|
||||||
|
reference: "Hala Mulki and Bilal Ghanem. 2021. Let-Mi: An Arabic Levantine Twitter Dataset for Misogynistic Language. In Proceedings of the Sixth Arabic Natural Language Processing Workshop, pages 154–163, Kyiv, Ukraine (Virtual). Association for Computational Linguistics"
|
||||||
|
---
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Measuring Hate Speech
|
||||||
|
link-to-publication: https://arxiv.org/abs/2009.10277
|
||||||
|
link-to-data: https://huggingface.co/datasets/ucberkeley-dlab/measuring-hate-speech
|
||||||
|
task-description: 10 ordinal labels (sentiment, (dis)respect, insult, humiliation, inferior status, violence, dehumanization, genocide, attack/defense, hate speech), which are debiased and aggregated into a continuous hate speech severity score (hate_speech_score) that includes a region for counterspeech & supportive speeech. Includes 8 target identity groups (race/ethnicity, religion, national origin/citizenship, gender, sexual orientation, age, disability, political ideology) and 42 identity subgroups.
|
||||||
|
details-of-task: Hate speech measurement on social media in English
|
||||||
|
size-of-dataset: "39,565 comments annotated by 7,912 annotators on 10 ordinal labels, for 1,355,560 total labels."
|
||||||
|
percentage-abusive: 25
|
||||||
|
language: English
|
||||||
|
level-of-annotation: ["Social media comment"]
|
||||||
|
platform: ["Twitter", "Reddit", "Youtube"]
|
||||||
|
medium: ["Text"]
|
||||||
|
reference: "Kennedy, C. J., Bacon, G., Sahn, A., & von Vacano, C. (2020). Constructing interval variables via faceted Rasch measurement and multitask deep learning: a hate speech application. arXiv preprint arXiv:2009.10277."
|
||||||
|
---
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Offensive Language and Hate Speech Detection for Danish
|
||||||
|
link-to-publication: http://www.derczynski.com/papers/danish_hsd.pdf
|
||||||
|
link-to-data: https://figshare.com/articles/Danish_Hate_Speech_Abusive_Language_data/12220805
|
||||||
|
task-description: "Branching structure of tasks: Binary (Offensive, Not), Within Offensive (Target, Not), Within Target (Individual, Group, Other)"
|
||||||
|
details-of-task: Group-directed + Person-directed
|
||||||
|
size-of-dataset: 3600
|
||||||
|
percentage-abusive: 0.12
|
||||||
|
language: Danish
|
||||||
|
level-of-annotation: ["Posts"]
|
||||||
|
platform: ["Twitter", "Reddit", "Newspaper comments"]
|
||||||
|
medium: ["Text"]
|
||||||
|
reference: "Sigurbergsson, G. and Derczynski, L., 2019. Offensive Language and Hate Speech Detection for Danish. ArXiv."
|
||||||
|
---
|
||||||
34
examples/alan-turing-portal/content/index.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
title: Hate Speech Dataset Catalogue
|
||||||
|
---
|
||||||
|
|
||||||
|
This page catalogues datasets annotated for hate speech, online abuse, and offensive language. They may be useful for e.g. training a natural language processing system to detect this language.
|
||||||
|
|
||||||
|
The list is maintained by Leon Derczynski, Bertie Vidgen, Hannah Rose Kirk, Pica Johansson, Yi-Ling Chung, Mads Guldborg Kjeldgaard Kongsbak, Laila Sprejer, and Philine Zeinert.
|
||||||
|
|
||||||
|
We provide a list of datasets and keywords. If you would like to contribute to our catalogue or add your dataset, please see the instructions for contributing.
|
||||||
|
|
||||||
|
If you use these resources, please cite (and read!) our paper: Directions in Abusive Language Training Data: Garbage In, Garbage Out. And if you would like to find other resources for researching online hate, visit The Alan Turing Institute’s Online Hate Research Hub or read The Alan Turing Institute’s Reading List on Online Hate and Abuse Research.
|
||||||
|
|
||||||
|
If you’re looking for a good paper on online hate training datasets (beyond our paper, of course!) then have a look at ‘Resources and benchmark corpora for hate speech detection: a systematic review’ by Poletto et al. in Language Resources and Evaluation.
|
||||||
|
|
||||||
|
## How to contribute
|
||||||
|
|
||||||
|
We accept entries to our catalogue based on pull requests to the content folder. The dataset must be avaliable for download to be included in the list. If you want to add an entry, follow these steps!
|
||||||
|
|
||||||
|
Please send just one dataset addition/edit at a time - edit it in, then save. This will make everyone’s life easier (including yours!)
|
||||||
|
|
||||||
|
- Go to the repo url file and click the "Add file" dropdown and then click on "Create new file".
|
||||||
|

|
||||||
|
|
||||||
|
- In the following page type `content/datasets/<name-of-the-file>.md`. if you want to add an entry to the datasets catalog or `content/keywords/<name-of-the-file>.md` if you want to add an entry to the lists of abusive keywords, if you want to just add an static page you can leave in the root of `content` it will automatically get assigned an url eg: `/content/about.md` becomes the `/about` page
|
||||||
|

|
||||||
|
|
||||||
|
- Copy the contents of `templates/dataset.md` or `templates/keywords.md` respectively to the camp below, filling out the fields with the correct data format
|
||||||
|

|
||||||
|
|
||||||
|
- Click on "Commit changes", on the popup make sure you give some brief detail on the proposed change. and then click on Propose changes
|
||||||
|
<img src='https://i.imgur.com/BxuxKEJ.png' style={{ maxWidth: '50%', margin: '0 auto' }}/>
|
||||||
|
|
||||||
|
- Submit the pull request on the next page when prompted.
|
||||||
|
|
||||||
10
examples/alan-turing-portal/content/keywords/hurtlex.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
title: Hurtlex
|
||||||
|
description: HurtLex is a lexicon of offensive, aggressive, and hateful words in over 50 languages. The words are divided into 17 categories, plus a macro-category indicating whether there is stereotype involved.
|
||||||
|
data-link: https://github.com/valeriobasile/hurtlex
|
||||||
|
reference: http://ceur-ws.org/Vol-2253/paper49.pdf, Proc. CLiC-it 2018
|
||||||
|
---
|
||||||
|
|
||||||
|
## Markdown TEST
|
||||||
|
|
||||||
|
Some text
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: SexHateLex is a Chinese lexicon of hateful and sexist words.
|
||||||
|
data-link: https://doi.org/10.5281/zenodo.4773875
|
||||||
|
reference: http://ceur-ws.org/Vol-2253/paper49.pdf, Journal of OSNEM, Vol.27, 2022, 100182, ISSN 2468-6964.
|
||||||
|
---
|
||||||
11
examples/alan-turing-portal/jsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/components/*": ["components/*"],
|
||||||
|
"@/pages/*": ["pages/*"],
|
||||||
|
"@/lib/*": ["lib/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
8
examples/alan-turing-portal/lib/formatDate.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export function formatDate(dateString) {
|
||||||
|
return new Date(`${dateString}T00:00:00Z`).toLocaleDateString('en-US', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
timeZone: 'UTC',
|
||||||
|
})
|
||||||
|
}
|
||||||
98
examples/alan-turing-portal/lib/markdown.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import matter from "gray-matter";
|
||||||
|
import mdxmermaid from "mdx-mermaid";
|
||||||
|
import { h } from "hastscript";
|
||||||
|
import remarkCallouts from "@flowershow/remark-callouts";
|
||||||
|
import remarkEmbed from "@flowershow/remark-embed";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import remarkMath from "remark-math";
|
||||||
|
import remarkSmartypants from "remark-smartypants";
|
||||||
|
import remarkToc from "remark-toc";
|
||||||
|
import remarkWikiLink from "@flowershow/remark-wiki-link";
|
||||||
|
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||||
|
import rehypeKatex from "rehype-katex";
|
||||||
|
import rehypeSlug from "rehype-slug";
|
||||||
|
import rehypePrismPlus from "rehype-prism-plus";
|
||||||
|
|
||||||
|
import { serialize } from "next-mdx-remote/serialize";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a markdown or MDX file to an MDX source form + front matter data
|
||||||
|
*
|
||||||
|
* @source: the contents of a markdown or mdx file
|
||||||
|
* @format: used to indicate to next-mdx-remote which format to use (md or mdx)
|
||||||
|
* @returns: { mdxSource: mdxSource, frontMatter: ...}
|
||||||
|
*/
|
||||||
|
const parse = async function (source, format) {
|
||||||
|
const { content, data } = matter(source);
|
||||||
|
|
||||||
|
const mdxSource = await serialize(
|
||||||
|
{ value: content, path: format },
|
||||||
|
{
|
||||||
|
// Optionally pass remark/rehype plugins
|
||||||
|
mdxOptions: {
|
||||||
|
remarkPlugins: [
|
||||||
|
remarkEmbed,
|
||||||
|
remarkGfm,
|
||||||
|
[remarkSmartypants, { quotes: false, dashes: "oldschool" }],
|
||||||
|
remarkMath,
|
||||||
|
remarkCallouts,
|
||||||
|
remarkWikiLink,
|
||||||
|
[
|
||||||
|
remarkToc,
|
||||||
|
{
|
||||||
|
heading: "Table of contents",
|
||||||
|
tight: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[mdxmermaid, {}],
|
||||||
|
],
|
||||||
|
rehypePlugins: [
|
||||||
|
rehypeSlug,
|
||||||
|
[
|
||||||
|
rehypeAutolinkHeadings,
|
||||||
|
{
|
||||||
|
properties: { className: "heading-link" },
|
||||||
|
test(element) {
|
||||||
|
return (
|
||||||
|
["h2", "h3", "h4", "h5", "h6"].includes(element.tagName) &&
|
||||||
|
element.properties?.id !== "table-of-contents" &&
|
||||||
|
element.properties?.className !== "blockquote-heading"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
content() {
|
||||||
|
return [
|
||||||
|
h(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
xmlns: "http:www.w3.org/2000/svg",
|
||||||
|
fill: "#ab2b65",
|
||||||
|
viewBox: "0 0 20 20",
|
||||||
|
className: "w-5 h-5",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h("path", {
|
||||||
|
fillRule: "evenodd",
|
||||||
|
clipRule: "evenodd",
|
||||||
|
d: "M9.493 2.853a.75.75 0 00-1.486-.205L7.545 6H4.198a.75.75 0 000 1.5h3.14l-.69 5H3.302a.75.75 0 000 1.5h3.14l-.435 3.148a.75.75 0 001.486.205L7.955 14h2.986l-.434 3.148a.75.75 0 001.486.205L12.456 14h3.346a.75.75 0 000-1.5h-3.14l.69-5h3.346a.75.75 0 000-1.5h-3.14l.435-3.147a.75.75 0 00-1.486-.205L12.045 6H9.059l.434-3.147zM8.852 7.5l-.69 5h2.986l.69-5H8.852z",
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[rehypeKatex, { output: "mathml" }],
|
||||||
|
[rehypePrismPlus, { ignoreMissing: true }],
|
||||||
|
],
|
||||||
|
format,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mdxSource: mdxSource,
|
||||||
|
frontMatter: data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default parse;
|
||||||
14
examples/alan-turing-portal/lib/mddb.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { MarkdownDB } from "@flowershow/markdowndb";
|
||||||
|
|
||||||
|
const dbPath = "markdown.db";
|
||||||
|
|
||||||
|
const client = new MarkdownDB({
|
||||||
|
client: "sqlite3",
|
||||||
|
connection: {
|
||||||
|
filename: dbPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientPromise = client.init();
|
||||||
|
|
||||||
|
export default clientPromise;
|
||||||
BIN
examples/alan-turing-portal/markdown.db
Normal file
5
examples/alan-turing-portal/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/// <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.
|
||||||
3
examples/alan-turing-portal/next.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
swcMinify: true,
|
||||||
|
};
|
||||||
22977
examples/alan-turing-portal/package-lock.json
generated
Normal file
73
examples/alan-turing-portal/package.json
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"name": "tailwindui-template",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"prebuild": "npm run mddb",
|
||||||
|
"mddb": "mddb ./content"
|
||||||
|
},
|
||||||
|
"browserslist": "defaults, not ie <= 11",
|
||||||
|
"dependencies": {
|
||||||
|
"@flowershow/core": "^0.4.10",
|
||||||
|
"@flowershow/markdowndb": "^0.1.1",
|
||||||
|
"@flowershow/remark-callouts": "^1.0.0",
|
||||||
|
"@flowershow/remark-embed": "^1.0.0",
|
||||||
|
"@flowershow/remark-wiki-link": "^1.1.2",
|
||||||
|
"@headlessui/react": "^1.7.13",
|
||||||
|
"@heroicons/react": "^2.0.17",
|
||||||
|
"@mapbox/rehype-prism": "^0.8.0",
|
||||||
|
"@mdx-js/loader": "^2.1.5",
|
||||||
|
"@mdx-js/react": "^2.1.5",
|
||||||
|
"@next/mdx": "^13.0.2",
|
||||||
|
"@opentelemetry/api": "^1.4.0",
|
||||||
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
|
"@tailwindcss/typography": "^0.5.4",
|
||||||
|
"@tanstack/react-table": "^8.8.5",
|
||||||
|
"@types/node": "18.16.0",
|
||||||
|
"@types/react": "18.2.0",
|
||||||
|
"@types/react-dom": "18.2.0",
|
||||||
|
"autoprefixer": "^10.4.12",
|
||||||
|
"clsx": "^1.2.1",
|
||||||
|
"eslint": "8.39.0",
|
||||||
|
"eslint-config-next": "13.3.1",
|
||||||
|
"fast-glob": "^3.2.11",
|
||||||
|
"feed": "^4.2.2",
|
||||||
|
"flexsearch": "^0.7.31",
|
||||||
|
"focus-visible": "^5.2.0",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
|
"hastscript": "^7.2.0",
|
||||||
|
"mdx-mermaid": "^2.0.0-rc7",
|
||||||
|
"mermaid": "^10.1.0",
|
||||||
|
"next": "13.2.1",
|
||||||
|
"next-mdx-remote": "^4.4.1",
|
||||||
|
"next-router-mock": "^0.9.3",
|
||||||
|
"next-superjson-plugin": "^0.5.7",
|
||||||
|
"papaparse": "^5.4.1",
|
||||||
|
"postcss-focus-visible": "^6.0.4",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"react-hook-form": "^7.43.9",
|
||||||
|
"react-markdown": "^8.0.7",
|
||||||
|
"react-vega": "^7.6.0",
|
||||||
|
"rehype-autolink-headings": "^6.1.1",
|
||||||
|
"rehype-katex": "^6.0.3",
|
||||||
|
"rehype-prism-plus": "^1.5.1",
|
||||||
|
"rehype-slug": "^5.1.0",
|
||||||
|
"remark-gfm": "^3.0.1",
|
||||||
|
"remark-math": "^5.1.1",
|
||||||
|
"remark-smartypants": "^2.0.0",
|
||||||
|
"remark-toc": "^8.0.1",
|
||||||
|
"superjson": "^1.12.3",
|
||||||
|
"tailwindcss": "^3.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "8.26.0",
|
||||||
|
"eslint-config-next": "13.0.2",
|
||||||
|
"prettier": "^2.8.7",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.2.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
105
examples/alan-turing-portal/pages/[...slug].jsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { Container } from '../components/Container'
|
||||||
|
import clientPromise from '../lib/mddb'
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import { MDXRemote } from 'next-mdx-remote'
|
||||||
|
import { serialize } from 'next-mdx-remote/serialize'
|
||||||
|
import { Card } from '../components/Card'
|
||||||
|
import Head from 'next/head'
|
||||||
|
import parse from '../lib/markdown'
|
||||||
|
import { Mermaid } from '@flowershow/core';
|
||||||
|
|
||||||
|
export const getStaticProps = async ({ params }) => {
|
||||||
|
const urlPath = params.slug ? params.slug.join('/') : ''
|
||||||
|
|
||||||
|
const mddb = await clientPromise
|
||||||
|
const dbFile = await mddb.getFileByUrl(urlPath)
|
||||||
|
|
||||||
|
const source = await fs.readFile(dbFile.file_path,'utf-8')
|
||||||
|
let mdxSource = await parse(source, '.mdx')
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
mdxSource,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const mddb = await clientPromise
|
||||||
|
const allDocuments = await mddb.getFiles({ extensions: ['md', 'mdx'] })
|
||||||
|
|
||||||
|
const paths = allDocuments.filter(document => document.url_path !== '/').map((page) => {
|
||||||
|
const parts = page.url_path.split('/')
|
||||||
|
return { params: { slug: parts } }
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
fallback: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidUrl = (urlString) => {
|
||||||
|
try {
|
||||||
|
return Boolean(new URL(urlString))
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Meta = ({keyValuePairs}) => {
|
||||||
|
const prettifyMetaValue = (value) => value.replaceAll('-',' ').charAt(0).toUpperCase() + value.replaceAll('-',' ').slice(1);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{keyValuePairs.map((entry) => {
|
||||||
|
return isValidUrl(entry[1]) ? (
|
||||||
|
<Card.Description>
|
||||||
|
<span className="font-semibold">
|
||||||
|
{prettifyMetaValue(entry[0])}: {' '}
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
className="text-ellipsis underline transition hover:text-teal-400 dark:hover:text-teal-900"
|
||||||
|
href={entry[1]}
|
||||||
|
>
|
||||||
|
{entry[1]}
|
||||||
|
</a>
|
||||||
|
</Card.Description>
|
||||||
|
) : (
|
||||||
|
<Card.Description>
|
||||||
|
<span className="font-semibold">{prettifyMetaValue(entry[0])}: </span>
|
||||||
|
{Array.isArray(entry[1]) ? entry[1].join(', ') : entry[1]}
|
||||||
|
</Card.Description>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DRDPage({ mdxSource }) {
|
||||||
|
const meta = mdxSource.frontMatter
|
||||||
|
const keyValuePairs = Object.entries(meta).filter(
|
||||||
|
(entry) => entry[0] !== 'title'
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{meta.title}</title>
|
||||||
|
</Head>
|
||||||
|
<Container className="mt-16 lg:mt-32">
|
||||||
|
<article>
|
||||||
|
<header className="flex flex-col">
|
||||||
|
<h1 className="mt-6 text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||||
|
{meta.title}
|
||||||
|
</h1>
|
||||||
|
<Card as="article">
|
||||||
|
<Meta keyValuePairs={keyValuePairs} />
|
||||||
|
</Card>
|
||||||
|
</header>
|
||||||
|
<div className="prose dark:prose-invert">
|
||||||
|
<MDXRemote {...mdxSource.mdxSource} components={{mermaid: Mermaid}} />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
38
examples/alan-turing-portal/pages/_app.jsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
import { Footer } from '../components/Footer'
|
||||||
|
import { Header } from '../components/Header'
|
||||||
|
|
||||||
|
import '../styles/tailwind.css'
|
||||||
|
import 'focus-visible'
|
||||||
|
|
||||||
|
function usePrevious(value) {
|
||||||
|
let ref = useRef()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
return ref.current
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App({ Component, pageProps, router }) {
|
||||||
|
let previousPathname = usePrevious(router.pathname)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="fixed inset-0 flex justify-center sm:px-8">
|
||||||
|
<div className="flex w-full max-w-7xl lg:px-8">
|
||||||
|
<div className="w-full bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<Header />
|
||||||
|
<main>
|
||||||
|
<Component previousPathname={previousPathname} {...pageProps} />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
60
examples/alan-turing-portal/pages/_document.jsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { Head, Html, Main, NextScript } from 'next/document'
|
||||||
|
|
||||||
|
const modeScript = `
|
||||||
|
let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
|
||||||
|
updateMode()
|
||||||
|
darkModeMediaQuery.addEventListener('change', updateModeWithoutTransitions)
|
||||||
|
window.addEventListener('storage', updateModeWithoutTransitions)
|
||||||
|
|
||||||
|
function updateMode() {
|
||||||
|
let isSystemDarkMode = darkModeMediaQuery.matches
|
||||||
|
let isDarkMode = window.localStorage.isDarkMode === 'true' || (!('isDarkMode' in window.localStorage) && isSystemDarkMode)
|
||||||
|
|
||||||
|
if (isDarkMode) {
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDarkMode === isSystemDarkMode) {
|
||||||
|
delete window.localStorage.isDarkMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableTransitionsTemporarily() {
|
||||||
|
document.documentElement.classList.add('[&_*]:!transition-none')
|
||||||
|
window.setTimeout(() => {
|
||||||
|
document.documentElement.classList.remove('[&_*]:!transition-none')
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateModeWithoutTransitions() {
|
||||||
|
disableTransitionsTemporarily()
|
||||||
|
updateMode()
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function Document() {
|
||||||
|
return (
|
||||||
|
<Html className="h-full antialiased" lang="en">
|
||||||
|
<Head>
|
||||||
|
<script dangerouslySetInnerHTML={{ __html: modeScript }} />
|
||||||
|
<link
|
||||||
|
rel="alternate"
|
||||||
|
type="application/rss+xml"
|
||||||
|
href={`${process.env.NEXT_PUBLIC_SITE_URL}/rss/feed.xml`}
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="alternate"
|
||||||
|
type="application/feed+json"
|
||||||
|
href={`${process.env.NEXT_PUBLIC_SITE_URL}/rss/feed.json`}
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
|
<body className="flex h-full flex-col bg-zinc-50 dark:bg-black">
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
||||||
281
examples/alan-turing-portal/pages/index.jsx
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import Head from 'next/head'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
|
import { Card } from '../components/Card'
|
||||||
|
import { Container } from '../components/Container'
|
||||||
|
import clientPromise from '../lib/mddb'
|
||||||
|
import { Index } from 'flexsearch'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { serialize } from 'next-mdx-remote/serialize'
|
||||||
|
import { MDXRemote } from 'next-mdx-remote'
|
||||||
|
|
||||||
|
function DatasetCard({ dataset }) {
|
||||||
|
return (
|
||||||
|
<Card as="article">
|
||||||
|
<Card.Title><Link href={dataset.url}>{dataset.title}</Link></Card.Title>
|
||||||
|
<Card.Description>
|
||||||
|
<span className="font-semibold">Link to publication: </span>{' '}
|
||||||
|
<a
|
||||||
|
className="text-ellipsis underline transition hover:text-teal-400 dark:hover:text-teal-900"
|
||||||
|
href={dataset['link-to-publication']}
|
||||||
|
>
|
||||||
|
{dataset['link-to-publication']}
|
||||||
|
</a>
|
||||||
|
</Card.Description>
|
||||||
|
<Card.Description>
|
||||||
|
<span className="font-semibold">Link to data: </span>
|
||||||
|
<a
|
||||||
|
className="text-ellipsis underline transition hover:text-teal-600 dark:hover:text-teal-900"
|
||||||
|
href={dataset['link-to-data']}
|
||||||
|
>
|
||||||
|
{dataset['link-to-data']}
|
||||||
|
</a>
|
||||||
|
</Card.Description>
|
||||||
|
<Card.Description>
|
||||||
|
<span className="font-semibold">Task Description: </span>
|
||||||
|
{dataset['task-description']}
|
||||||
|
</Card.Description>
|
||||||
|
<Card.Description>
|
||||||
|
<span className="font-semibold">Details of Task: </span>{' '}
|
||||||
|
{dataset['details-of-task']}
|
||||||
|
</Card.Description>
|
||||||
|
<Card.Description>
|
||||||
|
<span className="font-semibold">Size of Dataset: </span>{' '}
|
||||||
|
{dataset['size-of-dataset']}
|
||||||
|
</Card.Description>
|
||||||
|
<Card.Description>
|
||||||
|
<span className="font-semibold">Percentage Abusive: </span>
|
||||||
|
{dataset['percentage-abusive']}%
|
||||||
|
</Card.Description>
|
||||||
|
<Card.Description>
|
||||||
|
<span className="font-semibold">Language: </span>
|
||||||
|
{dataset['language']}
|
||||||
|
</Card.Description>
|
||||||
|
<Card.Description>
|
||||||
|
<span className="font-semibold">Level of Annotation: </span>
|
||||||
|
{dataset['level-of-annotation'].join(', ')}
|
||||||
|
</Card.Description>
|
||||||
|
<Card.Description>
|
||||||
|
<span className="font-semibold">Platform: </span>
|
||||||
|
{dataset['platform'].join(', ')}
|
||||||
|
</Card.Description>
|
||||||
|
<Card.Description>
|
||||||
|
<span className="font-semibold">Medium: </span>
|
||||||
|
{dataset['medium'].join(', ')}
|
||||||
|
</Card.Description>
|
||||||
|
<Card.Description>
|
||||||
|
<span className="font-semibold">Reference: </span>
|
||||||
|
{dataset['reference']}
|
||||||
|
</Card.Description>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListOfAbusiveKeywordsCard({ list }) {
|
||||||
|
return (
|
||||||
|
<Card as="article">
|
||||||
|
<Card.Title><Link href={list.url}>{list.title}</Link></Card.Title>
|
||||||
|
{list.description && (
|
||||||
|
<Card.Description>
|
||||||
|
<span className="font-semibold">List Description: </span>{' '}
|
||||||
|
{list.description}
|
||||||
|
</Card.Description>
|
||||||
|
)}
|
||||||
|
<Card.Description>
|
||||||
|
<span className="font-semibold">Data Link: </span>
|
||||||
|
<a
|
||||||
|
className="text-ellipsis underline transition hover:text-teal-600 dark:hover:text-teal-900"
|
||||||
|
href={list['data-link']}
|
||||||
|
>
|
||||||
|
{list['data-link']}
|
||||||
|
</a>
|
||||||
|
</Card.Description>
|
||||||
|
<Card.Description>
|
||||||
|
<span className="font-semibold">Reference: </span>
|
||||||
|
<a
|
||||||
|
className="text-ellipsis underline transition hover:text-teal-600 dark:hover:text-teal-900"
|
||||||
|
href={list.reference}
|
||||||
|
>
|
||||||
|
{list.reference}
|
||||||
|
</a>
|
||||||
|
</Card.Description>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home({
|
||||||
|
datasets,
|
||||||
|
indexText,
|
||||||
|
listsOfKeywords,
|
||||||
|
availableLanguages,
|
||||||
|
availablePlatforms,
|
||||||
|
}) {
|
||||||
|
const index = new Index()
|
||||||
|
datasets.forEach((dataset) =>
|
||||||
|
index.add(
|
||||||
|
dataset.id,
|
||||||
|
`${dataset.title} ${dataset['task-description']} ${dataset['details-of-task']} ${dataset['reference']}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const { register, watch, handleSubmit, reset } = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
searchTerm: '',
|
||||||
|
lang: '',
|
||||||
|
platform: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Hate Speech Dataset Catalogue</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Catalog of abusive language data (PLoS 2020)"
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
|
<Container className="mt-9">
|
||||||
|
<div className="max-w-2xl">
|
||||||
|
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||||
|
{indexText.frontmatter.title}
|
||||||
|
</h1>
|
||||||
|
<article className="mt-6 index-text flex flex-col gap-y-2 text-base text-zinc-600 dark:text-zinc-400 prose dark:prose-invert">
|
||||||
|
<MDXRemote {...indexText} />
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
<Container className="mt-24 md:mt-28">
|
||||||
|
<div className="mx-auto grid max-w-7xl grid-cols-1 gap-y-8 lg:max-w-none">
|
||||||
|
<h2 className="text-xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||||
|
Datasets
|
||||||
|
</h2>
|
||||||
|
<form onSubmit={handleSubmit(() => reset())} className="rounded-2xl border border-zinc-100 px-4 py-6 dark:border-zinc-700/40 sm:p-6">
|
||||||
|
<p className="mt-2 text-lg font-semibold text-zinc-600 dark:text-zinc-100">
|
||||||
|
Search for datasets
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
|
||||||
|
<input
|
||||||
|
placeholder="Search here"
|
||||||
|
aria-label="Hate speech on Twitter"
|
||||||
|
{...register('searchTerm')}
|
||||||
|
className="min-w-0 flex-auto appearance-none rounded-md border border-zinc-900/10 bg-white px-3 py-[calc(theme(spacing.2)-1px)] shadow-md shadow-zinc-800/5 placeholder:text-zinc-600 focus:border-teal-500 focus:outline-none focus:ring-4 focus:ring-teal-500/10 dark:border-zinc-700 dark:bg-zinc-700/[0.15] dark:text-zinc-200 dark:placeholder:text-zinc-200 dark:focus:border-teal-400 dark:focus:ring-teal-400/10 sm:text-sm"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
placeholder="Language"
|
||||||
|
defaultValue=""
|
||||||
|
className="min-w-0 flex-auto appearance-none rounded-md border border-zinc-900/10 bg-white px-3 py-[calc(theme(spacing.2)-1px)] text-zinc-600 shadow-md shadow-zinc-800/5 placeholder:text-zinc-400 focus:border-teal-500 focus:outline-none focus:ring-4 focus:ring-teal-500/10 dark:border-zinc-700 dark:bg-zinc-700/[0.15] dark:text-zinc-200 dark:placeholder:text-zinc-500 dark:focus:border-teal-400 dark:focus:ring-teal-400/10 sm:text-sm"
|
||||||
|
{...register('lang')}
|
||||||
|
>
|
||||||
|
<option value="" disabled hidden>
|
||||||
|
Filter by language
|
||||||
|
</option>
|
||||||
|
{availableLanguages.map((lang) => (
|
||||||
|
<option
|
||||||
|
key={lang}
|
||||||
|
className="dark:bg-white dark:text-black"
|
||||||
|
value={lang}
|
||||||
|
>
|
||||||
|
{lang}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
placeholder="Platform"
|
||||||
|
defaultValue=""
|
||||||
|
className="min-w-0 flex-auto appearance-none rounded-md border border-zinc-900/10 bg-white px-3 py-[calc(theme(spacing.2)-1px)] text-zinc-600 shadow-md shadow-zinc-800/5 placeholder:text-zinc-400 focus:border-teal-500 focus:outline-none focus:ring-4 focus:ring-teal-500/10 dark:border-zinc-700 dark:bg-zinc-700/[0.15] dark:text-zinc-200 dark:placeholder:text-zinc-500 dark:focus:border-teal-400 dark:focus:ring-teal-400/10 sm:text-sm"
|
||||||
|
{...register('platform')}
|
||||||
|
>
|
||||||
|
<option value="" disabled hidden>
|
||||||
|
Filter by platform
|
||||||
|
</option>
|
||||||
|
{availablePlatforms.map((platform) => (
|
||||||
|
<option
|
||||||
|
key={platform}
|
||||||
|
className="dark:bg-white dark:text-black"
|
||||||
|
value={platform}
|
||||||
|
>
|
||||||
|
{platform}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button type='submit' className='inline-flex items-center gap-2 justify-center rounded-md py-2 px-3 text-sm outline-offset-2 transition active:transition-none bg-zinc-800 font-semibold text-zinc-100 hover:bg-zinc-700 active:bg-zinc-800 active:text-zinc-100/70 dark:bg-zinc-700 dark:hover:bg-zinc-600 dark:active:bg-zinc-700 dark:active:text-zinc-100/70 flex-none'>Clear filters</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div className="flex flex-col gap-16">
|
||||||
|
{datasets
|
||||||
|
.filter((dataset) =>
|
||||||
|
watch().searchTerm && watch().searchTerm !== ''
|
||||||
|
? index.search(watch().searchTerm).includes(dataset.id)
|
||||||
|
: true
|
||||||
|
)
|
||||||
|
.filter((dataset) =>
|
||||||
|
watch().lang && watch().lang !== ''
|
||||||
|
? dataset.language === watch().lang
|
||||||
|
: true
|
||||||
|
)
|
||||||
|
.filter((dataset) =>
|
||||||
|
watch().platform && watch().platform !== ''
|
||||||
|
? dataset.platform.includes(watch().platform)
|
||||||
|
: true
|
||||||
|
)
|
||||||
|
.map((dataset) => (
|
||||||
|
<DatasetCard key={dataset.title} dataset={dataset} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
<Container className="mt-16">
|
||||||
|
<h2 className="text-xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||||
|
Lists of Abusive Keywords
|
||||||
|
</h2>
|
||||||
|
<div className="mt-3 flex flex-col gap-16">
|
||||||
|
{listsOfKeywords.map((list) => (
|
||||||
|
<ListOfAbusiveKeywordsCard key={list.title} list={list} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticProps() {
|
||||||
|
const mddb = await clientPromise
|
||||||
|
const datasetPages = await mddb.getFiles({
|
||||||
|
folder: 'datasets',
|
||||||
|
extensions: ['md', 'mdx'],
|
||||||
|
})
|
||||||
|
const datasets = datasetPages.map((page) => ({
|
||||||
|
...page.metadata,
|
||||||
|
id: page._id,
|
||||||
|
url: page.url_path,
|
||||||
|
}))
|
||||||
|
const listsOfKeywordsPages = await mddb.getFiles({
|
||||||
|
folder: 'keywords',
|
||||||
|
extensions: ['md', 'mdx'],
|
||||||
|
})
|
||||||
|
const listsOfKeywords = listsOfKeywordsPages.map((page) => ({
|
||||||
|
...page.metadata,
|
||||||
|
id: page._id,
|
||||||
|
url: page.url_path,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const index = await mddb.getFileByUrl('/')
|
||||||
|
let indexSource = fs.readFileSync(index.file_path, { encoding: 'utf-8' })
|
||||||
|
indexSource = await serialize(indexSource, { parseFrontmatter: true })
|
||||||
|
|
||||||
|
const availableLanguages = [
|
||||||
|
...new Set(datasets.map((dataset) => dataset.language)),
|
||||||
|
]
|
||||||
|
const availablePlatforms = [
|
||||||
|
...new Set(datasets.map((dataset) => dataset.platform).flat()),
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
datasets,
|
||||||
|
listsOfKeywords,
|
||||||
|
indexText: indexSource,
|
||||||
|
availableLanguages,
|
||||||
|
availablePlatforms,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
9
examples/alan-turing-portal/postcss.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
'postcss-focus-visible': {
|
||||||
|
replaceWith: '[data-focus-visible-added]',
|
||||||
|
},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
5
examples/alan-turing-portal/prettier.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
singleQuote: true,
|
||||||
|
semi: false,
|
||||||
|
plugins: [require('prettier-plugin-tailwindcss')],
|
||||||
|
}
|
||||||
BIN
examples/alan-turing-portal/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 566 B |
47
examples/alan-turing-portal/styles/prism.css
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
pre[class*='language-'] {
|
||||||
|
color: theme('colors.zinc.100');
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.tag,
|
||||||
|
.token.class-name,
|
||||||
|
.token.selector,
|
||||||
|
.token.selector .class,
|
||||||
|
.token.selector.class,
|
||||||
|
.token.function {
|
||||||
|
color: theme('colors.pink.400');
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.attr-name,
|
||||||
|
.token.keyword,
|
||||||
|
.token.rule,
|
||||||
|
.token.pseudo-class,
|
||||||
|
.token.important {
|
||||||
|
color: theme('colors.zinc.300');
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.module {
|
||||||
|
color: theme('colors.pink.400');
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.attr-value,
|
||||||
|
.token.class,
|
||||||
|
.token.string,
|
||||||
|
.token.property {
|
||||||
|
color: theme('colors.teal.300');
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation,
|
||||||
|
.token.attr-equals {
|
||||||
|
color: theme('colors.zinc.500');
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.unit,
|
||||||
|
.language-css .token.function {
|
||||||
|
color: theme('colors.sky.200');
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.comment,
|
||||||
|
.token.operator,
|
||||||
|
.token.combinator {
|
||||||
|
color: theme('colors.zinc.400');
|
||||||
|
}
|
||||||
13
examples/alan-turing-portal/styles/tailwind.css
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
@import 'tailwindcss/base';
|
||||||
|
@import 'tailwindcss/components';
|
||||||
|
@import './prism.css';
|
||||||
|
@import 'tailwindcss/utilities';
|
||||||
|
|
||||||
|
.index-text ul,
|
||||||
|
.index-text p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-text h2 {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
307
examples/alan-turing-portal/tailwind.config.js
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
],
|
||||||
|
darkMode: 'class',
|
||||||
|
plugins: [require('@tailwindcss/typography'), require('@tailwindcss/forms')],
|
||||||
|
theme: {
|
||||||
|
fontSize: {
|
||||||
|
xs: ['0.8125rem', { lineHeight: '1.5rem' }],
|
||||||
|
sm: ['0.875rem', { lineHeight: '1.5rem' }],
|
||||||
|
base: ['1rem', { lineHeight: '1.75rem' }],
|
||||||
|
lg: ['1.125rem', { lineHeight: '1.75rem' }],
|
||||||
|
xl: ['1.25rem', { lineHeight: '2rem' }],
|
||||||
|
'2xl': ['1.5rem', { lineHeight: '2rem' }],
|
||||||
|
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
|
||||||
|
'4xl': ['2rem', { lineHeight: '2.5rem' }],
|
||||||
|
'5xl': ['3rem', { lineHeight: '3.5rem' }],
|
||||||
|
'6xl': ['3.75rem', { lineHeight: '1' }],
|
||||||
|
'7xl': ['4.5rem', { lineHeight: '1' }],
|
||||||
|
'8xl': ['6rem', { lineHeight: '1' }],
|
||||||
|
'9xl': ['8rem', { lineHeight: '1' }],
|
||||||
|
},
|
||||||
|
typography: (theme) => ({
|
||||||
|
invert: {
|
||||||
|
css: {
|
||||||
|
'--tw-prose-body': 'var(--tw-prose-invert-body)',
|
||||||
|
'--tw-prose-headings': 'var(--tw-prose-invert-headings)',
|
||||||
|
'--tw-prose-links': 'var(--tw-prose-invert-links)',
|
||||||
|
'--tw-prose-links-hover': 'var(--tw-prose-invert-links-hover)',
|
||||||
|
'--tw-prose-underline': 'var(--tw-prose-invert-underline)',
|
||||||
|
'--tw-prose-underline-hover':
|
||||||
|
'var(--tw-prose-invert-underline-hover)',
|
||||||
|
'--tw-prose-bold': 'var(--tw-prose-invert-bold)',
|
||||||
|
'--tw-prose-counters': 'var(--tw-prose-invert-counters)',
|
||||||
|
'--tw-prose-bullets': 'var(--tw-prose-invert-bullets)',
|
||||||
|
'--tw-prose-hr': 'var(--tw-prose-invert-hr)',
|
||||||
|
'--tw-prose-quote-borders': 'var(--tw-prose-invert-quote-borders)',
|
||||||
|
'--tw-prose-captions': 'var(--tw-prose-invert-captions)',
|
||||||
|
'--tw-prose-code': 'var(--tw-prose-invert-code)',
|
||||||
|
'--tw-prose-code-bg': 'var(--tw-prose-invert-code-bg)',
|
||||||
|
'--tw-prose-pre-code': 'var(--tw-prose-invert-pre-code)',
|
||||||
|
'--tw-prose-pre-bg': 'var(--tw-prose-invert-pre-bg)',
|
||||||
|
'--tw-prose-pre-border': 'var(--tw-prose-invert-pre-border)',
|
||||||
|
'--tw-prose-th-borders': 'var(--tw-prose-invert-th-borders)',
|
||||||
|
'--tw-prose-td-borders': 'var(--tw-prose-invert-td-borders)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DEFAULT: {
|
||||||
|
css: {
|
||||||
|
'--tw-prose-body': theme('colors.zinc.600'),
|
||||||
|
'--tw-prose-headings': theme('colors.zinc.900'),
|
||||||
|
'--tw-prose-links': theme('colors.teal.500'),
|
||||||
|
'--tw-prose-links-hover': theme('colors.teal.600'),
|
||||||
|
'--tw-prose-underline': theme('colors.teal.500 / 0.2'),
|
||||||
|
'--tw-prose-underline-hover': theme('colors.teal.500'),
|
||||||
|
'--tw-prose-bold': theme('colors.zinc.900'),
|
||||||
|
'--tw-prose-counters': theme('colors.zinc.900'),
|
||||||
|
'--tw-prose-bullets': theme('colors.zinc.900'),
|
||||||
|
'--tw-prose-hr': theme('colors.zinc.100'),
|
||||||
|
'--tw-prose-quote-borders': theme('colors.zinc.200'),
|
||||||
|
'--tw-prose-captions': theme('colors.zinc.400'),
|
||||||
|
'--tw-prose-code': theme('colors.zinc.700'),
|
||||||
|
'--tw-prose-code-bg': theme('colors.zinc.300 / 0.2'),
|
||||||
|
'--tw-prose-pre-code': theme('colors.zinc.100'),
|
||||||
|
'--tw-prose-pre-bg': theme('colors.zinc.900'),
|
||||||
|
'--tw-prose-pre-border': 'transparent',
|
||||||
|
'--tw-prose-th-borders': theme('colors.zinc.200'),
|
||||||
|
'--tw-prose-td-borders': theme('colors.zinc.100'),
|
||||||
|
|
||||||
|
'--tw-prose-invert-body': theme('colors.zinc.400'),
|
||||||
|
'--tw-prose-invert-headings': theme('colors.zinc.200'),
|
||||||
|
'--tw-prose-invert-links': theme('colors.teal.400'),
|
||||||
|
'--tw-prose-invert-links-hover': theme('colors.teal.400'),
|
||||||
|
'--tw-prose-invert-underline': theme('colors.teal.400 / 0.3'),
|
||||||
|
'--tw-prose-invert-underline-hover': theme('colors.teal.400'),
|
||||||
|
'--tw-prose-invert-bold': theme('colors.zinc.200'),
|
||||||
|
'--tw-prose-invert-counters': theme('colors.zinc.200'),
|
||||||
|
'--tw-prose-invert-bullets': theme('colors.zinc.200'),
|
||||||
|
'--tw-prose-invert-hr': theme('colors.zinc.700 / 0.4'),
|
||||||
|
'--tw-prose-invert-quote-borders': theme('colors.zinc.500'),
|
||||||
|
'--tw-prose-invert-captions': theme('colors.zinc.500'),
|
||||||
|
'--tw-prose-invert-code': theme('colors.zinc.300'),
|
||||||
|
'--tw-prose-invert-code-bg': theme('colors.zinc.200 / 0.05'),
|
||||||
|
'--tw-prose-invert-pre-code': theme('colors.zinc.100'),
|
||||||
|
'--tw-prose-invert-pre-bg': 'rgb(0 0 0 / 0.4)',
|
||||||
|
'--tw-prose-invert-pre-border': theme('colors.zinc.200 / 0.1'),
|
||||||
|
'--tw-prose-invert-th-borders': theme('colors.zinc.700'),
|
||||||
|
'--tw-prose-invert-td-borders': theme('colors.zinc.800'),
|
||||||
|
|
||||||
|
// Base
|
||||||
|
color: 'var(--tw-prose-body)',
|
||||||
|
lineHeight: theme('lineHeight.7'),
|
||||||
|
'> *': {
|
||||||
|
marginTop: theme('spacing.10'),
|
||||||
|
marginBottom: theme('spacing.10'),
|
||||||
|
},
|
||||||
|
p: {
|
||||||
|
marginTop: theme('spacing.7'),
|
||||||
|
marginBottom: theme('spacing.7'),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Headings
|
||||||
|
'h2, h3': {
|
||||||
|
color: 'var(--tw-prose-headings)',
|
||||||
|
fontWeight: theme('fontWeight.semibold'),
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontSize: theme('fontSize.xl')[0],
|
||||||
|
lineHeight: theme('lineHeight.7'),
|
||||||
|
marginTop: theme('spacing.20'),
|
||||||
|
marginBottom: theme('spacing.4'),
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontSize: theme('fontSize.base')[0],
|
||||||
|
lineHeight: theme('lineHeight.7'),
|
||||||
|
marginTop: theme('spacing.16'),
|
||||||
|
marginBottom: theme('spacing.4'),
|
||||||
|
},
|
||||||
|
':is(h2, h3) + *': {
|
||||||
|
marginTop: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Images
|
||||||
|
img: {
|
||||||
|
borderRadius: theme('borderRadius.3xl'),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Inline elements
|
||||||
|
a: {
|
||||||
|
color: 'var(--tw-prose-links)',
|
||||||
|
fontWeight: theme('fontWeight.semibold'),
|
||||||
|
textDecoration: 'underline',
|
||||||
|
textDecorationColor: 'var(--tw-prose-underline)',
|
||||||
|
transitionProperty: 'color, text-decoration-color',
|
||||||
|
transitionDuration: theme('transitionDuration.150'),
|
||||||
|
transitionTimingFunction: theme('transitionTimingFunction.in-out'),
|
||||||
|
},
|
||||||
|
'a:hover': {
|
||||||
|
color: 'var(--tw-prose-links-hover)',
|
||||||
|
textDecorationColor: 'var(--tw-prose-underline-hover)',
|
||||||
|
},
|
||||||
|
strong: {
|
||||||
|
color: 'var(--tw-prose-bold)',
|
||||||
|
fontWeight: theme('fontWeight.semibold'),
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
display: 'inline-block',
|
||||||
|
color: 'var(--tw-prose-code)',
|
||||||
|
fontSize: theme('fontSize.sm')[0],
|
||||||
|
fontWeight: theme('fontWeight.semibold'),
|
||||||
|
backgroundColor: 'var(--tw-prose-code-bg)',
|
||||||
|
borderRadius: theme('borderRadius.lg'),
|
||||||
|
paddingLeft: theme('spacing.1'),
|
||||||
|
paddingRight: theme('spacing.1'),
|
||||||
|
},
|
||||||
|
'a code': {
|
||||||
|
color: 'inherit',
|
||||||
|
},
|
||||||
|
':is(h2, h3) code': {
|
||||||
|
fontWeight: theme('fontWeight.bold'),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Quotes
|
||||||
|
blockquote: {
|
||||||
|
paddingLeft: theme('spacing.6'),
|
||||||
|
borderLeftWidth: theme('borderWidth.2'),
|
||||||
|
borderLeftColor: 'var(--tw-prose-quote-borders)',
|
||||||
|
fontStyle: 'italic',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Figures
|
||||||
|
figcaption: {
|
||||||
|
color: 'var(--tw-prose-captions)',
|
||||||
|
fontSize: theme('fontSize.sm')[0],
|
||||||
|
lineHeight: theme('lineHeight.6'),
|
||||||
|
marginTop: theme('spacing.3'),
|
||||||
|
},
|
||||||
|
'figcaption > p': {
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Lists
|
||||||
|
ul: {
|
||||||
|
listStyleType: 'disc',
|
||||||
|
},
|
||||||
|
ol: {
|
||||||
|
listStyleType: 'decimal',
|
||||||
|
},
|
||||||
|
'ul, ol': {
|
||||||
|
paddingLeft: theme('spacing.6'),
|
||||||
|
},
|
||||||
|
li: {
|
||||||
|
marginTop: theme('spacing.6'),
|
||||||
|
marginBottom: theme('spacing.6'),
|
||||||
|
paddingLeft: theme('spacing[3.5]'),
|
||||||
|
},
|
||||||
|
'li::marker': {
|
||||||
|
fontSize: theme('fontSize.sm')[0],
|
||||||
|
fontWeight: theme('fontWeight.semibold'),
|
||||||
|
},
|
||||||
|
'ol > li::marker': {
|
||||||
|
color: 'var(--tw-prose-counters)',
|
||||||
|
},
|
||||||
|
'ul > li::marker': {
|
||||||
|
color: 'var(--tw-prose-bullets)',
|
||||||
|
},
|
||||||
|
'li :is(ol, ul)': {
|
||||||
|
marginTop: theme('spacing.4'),
|
||||||
|
marginBottom: theme('spacing.4'),
|
||||||
|
},
|
||||||
|
'li :is(li, p)': {
|
||||||
|
marginTop: theme('spacing.3'),
|
||||||
|
marginBottom: theme('spacing.3'),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Code blocks
|
||||||
|
pre: {
|
||||||
|
color: 'var(--tw-prose-pre-code)',
|
||||||
|
fontSize: theme('fontSize.sm')[0],
|
||||||
|
fontWeight: theme('fontWeight.medium'),
|
||||||
|
backgroundColor: 'var(--tw-prose-pre-bg)',
|
||||||
|
borderRadius: theme('borderRadius.3xl'),
|
||||||
|
padding: theme('spacing.8'),
|
||||||
|
overflowX: 'auto',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'var(--tw-prose-pre-border)',
|
||||||
|
},
|
||||||
|
'pre code': {
|
||||||
|
display: 'inline',
|
||||||
|
color: 'inherit',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
fontWeight: 'inherit',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
borderRadius: 0,
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Horizontal rules
|
||||||
|
hr: {
|
||||||
|
marginTop: theme('spacing.20'),
|
||||||
|
marginBottom: theme('spacing.20'),
|
||||||
|
borderTopWidth: '1px',
|
||||||
|
borderColor: 'var(--tw-prose-hr)',
|
||||||
|
'@screen lg': {
|
||||||
|
marginLeft: `calc(${theme('spacing.12')} * -1)`,
|
||||||
|
marginRight: `calc(${theme('spacing.12')} * -1)`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tables
|
||||||
|
table: {
|
||||||
|
width: '100%',
|
||||||
|
tableLayout: 'auto',
|
||||||
|
textAlign: 'left',
|
||||||
|
fontSize: theme('fontSize.sm')[0],
|
||||||
|
},
|
||||||
|
thead: {
|
||||||
|
borderBottomWidth: '1px',
|
||||||
|
borderBottomColor: 'var(--tw-prose-th-borders)',
|
||||||
|
},
|
||||||
|
'thead th': {
|
||||||
|
color: 'var(--tw-prose-headings)',
|
||||||
|
fontWeight: theme('fontWeight.semibold'),
|
||||||
|
verticalAlign: 'bottom',
|
||||||
|
paddingBottom: theme('spacing.2'),
|
||||||
|
},
|
||||||
|
'thead th:not(:first-child)': {
|
||||||
|
paddingLeft: theme('spacing.2'),
|
||||||
|
},
|
||||||
|
'thead th:not(:last-child)': {
|
||||||
|
paddingRight: theme('spacing.2'),
|
||||||
|
},
|
||||||
|
'tbody tr': {
|
||||||
|
borderBottomWidth: '1px',
|
||||||
|
borderBottomColor: 'var(--tw-prose-td-borders)',
|
||||||
|
},
|
||||||
|
'tbody tr:last-child': {
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
},
|
||||||
|
'tbody td': {
|
||||||
|
verticalAlign: 'baseline',
|
||||||
|
},
|
||||||
|
tfoot: {
|
||||||
|
borderTopWidth: '1px',
|
||||||
|
borderTopColor: 'var(--tw-prose-th-borders)',
|
||||||
|
},
|
||||||
|
'tfoot td': {
|
||||||
|
verticalAlign: 'top',
|
||||||
|
},
|
||||||
|
':is(tbody, tfoot) td': {
|
||||||
|
paddingTop: theme('spacing.2'),
|
||||||
|
paddingBottom: theme('spacing.2'),
|
||||||
|
},
|
||||||
|
':is(tbody, tfoot) td:not(:first-child)': {
|
||||||
|
paddingLeft: theme('spacing.2'),
|
||||||
|
},
|
||||||
|
':is(tbody, tfoot) td:not(:last-child)': {
|
||||||
|
paddingRight: theme('spacing.2'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
14
examples/alan-turing-portal/templates/dataset.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: string
|
||||||
|
link-to-publication: url
|
||||||
|
link-to-data: url
|
||||||
|
task-description: string
|
||||||
|
details-of-task: string
|
||||||
|
size-of-dataset: number
|
||||||
|
percentage-abusive: number
|
||||||
|
language: string
|
||||||
|
level-of-annotation: list eg: ["Posts", "Comments", ...]
|
||||||
|
platform: list eg: ["Youtube", "Facebook", ...]
|
||||||
|
medium: list eg: ["Text", "Emojis", "Images", ...]
|
||||||
|
reference: string
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: string
|
||||||
|
data-link: url
|
||||||
|
reference: string
|
||||||
|
---
|
||||||
28
examples/alan-turing-portal/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"incremental": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
36
examples/learn-example/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# 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*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
1
examples/learn-example/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
PortalJS Learn Example - https://portaljs.org/docs
|
||||||
21
examples/learn-example/components/DRD.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { MDXRemote } from 'next-mdx-remote';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import { Mermaid } from '@flowershow/core';
|
||||||
|
|
||||||
|
// Custom components/renderers to pass to MDX.
|
||||||
|
// Since the MDX files aren't loaded by webpack, they have no knowledge of how
|
||||||
|
// to handle import statements. Instead, you must include components in scope
|
||||||
|
// here.
|
||||||
|
const components = {
|
||||||
|
Table: dynamic(() => import('./Table')),
|
||||||
|
mermaid: Mermaid,
|
||||||
|
// Excel: dynamic(() => import('../components/Excel')),
|
||||||
|
// TODO: try and make these dynamic ...
|
||||||
|
Vega: dynamic(() => import('./Vega')),
|
||||||
|
VegaLite: dynamic(() => import('./VegaLite')),
|
||||||
|
LineChart: dynamic(() => import('./LineChart')),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
export default function DRD({ source }: { source: any }) {
|
||||||
|
return <MDXRemote {...source} components={components} />;
|
||||||
|
}
|
||||||
32
examples/learn-example/components/DebouncedInput.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const DebouncedInput = ({
|
||||||
|
value: initialValue,
|
||||||
|
onChange,
|
||||||
|
debounce = 500,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const [value, setValue] = useState(initialValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(initialValue);
|
||||||
|
}, [initialValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
onChange(value);
|
||||||
|
}, debounce);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DebouncedInput;
|
||||||
55
examples/learn-example/components/LineChart.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import VegaLite from "./VegaLite";
|
||||||
|
|
||||||
|
export default function LineChart({
|
||||||
|
data = [],
|
||||||
|
fullWidth = false,
|
||||||
|
title = "",
|
||||||
|
xAxis = "x",
|
||||||
|
yAxis = "y",
|
||||||
|
}) {
|
||||||
|
var tmp = data;
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
tmp = data.map((r, i) => {
|
||||||
|
return { x: r[0], y: r[1] };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const vegaData = { table: tmp };
|
||||||
|
const spec = {
|
||||||
|
$schema: "https://vega.github.io/schema/vega-lite/v5.json",
|
||||||
|
title,
|
||||||
|
width: "container",
|
||||||
|
height: 300,
|
||||||
|
mark: {
|
||||||
|
type: "line",
|
||||||
|
color: "black",
|
||||||
|
strokeWidth: 1,
|
||||||
|
tooltip: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
name: "table",
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
grid: {
|
||||||
|
type: "interval",
|
||||||
|
bind: "scales",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
encoding: {
|
||||||
|
x: {
|
||||||
|
field: xAxis,
|
||||||
|
timeUnit: "year",
|
||||||
|
type: "temporal",
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
field: yAxis,
|
||||||
|
type: "quantitative",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
spec.data = { "url": data } as any
|
||||||
|
return <VegaLite fullWidth={fullWidth} spec={spec} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <VegaLite fullWidth={fullWidth} data={vegaData} spec={spec} />;
|
||||||
|
}
|
||||||
189
examples/learn-example/components/Table.tsx
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import {
|
||||||
|
createColumnHelper,
|
||||||
|
FilterFn,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ArrowDownIcon,
|
||||||
|
ArrowUpIcon,
|
||||||
|
ChevronDoubleLeftIcon,
|
||||||
|
ChevronDoubleRightIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
} from "@heroicons/react/24/solid";
|
||||||
|
|
||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import parseCsv from "../lib/parseCsv";
|
||||||
|
import DebouncedInput from "./DebouncedInput";
|
||||||
|
import loadData from "../lib/loadData";
|
||||||
|
|
||||||
|
const Table = ({
|
||||||
|
data: ogData = [],
|
||||||
|
cols: ogCols = [],
|
||||||
|
csv = "",
|
||||||
|
url = "",
|
||||||
|
fullWidth = false,
|
||||||
|
}) => {
|
||||||
|
if (csv) {
|
||||||
|
const out = parseCsv(csv);
|
||||||
|
ogData = out.rows;
|
||||||
|
ogCols = out.fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [data, setData] = React.useState(ogData);
|
||||||
|
const [cols, setCols] = React.useState(ogCols);
|
||||||
|
const [error, setError] = React.useState(""); // TODO: add error handling
|
||||||
|
|
||||||
|
const tableCols = useMemo(() => {
|
||||||
|
const columnHelper = createColumnHelper();
|
||||||
|
return cols.map((c) =>
|
||||||
|
columnHelper.accessor(c.key, {
|
||||||
|
header: () => c.name,
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [data, cols]);
|
||||||
|
|
||||||
|
const [globalFilter, setGlobalFilter] = useState("");
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns: tableCols,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
state: {
|
||||||
|
globalFilter,
|
||||||
|
},
|
||||||
|
globalFilterFn: globalFilterFn,
|
||||||
|
onGlobalFilterChange: setGlobalFilter,
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (url) {
|
||||||
|
loadData(url).then((data) => {
|
||||||
|
const { rows, fields } = parseCsv(data);
|
||||||
|
setData(rows);
|
||||||
|
setCols(fields);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${fullWidth ? "w-[90vw] ml-[calc(50%-45vw)]" : "w-full"}`}>
|
||||||
|
<DebouncedInput
|
||||||
|
value={globalFilter ?? ""}
|
||||||
|
onChange={(value) => setGlobalFilter(String(value))}
|
||||||
|
className="p-2 text-sm shadow border border-block"
|
||||||
|
placeholder="Search all columns..."
|
||||||
|
/>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
{table.getHeaderGroups().map((hg) => (
|
||||||
|
<tr key={hg.id}>
|
||||||
|
{hg.headers.map((h) => (
|
||||||
|
<th key={h.id}>
|
||||||
|
<div
|
||||||
|
{...{
|
||||||
|
className: h.column.getCanSort()
|
||||||
|
? "cursor-pointer select-none"
|
||||||
|
: "",
|
||||||
|
onClick: h.column.getToggleSortingHandler(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{flexRender(h.column.columnDef.header, h.getContext())}
|
||||||
|
{{
|
||||||
|
asc: (
|
||||||
|
<ArrowUpIcon className="inline-block ml-2 h-4 w-4" />
|
||||||
|
),
|
||||||
|
desc: (
|
||||||
|
<ArrowDownIcon className="inline-block ml-2 h-4 w-4" />
|
||||||
|
),
|
||||||
|
}[h.column.getIsSorted() as string] ?? (
|
||||||
|
<div className="inline-block ml-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{table.getRowModel().rows.map((r) => (
|
||||||
|
<tr key={r.id}>
|
||||||
|
{r.getVisibleCells().map((c) => (
|
||||||
|
<td key={c.id}>
|
||||||
|
{flexRender(c.column.columnDef.cell, c.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div className="flex gap-2 items-center justify-center">
|
||||||
|
<button
|
||||||
|
className={`w-6 h-6 ${
|
||||||
|
!table.getCanPreviousPage() ? "opacity-25" : "opacity-100"
|
||||||
|
}`}
|
||||||
|
onClick={() => table.setPageIndex(0)}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<ChevronDoubleLeftIcon />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`w-6 h-6 ${
|
||||||
|
!table.getCanPreviousPage() ? "opacity-25" : "opacity-100"
|
||||||
|
}`}
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</button>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<div>Page</div>
|
||||||
|
<strong>
|
||||||
|
{table.getState().pagination.pageIndex + 1} of{" "}
|
||||||
|
{table.getPageCount()}
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className={`w-6 h-6 ${
|
||||||
|
!table.getCanNextPage() ? "opacity-25" : "opacity-100"
|
||||||
|
}`}
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`w-6 h-6 ${
|
||||||
|
!table.getCanNextPage() ? "opacity-25" : "opacity-100"
|
||||||
|
}`}
|
||||||
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<ChevronDoubleRightIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const globalFilterFn: FilterFn<any> = (row, columnId, filterValue: string) => {
|
||||||
|
const search = filterValue.toLowerCase();
|
||||||
|
|
||||||
|
let value = row.getValue(columnId) as string;
|
||||||
|
if (typeof value === "number") value = String(value);
|
||||||
|
|
||||||
|
return value?.toLowerCase().includes(search);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Table;
|
||||||
6
examples/learn-example/components/Vega.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// Wrapper for the Vega component
|
||||||
|
import { Vega as VegaOg } from "react-vega";
|
||||||
|
|
||||||
|
export default function Vega(props) {
|
||||||
|
return <VegaOg {...props} />;
|
||||||
|
}
|
||||||
9
examples/learn-example/components/VegaLite.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Wrapper for the Vega Lite component
|
||||||
|
import { VegaLite as VegaLiteOg } from "react-vega";
|
||||||
|
import applyFullWidthDirective from "../lib/applyFullWidthDirective";
|
||||||
|
|
||||||
|
export default function VegaLite(props) {
|
||||||
|
const Component = applyFullWidthDirective({ Component: VegaLiteOg });
|
||||||
|
|
||||||
|
return <Component {...props} />;
|
||||||
|
}
|
||||||
7
examples/learn-example/content/index.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# My Dataset
|
||||||
|
|
||||||
|
Built with PortalJS
|
||||||
|
|
||||||
|
## Table
|
||||||
|
|
||||||
|
<Table url="data.csv" />
|
||||||
21
examples/learn-example/lib/applyFullWidthDirective.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export default function applyFullWidthDirective({
|
||||||
|
Component,
|
||||||
|
defaultWFull = true,
|
||||||
|
}) {
|
||||||
|
return (props) => {
|
||||||
|
const newProps = { ...props };
|
||||||
|
|
||||||
|
let newClassName = newProps.className || "";
|
||||||
|
if (newProps.fullWidth === true) {
|
||||||
|
newClassName += " w-[90vw] ml-[calc(50%-45vw)] max-w-none";
|
||||||
|
} else if (defaultWFull) {
|
||||||
|
// So that charts and tables will have the
|
||||||
|
// same width as the text content, but images
|
||||||
|
// can have its width set using the width prop
|
||||||
|
newClassName += " w-full";
|
||||||
|
}
|
||||||
|
newProps.className = newClassName;
|
||||||
|
|
||||||
|
return <Component {...newProps} />;
|
||||||
|
};
|
||||||
|
}
|
||||||
5
examples/learn-example/lib/loadData.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default async function loadData(url: string) {
|
||||||
|
const response = await fetch(url)
|
||||||
|
const data = await response.text()
|
||||||
|
return data
|
||||||
|
}
|
||||||
105
examples/learn-example/lib/markdown.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import matter from "gray-matter";
|
||||||
|
import mdxmermaid from "mdx-mermaid";
|
||||||
|
import { h } from "hastscript";
|
||||||
|
import remarkCallouts from "@flowershow/remark-callouts";
|
||||||
|
import remarkEmbed from "@flowershow/remark-embed";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import remarkMath from "remark-math";
|
||||||
|
import remarkSmartypants from "remark-smartypants";
|
||||||
|
import remarkToc from "remark-toc";
|
||||||
|
import remarkWikiLink from "@flowershow/remark-wiki-link";
|
||||||
|
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||||
|
import rehypeKatex from "rehype-katex";
|
||||||
|
import rehypeSlug from "rehype-slug";
|
||||||
|
import rehypePrismPlus from "rehype-prism-plus";
|
||||||
|
|
||||||
|
import { serialize } from "next-mdx-remote/serialize";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a markdown or MDX file to an MDX source form + front matter data
|
||||||
|
*
|
||||||
|
* @source: the contents of a markdown or mdx file
|
||||||
|
* @format: used to indicate to next-mdx-remote which format to use (md or mdx)
|
||||||
|
* @returns: { mdxSource: mdxSource, frontMatter: ...}
|
||||||
|
*/
|
||||||
|
const parse = async function (source, format) {
|
||||||
|
const { content, data, excerpt } = matter(source, {
|
||||||
|
excerpt: (file, options) => {
|
||||||
|
// Generate an excerpt for the file
|
||||||
|
file.excerpt = file.content.split("\n\n")[0];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mdxSource = await serialize(
|
||||||
|
{ value: content, path: format },
|
||||||
|
{
|
||||||
|
// Optionally pass remark/rehype plugins
|
||||||
|
mdxOptions: {
|
||||||
|
remarkPlugins: [
|
||||||
|
remarkEmbed,
|
||||||
|
remarkGfm,
|
||||||
|
[remarkSmartypants, { quotes: false, dashes: "oldschool" }],
|
||||||
|
remarkMath,
|
||||||
|
remarkCallouts,
|
||||||
|
remarkWikiLink,
|
||||||
|
[
|
||||||
|
remarkToc,
|
||||||
|
{
|
||||||
|
heading: "Table of contents",
|
||||||
|
tight: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[mdxmermaid, {}],
|
||||||
|
],
|
||||||
|
rehypePlugins: [
|
||||||
|
rehypeSlug,
|
||||||
|
[
|
||||||
|
rehypeAutolinkHeadings,
|
||||||
|
{
|
||||||
|
properties: { className: 'heading-link' },
|
||||||
|
test(element) {
|
||||||
|
return (
|
||||||
|
["h2", "h3", "h4", "h5", "h6"].includes(element.tagName) &&
|
||||||
|
element.properties?.id !== "table-of-contents" &&
|
||||||
|
element.properties?.className !== "blockquote-heading"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
content() {
|
||||||
|
return [
|
||||||
|
h(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
xmlns: "http:www.w3.org/2000/svg",
|
||||||
|
fill: "#ab2b65",
|
||||||
|
viewBox: "0 0 20 20",
|
||||||
|
className: "w-5 h-5",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h("path", {
|
||||||
|
fillRule: "evenodd",
|
||||||
|
clipRule: "evenodd",
|
||||||
|
d: "M9.493 2.853a.75.75 0 00-1.486-.205L7.545 6H4.198a.75.75 0 000 1.5h3.14l-.69 5H3.302a.75.75 0 000 1.5h3.14l-.435 3.148a.75.75 0 001.486.205L7.955 14h2.986l-.434 3.148a.75.75 0 001.486.205L12.456 14h3.346a.75.75 0 000-1.5h-3.14l.69-5h3.346a.75.75 0 000-1.5h-3.14l.435-3.147a.75.75 0 00-1.486-.205L12.045 6H9.059l.434-3.147zM8.852 7.5l-.69 5h2.986l.69-5H8.852z",
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[rehypeKatex, { output: "mathml" }],
|
||||||
|
[rehypePrismPlus, { ignoreMissing: true }],
|
||||||
|
],
|
||||||
|
format,
|
||||||
|
},
|
||||||
|
scope: data,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mdxSource: mdxSource,
|
||||||
|
frontMatter: data,
|
||||||
|
excerpt,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default parse;
|
||||||
16
examples/learn-example/lib/parseCsv.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import papa from "papaparse";
|
||||||
|
|
||||||
|
const parseCsv = (csv) => {
|
||||||
|
csv = csv.trim();
|
||||||
|
const rawdata = papa.parse(csv, { header: true });
|
||||||
|
const cols = rawdata.meta.fields.map((r, i) => {
|
||||||
|
return { key: r, name: r };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows: rawdata.data,
|
||||||
|
fields: cols,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default parseCsv;
|
||||||
10959
examples/learn-example/package-lock.json
generated
Normal file
50
examples/learn-example/package.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "basic-example",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"export": "npm run build && next export -o out"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@flowershow/core": "^0.4.10",
|
||||||
|
"@flowershow/remark-callouts": "^1.0.0",
|
||||||
|
"@flowershow/remark-embed": "^1.0.0",
|
||||||
|
"@flowershow/remark-wiki-link": "^1.1.2",
|
||||||
|
"@heroicons/react": "^2.0.17",
|
||||||
|
"@opentelemetry/api": "^1.4.0",
|
||||||
|
"@tanstack/react-table": "^8.8.5",
|
||||||
|
"@types/node": "18.16.0",
|
||||||
|
"@types/react": "18.2.0",
|
||||||
|
"@types/react-dom": "18.2.0",
|
||||||
|
"eslint": "8.39.0",
|
||||||
|
"eslint-config-next": "13.3.1",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
|
"hastscript": "^7.2.0",
|
||||||
|
"mdx-mermaid": "2.0.0-rc7",
|
||||||
|
"next": "13.2.1",
|
||||||
|
"next-mdx-remote": "^4.4.1",
|
||||||
|
"papaparse": "^5.4.1",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"react-vega": "^7.6.0",
|
||||||
|
"rehype-autolink-headings": "^6.1.1",
|
||||||
|
"rehype-katex": "^6.0.3",
|
||||||
|
"rehype-prism-plus": "^1.5.1",
|
||||||
|
"rehype-slug": "^5.1.0",
|
||||||
|
"remark-gfm": "^3.0.1",
|
||||||
|
"remark-math": "^5.1.1",
|
||||||
|
"remark-smartypants": "^2.0.0",
|
||||||
|
"remark-toc": "^8.0.1",
|
||||||
|
"typescript": "5.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"postcss": "^8.4.23",
|
||||||
|
"tailwindcss": "^3.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
60
examples/learn-example/pages/[[...path]].tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import parse from '../lib/markdown';
|
||||||
|
import DRD from '../components/DRD';
|
||||||
|
|
||||||
|
export const getStaticPaths = async () => {
|
||||||
|
const contentDir = path.join(process.cwd(), '/content/');
|
||||||
|
const contentFolders = await fs.readdir(contentDir, 'utf8');
|
||||||
|
const paths = contentFolders.map((folder: string) =>
|
||||||
|
folder === 'index.md'
|
||||||
|
? { params: { path: [] } }
|
||||||
|
: { params: { path: [folder] } }
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
fallback: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStaticProps = async (context) => {
|
||||||
|
let pathToFile = 'index.md';
|
||||||
|
if (context.params.path) {
|
||||||
|
pathToFile = context.params.path.join('/') + '/index.md';
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexFile = path.join(process.cwd(), '/content/' + pathToFile);
|
||||||
|
const readme = await fs.readFile(indexFile, 'utf8');
|
||||||
|
let { mdxSource, frontMatter } = await parse(readme, '.mdx');
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
mdxSource,
|
||||||
|
frontMatter,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DatasetPage({ mdxSource, frontMatter }) {
|
||||||
|
return (
|
||||||
|
<div className="prose dark:prose-invert mx-auto">
|
||||||
|
<header>
|
||||||
|
<div className="mb-6">
|
||||||
|
<>
|
||||||
|
<h1>{frontMatter.title}</h1>
|
||||||
|
{frontMatter.author && (
|
||||||
|
<div className="-mt-6">
|
||||||
|
<p className="opacity-60 pl-1">{frontMatter.author}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{frontMatter.description && (
|
||||||
|
<p className="description">{frontMatter.description}</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<DRD source={mdxSource} />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
examples/learn-example/pages/_app.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import '../styles/globals.css'
|
||||||
|
import type { AppProps } from 'next/app'
|
||||||
|
|
||||||
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
|
return <Component {...pageProps} />
|
||||||
|
}
|
||||||
19
examples/learn-example/pages/_document.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import Document, { Html, Main, Head, NextScript } from 'next/document';
|
||||||
|
|
||||||
|
class MyDocument extends Document {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head>
|
||||||
|
<link rel="icon" href="/favicon.png" />
|
||||||
|
</Head>
|
||||||
|
<body className='bg-white dark:bg-gray-900'>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MyDocument;
|
||||||
6
examples/learn-example/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
6
examples/learn-example/public/data.csv
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Year,Rating
|
||||||
|
2008,86
|
||||||
|
2009,96
|
||||||
|
2010,100
|
||||||
|
2011,100
|
||||||
|
2012,97
|
||||||
|
BIN
examples/learn-example/public/favicon.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
105
examples/learn-example/styles/globals.css
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
@import "@flowershow/remark-callouts/styles.css";
|
||||||
|
|
||||||
|
.w-5 {
|
||||||
|
width: 1.25rem
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-5 {
|
||||||
|
height: 1.25rem
|
||||||
|
}
|
||||||
|
|
||||||
|
/* mathjax */
|
||||||
|
.math-inline > mjx-container > svg {
|
||||||
|
display: inline;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* smooth scrolling in modern browsers */
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tooltip fade-out clip */
|
||||||
|
.tooltip-body::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 3.6rem; /* multiple of $line-height used on the tooltip body (defined in tooltipBodyStyle) */
|
||||||
|
height: 1.2rem; /* ($top + $height)/$line-height is the number of lines we want to clip tooltip text at*/
|
||||||
|
width: 10rem;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(255, 255, 255, 0),
|
||||||
|
rgba(255, 255, 255, 1) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(h2, h3, h4, h5, h6):not(.blogitem-title) {
|
||||||
|
margin-left: -2rem !important;
|
||||||
|
padding-left: 2rem !important;
|
||||||
|
scroll-margin-top: 4.5rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-link {
|
||||||
|
padding: 1px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
margin: auto 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #1e293b;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light .heading-link {
|
||||||
|
/* border: 1px solid #ab2b65; */
|
||||||
|
/* background: none; */
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(h2, h3, h4, h5, h6):not(.blogitem-title):hover .heading-link {
|
||||||
|
opacity: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-link svg {
|
||||||
|
transform: scale(0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 640px) {
|
||||||
|
.heading-link {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||||
|
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
html {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
color: white;
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
examples/learn-example/tailwind.config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [require('@tailwindcss/typography')],
|
||||||
|
};
|
||||||
20
examples/learn-example/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "middleware.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
22270
package-lock.json
generated
@@ -19,6 +19,7 @@ const items = [
|
|||||||
href: 'https://opendata.fcsc.gov.ae/',
|
href: 'https://opendata.fcsc.gov.ae/',
|
||||||
image: '/images/showcases/uae.png',
|
image: '/images/showcases/uae.png',
|
||||||
description: 'Government Open Data Portal',
|
description: 'Government Open Data Portal',
|
||||||
|
sourceUrl: 'https://github.com/FCSCOpendata/frontend',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Brazil Open Data',
|
title: 'Brazil Open Data',
|
||||||
@@ -37,12 +38,18 @@ const items = [
|
|||||||
href: 'https://example.portaljs.org/',
|
href: 'https://example.portaljs.org/',
|
||||||
image: '/images/showcases/example-simple-catalog.png',
|
image: '/images/showcases/example-simple-catalog.png',
|
||||||
description: 'Simple data catalog',
|
description: 'Simple data catalog',
|
||||||
|
sourceUrl:
|
||||||
|
'https://github.com/datopian/portaljs/tree/main/examples/simple-example',
|
||||||
|
docsUrl: '/docs/example-data-catalog',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Example: Portal with CKAN',
|
title: 'Example: Portal with CKAN',
|
||||||
href: 'https://ckan-example.portaljs.org/',
|
href: 'https://ckan-example.portaljs.org/',
|
||||||
image: '/images/showcases/example-ckan.png',
|
image: '/images/showcases/example-ckan.png',
|
||||||
description: 'Simple portal with data coming from CKAN',
|
description: 'Simple portal with data coming from CKAN',
|
||||||
|
sourceUrl:
|
||||||
|
'https://github.com/datopian/portaljs/tree/main/examples/ckan-example',
|
||||||
|
docsUrl: '/docs/example-ckan',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,65 @@
|
|||||||
|
const IconBeaker = () => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15a9.065 9.065 0 00-6.23-.693L5 14.5m14.8.8l1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0112 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const IconDocs = () => (
|
||||||
|
<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="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const IconCode = () => (
|
||||||
|
<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="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ActionButton = ({ title, Icon, href, className = '' }) => (
|
||||||
|
<a
|
||||||
|
title={title}
|
||||||
|
target="_blank"
|
||||||
|
href={href}
|
||||||
|
className={`rounded-full p-2 hover:bg-secondary transition-all duration-250 ${className}`}
|
||||||
|
>
|
||||||
|
<Icon />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
|
||||||
export default function GalleryItem({ item }) {
|
export default function GalleryItem({ item }) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
@@ -16,6 +78,26 @@ export default function GalleryItem({ item }) {
|
|||||||
<div className="text-center text-primary-dark">
|
<div className="text-center text-primary-dark">
|
||||||
<span className="text-xl font-semibold">{item.title}</span>
|
<span className="text-xl font-semibold">{item.title}</span>
|
||||||
<p className="text-base font-medium">{item.description}</p>
|
<p className="text-base font-medium">{item.description}</p>
|
||||||
|
<div className="flex justify-center mt-2">
|
||||||
|
<ActionButton Icon={IconBeaker} title="Demo" href={item.href} />
|
||||||
|
{item.docsUrl && (
|
||||||
|
<ActionButton
|
||||||
|
Icon={IconDocs}
|
||||||
|
title="Documentation"
|
||||||
|
href={item.docsUrl}
|
||||||
|
className="mx-5"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{item.sourceUrl && (
|
||||||
|
<ActionButton
|
||||||
|
Icon={IconCode}
|
||||||
|
title="Source code"
|
||||||
|
href={item.sourceUrl}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Maybe: Blog post */}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
BIN
site/content/assets/docs/datasets-index-page.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
site/content/assets/docs/editing-the-page-1.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
site/content/assets/docs/my-incredible-dataset.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
site/content/assets/docs/tutorial-1-img-1.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
site/content/assets/docs/tutorial-1-img-2.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
site/content/assets/docs/tutorial-1-img-3.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
site/content/assets/docs/tutorial-1-img-4.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
site/content/assets/docs/tutorial-1-img-5.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
site/content/assets/docs/tutorial-1-img-6.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
site/content/assets/examples/basic-example.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
@@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Example: data catalog with data coming from CKAN"
|
|
||||||
authors: ['Luccas Mateus']
|
|
||||||
date: 2023-04-20
|
|
||||||
---
|
|
||||||
|
|
||||||
PortalJS is an open source project that aims to simplify the creation of web-based data portals, making it easy for users to create and share data-driven applications.
|
|
||||||
|
|
||||||
The ckan-example added to PortalJS is intended to provide users with an easy way to set up a data catalog that can be used to display and share data stores behind a CKAN Backend. With this example, users can quickly set up a web-based portal that allows them to showcase their data and make it accessible to others, all this being done just by adding a simple env variable pointing to a CKAN Deployment.
|
|
||||||
|
|
||||||
To get a feel of the project, users can check the [live deployment](https://ckan-example.portaljs.org).
|
|
||||||
|
|
||||||
Below are some screenshots:
|
|
||||||
|
|
||||||
### Front page
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Individual dataset page
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- [Documentation](/docs/example-ckan)
|
|
||||||
- [Repo](https://github.com/datopian/portaljs/tree/main/examples/ckan-example)
|
|
||||||
- [Live Demo](https://ckan-example.portaljs.org)
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Example: simple data catalog"
|
|
||||||
authors: ['Luccas Mateus']
|
|
||||||
date: 2023-04-20
|
|
||||||
---
|
|
||||||
|
|
||||||
PortalJS is an open source project that aims to simplify the creation of web-based data portals, making it easy for users to create and share data-driven applications.
|
|
||||||
|
|
||||||
The simple-example added to PortalJS is intended to provide users with an easy way to set up a data catalog that can be used to display and share data stores stored in GitHub repositories. With this example, users can quickly set up a web-based portal that allows them to showcase their data and make it accessible to others, all this being done thru the configuration of a simple `datasets.json` file.
|
|
||||||
|
|
||||||
To get a feel of the project, users can check the [live deployment](https://example.portaljs.org).
|
|
||||||
|
|
||||||
Below are some screenshots:
|
|
||||||
|
|
||||||
### Front page
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Individual dataset page
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- [Documentation](/docs/example-data-catalog)
|
|
||||||
- [Repo](https://github.com/datopian/portaljs/tree/main/examples/simple-example)
|
|
||||||
- [Live Demo](https://example.portaljs.org)
|
|
||||||
@@ -50,7 +50,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
github: "https://github.com/datopian/portaljs",
|
github: "https://github.com/datopian/portaljs",
|
||||||
discord: "https://discord.gg/An7Bu5x8",
|
discord: "https://discord.gg/EeyfGrGu4U",
|
||||||
tableOfContents: true,
|
tableOfContents: true,
|
||||||
// analytics: "xxxxxx",
|
// analytics: "xxxxxx",
|
||||||
// editLinkShow: true,
|
// editLinkShow: true,
|
||||||
|
|||||||
249
site/content/docs/dms/authentication.md
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
# Authentication
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
The core function of authentication is to **Identify** Users of the Portal (in a federated way) so we can base access on their identity.
|
||||||
|
|
||||||
|
There are 3 major conceptual components: Identity, Accounts and Sessions which come together in the following stages:
|
||||||
|
|
||||||
|
* **Root Identity Determination:** Determine Identity often via Delegation
|
||||||
|
* **Sessions:** Persistence of the identity in the web application in a secure way (without new identity determination on each request! I don't want to have to login via third party service every time)
|
||||||
|
* **Account (aka profile):** Storing Related Account/Profile Information in our application (not in third party identity) eg. email, name (other preferences)
|
||||||
|
* This will get auto-created usually at first Identification
|
||||||
|
* In limited case this can be seen as a cache of info from Identity system (e.g. your email)
|
||||||
|
* However often richer info that is app specific that is generated (relevant for personalization)
|
||||||
|
|
||||||
|
### Root Identity Determination options :key:
|
||||||
|
|
||||||
|
The identity determination can be done in multiple ways. In this article we're considering following 3 options that we believe are widely used:
|
||||||
|
|
||||||
|
- Password authentication - traditional username and password pair
|
||||||
|
- Single Sign-on (SSO) via protocols such as OAuth, SAML, OpenID Connect
|
||||||
|
- One-time password (OTP) via email or SMS (aka passwordless connection)
|
||||||
|
|
||||||
|
#### Password authentication
|
||||||
|
|
||||||
|
Traditional way of authentication of users. When signing up user provides at least username and password pair which is then stored in a database for future authentication processes. Normally, additional information such as email address, full name etc. is also requested when registering.
|
||||||
|
|
||||||
|
Examples of password authentication in popular services:
|
||||||
|
|
||||||
|
- GitHub - https://github.com/join
|
||||||
|
- GitLab - https://gitlab.com/users/sign_up
|
||||||
|
- NPM - https://www.npmjs.com/signup
|
||||||
|
|
||||||
|
#### Single Sign-on (SSO)
|
||||||
|
|
||||||
|
The way of delegating identity determination process to some third-party service. Normally, popular social network services are used, e.g., Google, Facebook, Twitter etc. SSO implementations can be done using OAuth or SAML protocols. In addition, there is OpenID Connect protocol which is an extension of OAuth2.0.
|
||||||
|
|
||||||
|
- OAuth
|
||||||
|
- JWT based
|
||||||
|
- JSON based
|
||||||
|
- 'webby'
|
||||||
|
- SAML
|
||||||
|
- XML based
|
||||||
|
- SOAP based
|
||||||
|
- 'enterprisey'
|
||||||
|
|
||||||
|
List of OAuth providers:
|
||||||
|
|
||||||
|
https://en.wikipedia.org/wiki/List_of_OAuth_providers
|
||||||
|
|
||||||
|
Examples of SSO in popular projects:
|
||||||
|
|
||||||
|
- https://datahub.io/login
|
||||||
|
- https://vercel.com/signup
|
||||||
|
|
||||||
|
#### One-time password (OTP)
|
||||||
|
|
||||||
|
Also known as dynamic password, OTP also solves limitations of traditional password authentication method. Usually, the one time passwords are received via email or SMS.
|
||||||
|
|
||||||
|
### Account (aka profile)
|
||||||
|
|
||||||
|
- Storage of user profile information (email, fullname, gravatar etc.)
|
||||||
|
- Retrieving user profile information via API
|
||||||
|
- Updating profile
|
||||||
|
- Deleting profile
|
||||||
|
|
||||||
|
### Sessions
|
||||||
|
|
||||||
|
- Log out: DePersisting the Session
|
||||||
|
- Invalidating all Sessions: e.g. if a security issue
|
||||||
|
- Sessions outside of browsers
|
||||||
|
|
||||||
|
## Key Job Stories
|
||||||
|
|
||||||
|
When a user signs in, I want to know her/his identity so that I can limit access and editing based on who she/he is.
|
||||||
|
|
||||||
|
When a user visits the data portal for the first time, I want to provide him/her a way to register easily/quickly so that more people uses the data portal.
|
||||||
|
|
||||||
|
When I visit the data portal for the first time, I want to sign up using my existing social network account so that I don't need to remember yet another credentials.
|
||||||
|
|
||||||
|
When I'm using the CLI app (or anything else outside browser), I want to be able to login so that I can work from the terminal (e.g., have write access: editing datasets etc.).
|
||||||
|
|
||||||
|
[More job stories](#more-job-stories).
|
||||||
|
|
||||||
|
## CKAN 2 (CKAN Classic)
|
||||||
|
|
||||||
|
### Basic CKAN authentication
|
||||||
|
|
||||||
|
In classic system, we have basic CKAN authentication. Below is how registration page looks like:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Registration flow in CKAN Classic:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
|
||||||
|
user->>ckan: fill in the form and submit
|
||||||
|
ckan->>ckan: check access (if user can create user)
|
||||||
|
ckan->>ckan: parse params
|
||||||
|
ckan->>ckan: check recaptcha
|
||||||
|
ckan->>ckan: call 'user_create' action
|
||||||
|
ckan->>ckan.model: add a new user into db
|
||||||
|
ckan->>ckan: create an activity
|
||||||
|
ckan->>ckan: log the user
|
||||||
|
ckan->>user: redirect to dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
We can extend basic CKAN authentication with:
|
||||||
|
|
||||||
|
- LDAP
|
||||||
|
- https://extensions.ckan.org/extension/ldap/
|
||||||
|
- https://github.com/NaturalHistoryMuseum/ckanext-ldap
|
||||||
|
- OAuth - see below
|
||||||
|
- SAML - https://extensions.ckan.org/extension/saml2/
|
||||||
|
|
||||||
|
### CKAN Classic as OAuth client
|
||||||
|
|
||||||
|
CKAN Classic can also be used as OAuth client:
|
||||||
|
|
||||||
|
- https://github.com/conwetlab/ckanext-oauth2 - this is the only one that's maintained.
|
||||||
|
- https://github.com/etalab/ckanext-oauth2 - outdated, the one above is based on this.
|
||||||
|
- https://github.com/okfn/ckanext-oauth - last commit 9 years ago.
|
||||||
|
- https://github.com/ckan/ckanext-oauth2waad - Windows Azure Active Directory specific and outdated.
|
||||||
|
|
||||||
|
How it works:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
|
||||||
|
user->>ckan: request for login via OAuth provider
|
||||||
|
ckan->>ckan.oauth: raise 401 and call `challenge` function
|
||||||
|
ckan.oauth->>user: redirect the user to the 3rd party log in page
|
||||||
|
user->>3rdparty: perform login
|
||||||
|
3rdparty->>ckan.oauth: redirect to /oauth2/callback with token
|
||||||
|
ckan.oauth->>3rdparty: call `authenticate` with token
|
||||||
|
3rdparty->>ckan.oauth: return user info
|
||||||
|
ckan.oauth->>ckan: if doesn't exist save that info in db or update it
|
||||||
|
ckan.oauth->>ckan.oauth: add cookies
|
||||||
|
ckan.oauth->>user: redirect to dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
## CKAN 3 (Next Gen)
|
||||||
|
|
||||||
|
We have considered some of popular and/or modern solutions for identity management that we can implement in CKAN 3:
|
||||||
|
|
||||||
|
https://docs.google.com/spreadsheets/d/1qXZyzAbA2NtpnoSZRJ2K_EbaWJnvxkrKVzQ_2rD5eQw/edit#gid=0
|
||||||
|
|
||||||
|
Shortlist based on scores from the spreadsheet above:
|
||||||
|
|
||||||
|
- Auth0
|
||||||
|
- AuthN
|
||||||
|
- Ory/Kratos
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
|
||||||
|
All projects from the shortlist can be considered for a project. It worth to give a try for each of them and find out what works best for your project's needs. Testing out Auth0 should be straightforward and take less than an hour. AuthN and Ory/Kratos would require to build docker images and to run it locally but overall it should not be time consuming.
|
||||||
|
|
||||||
|
### Existing work
|
||||||
|
|
||||||
|
In datahub.io we have implemented SSO via Google/Github. Below is sequence diagram showing the auth flow with datopian/auth + frontend express app (similar to CKAN 3 frontend):
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
|
||||||
|
frontend.login->>auth.authenticate: authenticate(jwt=None,next=/success/...)
|
||||||
|
auth.authenticate->>frontend.login: failed + here are urls for logging on 3rd party including success
|
||||||
|
frontend.login->>user: login form with login urls to 3rd party including next url in state
|
||||||
|
user->>3rdparty: login
|
||||||
|
3rdparty->>auth.oauth_response: success
|
||||||
|
auth.oauth_response->>frontend.success: redirect to next url
|
||||||
|
frontend.success->>auth.authenticate: with valid jwt
|
||||||
|
auth.authenticate->>frontend.success: valid + here is profile
|
||||||
|
frontend.success->>frontend.success: decode jwt, check it, then see localstorage
|
||||||
|
frontend.success->>frontend.dashboard: redirect to dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
## CKAN 2 to CKAN 3 (aka Next Gen)
|
||||||
|
|
||||||
|
How does this conceptual framework map to an evolution of CKAN 2 to CKAN 3?
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
|
||||||
|
subgraph "CKAN Classic"
|
||||||
|
Signup["Classic signup, e.g., self-service or by sysadmin"]
|
||||||
|
Login["Classic login if you're using the classic UI"]
|
||||||
|
OAuth["OAuth2(ORY/Hydra)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Authentication service (ORY/Kratos)"
|
||||||
|
SSO["Social Sign-On: Github, Google, Facebook"]
|
||||||
|
CC["CKAN Classic"]
|
||||||
|
Admins["Sysadmin users"]
|
||||||
|
Curators["Data curators"]
|
||||||
|
Users["Regular users"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Frontend v3"
|
||||||
|
SignupFront["Signup via Kratos"]
|
||||||
|
LoginFront["Login via Kratos"]
|
||||||
|
end
|
||||||
|
|
||||||
|
SignupFront --"Regular user"--> SSO
|
||||||
|
LoginFront --"Regular user"--> SSO
|
||||||
|
|
||||||
|
LoginFront --"Data curator"--> CC
|
||||||
|
|
||||||
|
CC --> Admins
|
||||||
|
CC --> Curators
|
||||||
|
SSO --> Users
|
||||||
|
|
||||||
|
CC --"Redirect"--> OAuth
|
||||||
|
OAuth --> Login
|
||||||
|
```
|
||||||
|
|
||||||
|
Sequence diagram of login process:
|
||||||
|
|
||||||
|
[](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG5cdEJyb3dzZXItPj5Gcm9udGVuZDogUmVxdWVzdCB0byBgL2F1dGgvbG9naW5gXG4gIEZyb250ZW5kLT4-S3JhdG9zOiBBdXRoIHJlcXVlc3RcbiAgS3JhdG9zLT4-QnJvd3NlcjogUmVkaXJlY3QgdG8gYC9hdXRoL2xvZ2luP3JlcXVlc3Q9e2lkfWAgcGFyYW1cbiAgQnJvd3Nlci0-PkZyb250ZW5kOiBHZXQgYC9hdXRoL2xvZ2luP3JlcXVlc3Q9e2lkfWBcbiAgRnJvbnRlbmQtPj5LcmF0b3M6IEZldGNoIGRhdGEgZm9yIHJlbmRlcmluZyB0aGUgZm9ybVxuICBLcmF0b3MtPj5Gcm9udGVuZDogTG9naW4gb3B0aW9uc1xuICBGcm9udGVuZC0-PkJyb3dzZXI6IFJlbmRlciB0aGUgbG9naW4gZm9ybSB3aXRoIGF2YWlsYWJsZSBvcHRpb25zXG4gIEJyb3dzZXItPj5Gcm9udGVuZDogU3VwcGx5IGZvcm0gZGF0YVxuICBGcm9udGVuZC0-PktyYXRvczogVmFsaWRhdGUgYW5kIGxvZ2luXG4gIEtyYXRvcy0-PkZyb250ZW5kOiBTZXQgc2Vzc2lvblxuICBGcm9udGVuZC0-PkJyb3dzZXI6IFJlZGlyZWN0IHRvIC9kYXNoYm9hcmRcblxuXG5cdFx0XHRcdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)
|
||||||
|
|
||||||
|
From ORY/Kratos:
|
||||||
|
|
||||||
|
[](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEIgYXMgQnJvd3NlclxuICBwYXJ0aWNpcGFudCBLIGFzIE9SWSBLcmF0b3NcbiAgcGFydGljaXBhbnQgQSBhcyBZb3VyIEFwcGxpY2F0aW9uXG5cblxuICBCLT4-SzogSW5pdGlhdGUgTG9naW5cbiAgSy0-PkI6IFJlZGlyZWN0cyB0byB5b3VyIEFwcGxpY2F0aW9uJ3MgL2xvZ2luIGVuZHBvaW50XG4gIEItPj5BOiBDYWxscyAvbG9naW5cbiAgQS0tPj5LOiBGZXRjaGVzIGRhdGEgdG8gcmVuZGVyIGZvcm1zIGV0Y1xuICBCLS0-PkE6IEZpbGxzIG91dCBmb3JtcywgY2xpY2tzIGUuZy4gXCJTdWJtaXQgTG9naW5cIlxuICBCLT4-SzogUE9TVHMgZGF0YSB0b1xuICBLLS0-Pks6IFByb2Nlc3NlcyBMb2dpbiBJbmZvXG5cbiAgYWx0IExvZ2luIGRhdGEgdmFsaWRcbiAgICBLLS0-PkI6IFNldHMgc2Vzc2lvbiBjb29raWVcbiAgICBLLT4-QjogUmVkaXJlY3RzIHRvIGUuZy4gRGFzaGJvYXJkXG4gIGVsc2UgTG9naW4gZGF0YSBpbnZhbGlkXG4gICAgSy0tPj5COiBSZWRpcmVjdHMgdG8geW91ciBBcHBsaWNhaXRvbidzIC9sb2dpbiBlbmRwb2ludFxuICAgIEItPj5BOiBDYWxscyAvbG9naW5cbiAgICBBLS0-Pks6IEZldGNoZXMgZGF0YSB0byByZW5kZXIgZm9ybSBmaWVsZHMgYW5kIGVycm9yc1xuICAgIEItLT4-QTogRmlsbHMgb3V0IGZvcm1zIGFnYWluLCBjb3JyZWN0cyBlcnJvcnNcbiAgICBCLT4-SzogUE9TVHMgZGF0YSBhZ2FpbiAtIGFuZCBzbyBvbi4uLlxuICBlbmRcbiIsIm1lcm1haWQiOnsidGhlbWUiOiJuZXV0cmFsIiwic2VxdWVuY2VEaWFncmFtIjp7ImRpYWdyYW1NYXJnaW5YIjoxNSwiZGlhZ3JhbU1hcmdpblkiOjE1LCJib3hUZXh0TWFyZ2luIjowLCJub3RlTWFyZ2luIjoxNSwibWVzc2FnZU1hcmdpbiI6NDUsIm1pcnJvckFjdG9ycyI6dHJ1ZX19fQ)
|
||||||
|
|
||||||
|
|
||||||
|
Kratos to Hydra in CKAN Classic:
|
||||||
|
|
||||||
|
WIP
|
||||||
|
|
||||||
|
Questions
|
||||||
|
|
||||||
|
* Does CKAN Classic allow us to store arbitrary account information (are there "extras")
|
||||||
|
* How would we avoid having to support identity persistence, delegation etc in both NG frontend and Classic Admin UI?
|
||||||
|
* Can we share cookies (e.g. via using subdomains)
|
||||||
|
* How is login, identity determination etc done at least for frontend in DataHub.io
|
||||||
|
* Should account UI really be in NG frontend vs Classic Admin UI?
|
||||||
|
* how can we handle "invite a user" to my org set up ... (it's basically post processing after sign up ...)
|
||||||
|
|
||||||
|
## Appendix
|
||||||
|
|
||||||
|
### More job stories
|
||||||
|
|
||||||
|
When a user visits the data portal, I want to provide multiple options for him/her to sign up so that I have more users registered and using the data portal.
|
||||||
|
|
||||||
|
When a user needs to change his/her profile info, I want to make sure it is possible, so that I have the up-to-date information about users.
|
||||||
|
|
||||||
|
When my personal info (email etc.) is changed, I want to edit it in my profile so that I provide up-to-date information about me and I receive messages (eg, notifications) properly.
|
||||||
|
|
||||||
|
When I decide to stop using the data portal, I want to be able to delete my account, so that my personal details aren't stored in the service that I don't need anymore.
|
||||||
215
site/content/docs/dms/blob-storage.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# Blob Storage
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
DMS and data portals often need to *store* data as well as metadata. As such, they require a system for doing this. This page focuses on Blob Storage aka Bulk or Raw storage (see [storage](/docs/dms/storage) page for an overview of all types of storage).
|
||||||
|
|
||||||
|
Blob storage is for storing "blobs" of data, that is a raw stream of bytes like files on a filesystem. For blob storage think local filesystem or cloud storage like S3, GCS, etc.
|
||||||
|
|
||||||
|
Blob Storage in a DMS can be provided via:
|
||||||
|
|
||||||
|
* Local file system: storing on disk or storage directly connected to the instance
|
||||||
|
* Cloud storage like S3, Google Cloud Storage, Azure storage etc
|
||||||
|
|
||||||
|
Today, cloud storage would be the default in most cases.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Storage: Persistent, cost-efficient storage
|
||||||
|
* Download: Fast, reliable download (possibly even with support for edge distribution)
|
||||||
|
* Upload: reliable and rapid upload
|
||||||
|
* Direct upload to (cloud) storage by clients i.e. without going via the DMS. Why? Because cloud storage has many features that it would be costly replicate (e.g. multipart, resumable etc), excellent performance and reliability for upload. It also cuts out the middleman of the DMS backend thereby saving bandwidth, reducing load on the DMS backend and improving performance
|
||||||
|
* Upload UI: having an excellent UI for doing upload. NB: this UI is considered part of the [publish feature](/docs/dms/publish)
|
||||||
|
* Cloud: integrate with cloud storage
|
||||||
|
* Permissions: restricting access to data stored in blob storage based on the permissions of the DMS. For example, if Joe does not have access to a dataset on the DMS he should not be able to access associated blob data in the storage system
|
||||||
|
|
||||||
|
## Flows
|
||||||
|
|
||||||
|
### Direct to Cloud Upload
|
||||||
|
|
||||||
|
Want: Direct upload to cloud storage ... But you need to authorize that ... So give them a token from your app
|
||||||
|
|
||||||
|
A sequence diagram illustrating the process for a direct to cloud upload:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
|
||||||
|
participant Browser as Client (Browser / Code)
|
||||||
|
participant Authz as Authz Server
|
||||||
|
participant BitStore as Storage Access Token Service
|
||||||
|
participant Storage as Cloud Storage
|
||||||
|
|
||||||
|
Browser->>Authz: Give me a BitStore access token
|
||||||
|
Authz->>Browser: Token
|
||||||
|
Browser->>BitStore: Get a signed upload URL (access token, file metdata)
|
||||||
|
BitStore->>Browser: Signed URL
|
||||||
|
Browser->>Storage: Upload file (signed URL)
|
||||||
|
Storage->>Browser: OK (storage metadata)
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's a more elaborate version showing storage of metadata into the MetaStore afterwards (and skipping the Authz service):
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
|
||||||
|
participant browser as Client (Browser / Code)
|
||||||
|
participant vfts as MetaStore
|
||||||
|
participant bitstore as Storage Access Token Service
|
||||||
|
participant storage as Cloud Storage
|
||||||
|
|
||||||
|
browser->>browser: Select files to upload
|
||||||
|
browser->>browser: calculate file hashes (if doing content addressable)
|
||||||
|
browser->bitstore: get signed URLs(file1.csv URL, file2.csv URL, auth info)
|
||||||
|
bitstore->>browser: signed URLs
|
||||||
|
browser->>storage: upload file1.csv
|
||||||
|
storage->>browser: OK
|
||||||
|
browser->>storage: upload file2.csv
|
||||||
|
storage->>browser: OK
|
||||||
|
browser->>browser: Compose datapackage.json
|
||||||
|
browser->>vfts: create dataset(datapackage.json, file1.csv pointer, file2.csv pointer, jwt token, ...)
|
||||||
|
vfts->>browser: OK
|
||||||
|
```
|
||||||
|
|
||||||
|
## CKAN 2 (Classic)
|
||||||
|
|
||||||
|
Blob Storage is known as the FileStore in CKAN v2 and below. The default is local disk storage.
|
||||||
|
|
||||||
|
There is support for cloud storage via a variety of extensions the most prominent of which is `ckanext-cloudstorage`: https://github.com/TkTech/ckanext-cloudstorage
|
||||||
|
|
||||||
|
There are a variety of issues:
|
||||||
|
|
||||||
|
* Cloud storage is not a first class citizen in CKAN: CKAN defaults to local file storage but cloud storage is the default in the world and has much better scalability, performance as well as integratability with cloud deployment
|
||||||
|
* The FileStore interface definition has a poor separation of concerns (for example, blob storage file paths is set in the FileStore component not in core CKAN) which makes it hard / hacky to extend and use for key use cases e.g. versioning.
|
||||||
|
* `ckanext-cloudstorage` (the default cloud storage extension) is ok but has many issues e.g.
|
||||||
|
* No direct to cloud upload: it uses CKAN backend as a middleman so all data must go via ckan backend
|
||||||
|
* Implements its own (sometimes unreliable) version of multipart upload (which means additional code which isn't as reliable as cloud storage providers interface)
|
||||||
|
* No access to advanced features such as resumability etc
|
||||||
|
|
||||||
|
Generally, we at Datopian have seen a lot of issues around multipart / large file upload stability with clients and are still seeing issues when a lot of large files are uploaded via scripts. Fixing and refactoring code related to storage is very costly, and tends to result in client specific "hacks".
|
||||||
|
|
||||||
|
## CKAN v3
|
||||||
|
|
||||||
|
An approach to blob storage that leverages cloud blob storage directly (i.e. without having to upload and serve all files via the CKAN web server), unlocking the performance characteristics of the storage backend directly. It is designed with a microservice approach and supports direct to cloud uploads and downloads. The key components are listed in the next section. You can read more about the overall design approach in the [design section below](#Design).
|
||||||
|
|
||||||
|
It is backwards compatible with CKAN v2 and has been successfully deployed with CKAN v2.8 and v2.9.
|
||||||
|
|
||||||
|
**Status: Production.**
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
* [ckanext-blob-storage](https://github.com/datopian/ckanext-blob-storage) (formerly known as ckanext-external-storage)
|
||||||
|
* Hooking CKAN to Giftless replacing resource storage
|
||||||
|
* Depends on giftless-client and ckanext-authz-service
|
||||||
|
* Doesn't implement IUploader - completely overrides upload / download routes for resources
|
||||||
|
* [Giftless](https://github.com/datopian/giftless) - Git LFS compatible implementation for storage with some extras on top. This hands out access tokens to store data in cloud storage.
|
||||||
|
* Docs at https://giftless.datopian.com
|
||||||
|
* Backends for Azure, Google Cloud Storage and local
|
||||||
|
* Multipart support (on top of standard LFS protocol)
|
||||||
|
* Accepts JWT tokens for authentication and authorization
|
||||||
|
* [ckanext-authz-service](https://github.com/datopian/ckanext-authz-service/) - This extension uses CKAN’s built-in authentication and authorization capabilities to: a) Generate JWT tokens and provide them via CKAN’s Web API to clients and b) Validate JWT tokens.
|
||||||
|
* Allows hooking CKAN's authentication and authorization capabilities to generate signed JWT tokens, to integrate with external systems
|
||||||
|
* Not specific for Giftless, but this is what it was built for
|
||||||
|
* [ckanext-asset-storage](https://github.com/datopian/ckanext-asset-storage) - this takes care of storing non-data assets e.g. organization images etc.
|
||||||
|
* CKAN IUploader for assets (not resources!)
|
||||||
|
* Pluggable backends - currently local and Azure
|
||||||
|
* Much cleaner than older implementations (ckanext-cloudstorage etc.)
|
||||||
|
|
||||||
|
Clients:
|
||||||
|
|
||||||
|
* [giftless-client-py](https://github.com/datopian/giftless-client) - Python client for Git LFS and Giftless-specific features
|
||||||
|
* Used by ckanext-blob-storage and other tools
|
||||||
|
* [giftless-client-js](https://github.com/datopian/giftless-client-js) - Javascript client for Git LFS and Giftless-specific features
|
||||||
|
* Used by ckanext-blob-storage and other tools for creating uploaders in the UI
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
The goal of this project is to create a more **_flexible_** system for storing **_data files_** (AKA “resources”) for **_CKAN_ and _other implementations_** of a data portal so that CKAN can support versioning, large file upload (and great file upload UX), plug easily into cloud and local file storage backends and, in general, is easy to customize both for storage layer and for CKAN client code of that layer
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Do one thing and do it well: provide an API to store and retrieve files from storage, in a way that is pluggable into a micro-services based application and to existing CKAN (2.8 / 2.9)
|
||||||
|
* Does not force, and in fact is not aware of, a specific file naming logic (i.e. resource file names could be based on a user given name, a content hash, a revision ID or any mixture of these - it is up to the using system to decide)
|
||||||
|
* Does not force a specific storage backend; Should support Amazon S3, Azure Storage and local file storage in some way initially but in general backend should be pluggable
|
||||||
|
* Does not force a specific authentication scheme; Expects a signed JWT token, does not care who signed it and how the user got authenticated
|
||||||
|
* Does not force complex authorization scheme; Leave it to external system to do complex authorization if needed;
|
||||||
|
* By default, the system can work in an “admin party” mode where all authenticated users have full access to all files. This will be “good enough” for many DMS implementations including CKAN.
|
||||||
|
* Potentially, allow plugging in a more complex authorization logic that relies on JWT claims to perform granular authorization checks
|
||||||
|
|
||||||
|
### For Data Files (i.e. Blobs)
|
||||||
|
|
||||||
|
This system is about storing and providing access to blobs, or streams of bytes; It is not about providing access to the data stored within (i.e. it is not meant to replace CKAN’s datastore).
|
||||||
|
|
||||||
|
### For CKAN – whilst not necessarily CKAN Specific
|
||||||
|
|
||||||
|
While the system’s design should not be CKAN specific in any way, our current client needs require us to provide a CKAN extension that integrates with this system.
|
||||||
|
|
||||||
|
CKAN’s current IUploader interface has been identified to be too narrow to provide the functionality required by complex projects (resource versioning, direct cloud uploads and downloads, large file support and multipart support). While some of these needs could be and have been “hacked” through the IUploader interface, the implementations have been over complex and hard to debug.
|
||||||
|
|
||||||
|
Our goal should be to provide a CKAN extension that provides the following functionality directly:
|
||||||
|
|
||||||
|
* Uploading and downloading resource files directly from the client if supported by the storage backend
|
||||||
|
* Multipart upload support if supported by storage backend
|
||||||
|
* Handling of signed URLs for uploads and private downloads
|
||||||
|
* Client side code for handling multipart uploads
|
||||||
|
* TBD: If storage backend does not support direct uploads / downloads, fall back to …
|
||||||
|
|
||||||
|
In addition, this extension should provide an API for other extensions to do things like:
|
||||||
|
|
||||||
|
* Set the file naming scheme (We need this for ckanext-versions)
|
||||||
|
* Lower level file access, e.g. move and delete files. We may need this in the future to optimize storage and deduplicate files as proposed for ckanext-versions
|
||||||
|
|
||||||
|
In addition, this extension must “play nice” with common CKAN features such as the datastore extension and related datapusher / xloader extensions.
|
||||||
|
|
||||||
|
### Usable For other DMS implementations
|
||||||
|
|
||||||
|
There should be nothing in this system, except for the CKAN extension described above, that is specific to CKAN. That will allow to re-use and re-integrate this system as a micro-service in other DMS implementations such as ckan-ng and others.
|
||||||
|
In fact, the core part of this system should be a generic, abstract storage service with a light authorization layer. This could make it useful in a host of situations where storage micro-service is needed.
|
||||||
|
|
||||||
|
### High Level Principles
|
||||||
|
|
||||||
|
Common Principles
|
||||||
|
|
||||||
|
* Uploads and downloads directly from cloud provides to browser
|
||||||
|
* Signed uploads / downloads - for private / authorized only data access
|
||||||
|
* Support for AWS, Azure and potentially GCP storage
|
||||||
|
* Support for local (non cloud) storage, potentially through a system like [https://min.io/](https://min.io/)
|
||||||
|
* Multipart / large file upload support (a few GB in size should be supported for Gates)
|
||||||
|
* Not opinionated about file naming / paths; Allow users to set file locations under some pre-defined patchs / buckets
|
||||||
|
* Client side support - browser widgets / code for uploading and downloading files / multipart uploads directly to different backends
|
||||||
|
* Well-documented flow for using from API (not browser)
|
||||||
|
* Provided API for deleting and moving files
|
||||||
|
* Provided API for accessing storage-level metadata (e.g. file MD5) (do we need this could be useful for processes that do things like deduplicate storage)
|
||||||
|
* Provided API for managing storage-level object level settings (e.g. “Content-disposition” / “Content-type” headers, etc.)
|
||||||
|
* Authorization based on some kind of portable scheme (JWT)
|
||||||
|
|
||||||
|
CKAN integration specific (implemented as a CKAN extension)
|
||||||
|
|
||||||
|
* JWT generation based on current CKAN user permissions
|
||||||
|
* Client widgets integration (or CKAN specific widgets) in right places in CKAN templates
|
||||||
|
* Hook into resource upload / download / deletion controllers in CKAN
|
||||||
|
* API to allow other extensions to control storage level object metadata (headers, path)
|
||||||
|
* API to allow other extensions to hook into lifecycle events - upload completion, download request, deletion etc.
|
||||||
|
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
The Decoupled Storage solution should be split into several parts, with some parts being independent of others:
|
||||||
|
|
||||||
|
* [External] Cloud Storage service (or API similar if local file system) e.g. S3, GCS, Azure Storage, Min.io (for local file system)
|
||||||
|
* Cloud Storage Access Service
|
||||||
|
* [External] Permissions Service for granting general permission tokens that give access to Cloud Storage Access Service
|
||||||
|
* JWT tokens can be generated by any party that has the right signing key. Thus, we can initially do without this if JWT signing is implemented as part of the CKAN extension
|
||||||
|
* Browser based Client for Cloud Storage (compatible with #1 and with different cloud vendors)
|
||||||
|
* CKAN extension that wraps the two parts above to provide a storage solution for CKAN
|
||||||
|
|
||||||
|
### Questions
|
||||||
|
|
||||||
|
* What is file structure in cloud ... i.e. What is the file path for uploaded files? Options:
|
||||||
|
* Client chooses a name/path
|
||||||
|
* Content addressable i.e. the name is given by the content? How? Use a hash.]
|
||||||
|
* Beauty of that: standard way to name things. The same thing has the same name (modulo collisions)
|
||||||
|
* Goes with versioning => same file = same name, diff file = diff name
|
||||||
|
* And do you enforce that from your app
|
||||||
|
* Request for token needs to include the destination file path
|
||||||
503
site/content/docs/dms/ckan-client-guide/index.md
Normal file
@@ -0,0 +1,503 @@
|
|||||||
|
# CKAN Client Guide
|
||||||
|
|
||||||
|
Guide to interacting with [CKAN](/docs/dms/ckan) for power users such as data scientists, data engineers and data wranglers.
|
||||||
|
|
||||||
|
This guide is about adding and managing data in CKAN programmatically and it assumes:
|
||||||
|
|
||||||
|
* You are familiar with key concepts like metadata, data, etc.
|
||||||
|
* You are working programmatically with a programming language such as Python, JavaScript or R (_coming soon_).
|
||||||
|
|
||||||
|
## Frictionless Formats
|
||||||
|
|
||||||
|
Clients use [Frictionless formats](https://specs.frictionlessdata.io/) by default for describing dataset and resource objects passed to client methods. Internally, we then use the a *CKAN {'<=>'} Frictionless Mapper* (both [in JavaScript]( https://github.com/datopian/frictionless-ckan-mapper-js ) and [in Python](https://github.com/frictionlessdata/frictionless-ckan-mapper)) to convert objects to CKAN formats before calling the API. **Thus, you can use _Frictionless Formats_ by default with the client**.
|
||||||
|
|
||||||
|
>[!tip]As CKAN moves to Frictionless to default this will gradually become unnecessary.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
Most of this guide has Python programming language in mind, including its [convention regading using _snake case_ for instances and methods names](https://www.python.org/dev/peps/pep-0008/#descriptive-naming-styles).
|
||||||
|
|
||||||
|
If needed, you can adapt the instructions to JavaScript and R (coming soon) by using _camel case_ instead — for example, if in the Python code we have `client.push_blob(…)`, in JavaScript it would be `client.pushBlob(…)`.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
Install the client for your language of choice:
|
||||||
|
|
||||||
|
* Python: https://github.com/datopian/ckan-client-py#install
|
||||||
|
* JavaScript: https://github.com/datopian/ckan-client-js#install
|
||||||
|
* R: _coming soon_
|
||||||
|
|
||||||
|
### Create a client
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from ckanclient import Client
|
||||||
|
|
||||||
|
|
||||||
|
api_key = '771a05ad-af90-4a70-beea-cbb050059e14'
|
||||||
|
api_url = 'http://localhost:5000'
|
||||||
|
organization = 'datopian'
|
||||||
|
dataset = 'dailyprices'
|
||||||
|
lfs_url = 'http://localhost:9419'
|
||||||
|
|
||||||
|
client = Client(api_url, organization, dataset, lfs_url)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { Client } = require('ckanClient')
|
||||||
|
|
||||||
|
apiKey = '771a05ad-af90-4a70-beea-cbb050059e14'
|
||||||
|
apiUrl = 'http://localhost:5000'
|
||||||
|
organization = 'datopian'
|
||||||
|
dataset = 'dailyprices'
|
||||||
|
|
||||||
|
const client = Client(apiKey, organization, dataset, apiUrl)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upload a resource
|
||||||
|
|
||||||
|
That is to say, upload a file, implicitly creating a new dataset.
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from frictionless import describe
|
||||||
|
|
||||||
|
|
||||||
|
resource = describe('my-data.csv')
|
||||||
|
client.push_blob(resource)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a new empty Dataset with metadata
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
client.create('my-data')
|
||||||
|
client.push(resource)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding a resource to an existing Dataset
|
||||||
|
|
||||||
|
>[!note]Not implemented yet.
|
||||||
|
|
||||||
|
|
||||||
|
```python
|
||||||
|
client.create('my-data')
|
||||||
|
client.push_resource(resource)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Edit a Dataset's metadata
|
||||||
|
|
||||||
|
>[!note]Not implemented yet.
|
||||||
|
|
||||||
|
|
||||||
|
```python
|
||||||
|
dataset = client.retrieve('sample-dataset')
|
||||||
|
client.update_metadata(
|
||||||
|
dataset,
|
||||||
|
metadata: {'maintainer_email': 'sample@datopian.com'}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
For details of metadata see the [metadata reference below](#metadata-reference).
|
||||||
|
|
||||||
|
## API - Porcelain
|
||||||
|
|
||||||
|
### `Client.create`
|
||||||
|
|
||||||
|
Expects as a single argument: a _string_, or a _dict_ (in Python), or an _object_ (in JavaScript). This argument is either a valid dataset name or dictionary with metadata for the dataset in Frictionless format.
|
||||||
|
|
||||||
|
### `Client.push`
|
||||||
|
|
||||||
|
Expects a single argument: a _dict_ (in Python) or an _object_ (in JavaScript) with a dataset metadata in Frictionless format.
|
||||||
|
|
||||||
|
### `Client.retrieve`
|
||||||
|
|
||||||
|
Expects a single argument: a string with a dataset name or uniquer ID. Returns a Frictionless resource as a _dict_ (in Python) or as an _Promisse .<object>_ (in JavaScript).
|
||||||
|
|
||||||
|
### `Client.push_blob`
|
||||||
|
|
||||||
|
Expects a single argument: a _dict_ (in Python) or an _object_ (in JavaScript) with a Frictionless resource.
|
||||||
|
|
||||||
|
## API - Plumbing
|
||||||
|
|
||||||
|
### `Client.action`
|
||||||
|
|
||||||
|
This method bridges access to the CKAN API _action endpoint_.
|
||||||
|
|
||||||
|
#### In Python
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
| -------------------- | ---------- | ---------- | ------------------------------------------------------------ |
|
||||||
|
| `name` | `str` | (required) | The action name, for example, `site_read`, `package_show`… |
|
||||||
|
| `payload` | `dict` | (required) | The payload being sent to CKAN. When a payload is provided to a GET request, it will be converted to URL parameters and each key will be converted to snake case. |
|
||||||
|
| `http_get` | `bool` | `False` | Optional, if `True` will make `GET` request, otherwise `POST`. |
|
||||||
|
| `transform_payload` | `function` | `None` | Function to mutate the `payload` before making the request (useful to convert to and from CKAN and Frictionless formats). |
|
||||||
|
| `transform_response` | `function` | `None` | function to mutate the response data before returning it (useful to convert to and from CKAN and Frictionless formats). |
|
||||||
|
|
||||||
|
>[!note]The CKAN API uses the CKAN dataset and resource formats (rather than Frictionless formats).
|
||||||
|
In other words, to stick to Frictionless formats, you can pass `frictionless_ckan_mapper.frictionless_to_ckan` as `transform_payload`, and `frictionless_ckan_mapper.ckan_to_frictionless` as `transform_response`.
|
||||||
|
|
||||||
|
|
||||||
|
#### In JavaScript
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
| ------------ | ------------------- | ------------------ | ------------------------------------------------------------ |
|
||||||
|
| `actionName` | <code>string</code> | (required) | The action name, for example, `site_read`, `package_show`… |
|
||||||
|
| `payload` | <code>object</code> | (required) | The payload being sent to CKAN. When a payload is provided to a GET request, it will be converted to URL parameters and each key will be converted to snake case. |
|
||||||
|
| `useHttpGet` | <code>object</code> | <code>false</code> | Optional, if `True` will make `GET` request, otherwise `POST`. |
|
||||||
|
|
||||||
|
>[!note]The JavaScript implementation uses the CKAN dataset and resource formats (rather than Frictionless formats).
|
||||||
|
In other words, to stick to Frictionless formats, you need to convert from Frictionless to CKAN before calling `action` , and from CKAN to Frictionless after calling `action`.
|
||||||
|
|
||||||
|
## Metadata reference
|
||||||
|
|
||||||
|
>[!info]Your site may have custom metadata that differs from the example set below.
|
||||||
|
|
||||||
|
|
||||||
|
### Profile
|
||||||
|
|
||||||
|
**(`string`)** Defaults to _data-resource_.
|
||||||
|
|
||||||
|
The profile of this descriptor.
|
||||||
|
|
||||||
|
Every Package and Resource descriptor has a profile. The default profile, if none is declared, is `data-package` for Package and `data-resource` for Resource.
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
- `{"profile":"tabular-data-package"}`
|
||||||
|
|
||||||
|
- `{"profile":"http://example.com/my-profiles-json-schema.json"}`
|
||||||
|
|
||||||
|
### Name
|
||||||
|
|
||||||
|
**(`string`)**
|
||||||
|
|
||||||
|
An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.
|
||||||
|
|
||||||
|
This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
- `{"name":"my-nice-name"}`
|
||||||
|
|
||||||
|
### Path
|
||||||
|
|
||||||
|
A reference to the data for this resource, as either a path as a string, or an array of paths as strings. of valid URIs.
|
||||||
|
|
||||||
|
The dereferenced value of each referenced data source in `path` `MUST` be commensurate with a native, dereferenced representation of the data the resource describes. For example, in a *Tabular* Data Resource, this means that the dereferenced value of `path` `MUST` be an array.
|
||||||
|
|
||||||
|
#### Validation
|
||||||
|
|
||||||
|
##### It must satisfy one of these conditions
|
||||||
|
|
||||||
|
###### Path
|
||||||
|
|
||||||
|
**(`string`)**
|
||||||
|
|
||||||
|
A fully qualified URL, or a POSIX file path..
|
||||||
|
|
||||||
|
Implementations need to negotiate the type of path provided, and dereference the data accordingly.
|
||||||
|
|
||||||
|
**Examples**
|
||||||
|
|
||||||
|
- `{"path":"file.csv"}`
|
||||||
|
|
||||||
|
- `{"path":"http://example.com/file.csv"}`
|
||||||
|
|
||||||
|
**(`array`)**
|
||||||
|
|
||||||
|
**Examples**
|
||||||
|
|
||||||
|
- `["file.csv"]`
|
||||||
|
|
||||||
|
- `["http://example.com/file.csv"]`
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
- `{"path":["file.csv","file2.csv"]}`
|
||||||
|
|
||||||
|
- `{"path":["http://example.com/file.csv","http://example.com/file2.csv"]}`
|
||||||
|
|
||||||
|
- `{"path":"http://example.com/file.csv"}`
|
||||||
|
|
||||||
|
### Data
|
||||||
|
|
||||||
|
Inline data for this resource.
|
||||||
|
|
||||||
|
### Schema
|
||||||
|
|
||||||
|
**(`object`)**
|
||||||
|
|
||||||
|
A schema for this resource.
|
||||||
|
|
||||||
|
### Title
|
||||||
|
|
||||||
|
**(`string`)**
|
||||||
|
|
||||||
|
A human-readable title.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
- `{"title":"My Package Title"}`
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
**(`string`)**
|
||||||
|
|
||||||
|
A text description. Markdown is encouraged.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
- `{"description":"# My Package description\nAll about my package."}`
|
||||||
|
|
||||||
|
### Home Page
|
||||||
|
|
||||||
|
**(`string`)**
|
||||||
|
|
||||||
|
The home on the web that is related to this data package.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
- `{"homepage":"http://example.com/"}`
|
||||||
|
|
||||||
|
### Sources
|
||||||
|
|
||||||
|
**(`array`)**
|
||||||
|
|
||||||
|
The raw sources for this resource.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
- `{"sources":[{"title":"World Bank and OECD","path":"http://data.worldbank.org/indicator/NY.GDP.MKTP.CD"}]}`
|
||||||
|
|
||||||
|
### Licenses
|
||||||
|
|
||||||
|
**(`array`)**
|
||||||
|
|
||||||
|
The license(s) under which the resource is published.
|
||||||
|
|
||||||
|
This property is not legally binding and does not guarantee that the package is licensed under the terms defined herein.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
- `{"licenses":[{"name":"odc-pddl-1.0","path":"http://opendatacommons.org/licenses/pddl/","title":"Open Data Commons Public Domain Dedication and License v1.0"}]}`
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
**(`string`)**
|
||||||
|
|
||||||
|
The file format of this resource.
|
||||||
|
|
||||||
|
`csv`, `xls`, `json` are examples of common formats.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
- `{"format":"xls"}`
|
||||||
|
|
||||||
|
### Media Type
|
||||||
|
|
||||||
|
**(`string`)**
|
||||||
|
|
||||||
|
The media type of this resource. Can be any valid media type listed with [IANA](https://www.iana.org/assignments/media-types/media-types.xhtml).
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
- `{"mediatype":"text/csv"}`
|
||||||
|
|
||||||
|
### Encoding
|
||||||
|
|
||||||
|
**(`string`)** Defaults to _utf-8_.
|
||||||
|
|
||||||
|
The file encoding of this resource.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
- `{"encoding":"utf-8"}`
|
||||||
|
|
||||||
|
### Bytes
|
||||||
|
|
||||||
|
**(`integer`)**
|
||||||
|
|
||||||
|
The size of this resource in bytes.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
- `{"bytes":2082}`
|
||||||
|
|
||||||
|
### Hash
|
||||||
|
|
||||||
|
**(`string`)**
|
||||||
|
|
||||||
|
The MD5 hash of this resource. Indicate other hashing algorithms with the {'{algorithm}'}:{'{hash}'} format.
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
- `{"hash":"d25c9c77f588f5dc32059d2da1136c02"}`
|
||||||
|
|
||||||
|
- `{"hash":"SHA256:5262f12512590031bbcc9a430452bfd75c2791ad6771320bb4b5728bfb78c4d0"}`
|
||||||
|
|
||||||
|
## Generating templates
|
||||||
|
|
||||||
|
You can use [`jsv`](https://github.com/datopian/jsv) to generate a template script in Python, JavaScript, and R.
|
||||||
|
|
||||||
|
To install it:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npm install -g git+https://github.com/datopian/jsv.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```
|
||||||
|
$ jsv data-resource.json --output py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**
|
||||||
|
```python
|
||||||
|
dataset_metadata = {
|
||||||
|
"profile": "data-resource", # The profile of this descriptor.
|
||||||
|
# [example] "profile": "tabular-data-package"
|
||||||
|
# [example] "profile": "http://example.com/my-profiles-json-schema.json"
|
||||||
|
"name": "my-nice-name", # An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.
|
||||||
|
"path": ["file.csv","file2.csv"], # A reference to the data for this resource, as either a path as a string, or an array of paths as strings. of valid URIs.
|
||||||
|
# [example] "path": ["http://example.com/file.csv","http://example.com/file2.csv"]
|
||||||
|
# [example] "path": "http://example.com/file.csv"
|
||||||
|
"data": None, # Inline data for this resource.
|
||||||
|
"schema": None, # A schema for this resource.
|
||||||
|
"title": "My Package Title", # A human-readable title.
|
||||||
|
"description": "# My Package description\nAll about my package.", # A text description. Markdown is encouraged.
|
||||||
|
"homepage": "http://example.com/", # The home on the web that is related to this data package.
|
||||||
|
"sources": [{"title":"World Bank and OECD","path":"http://data.worldbank.org/indicator/NY.GDP.MKTP.CD"}], # The raw sources for this resource.
|
||||||
|
"licenses": [{"name":"odc-pddl-1.0","path":"http://opendatacommons.org/licenses/pddl/","title":"Open Data Commons Public Domain Dedication and License v1.0"}], # The license(s) under which the resource is published.
|
||||||
|
"format": "xls", # The file format of this resource.
|
||||||
|
"mediatype": "text/csv", # The media type of this resource. Can be any valid media type listed with [IANA](https://www.iana.org/assignments/media-types/media-types.xhtml).
|
||||||
|
"encoding": "utf-8", # The file encoding of this resource.
|
||||||
|
# [example] "encoding": "utf-8"
|
||||||
|
"bytes": 2082, # The size of this resource in bytes.
|
||||||
|
"hash": "d25c9c77f588f5dc32059d2da1136c02", # The MD5 hash of this resource. Indicate other hashing algorithms with the {algorithm}:{hash} format.
|
||||||
|
# [example] "hash": "SHA256:5262f12512590031bbcc9a430452bfd75c2791ad6771320bb4b5728bfb78c4d0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
```
|
||||||
|
$ jsv data-resource.json --output js
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**
|
||||||
|
```javascript
|
||||||
|
const datasetMetadata = {
|
||||||
|
// The profile of this descriptor.
|
||||||
|
profile: "data-resource",
|
||||||
|
// [example] profile: "tabular-data-package"
|
||||||
|
// [example] profile: "http://example.com/my-profiles-json-schema.json"
|
||||||
|
// An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.
|
||||||
|
name: "my-nice-name",
|
||||||
|
// A reference to the data for this resource, as either a path as a string, or an array of paths as strings. of valid URIs.
|
||||||
|
path: ["file.csv", "file2.csv"],
|
||||||
|
// [example] path: ["http://example.com/file.csv","http://example.com/file2.csv"]
|
||||||
|
// [example] path: "http://example.com/file.csv"
|
||||||
|
// Inline data for this resource.
|
||||||
|
data: null,
|
||||||
|
// A schema for this resource.
|
||||||
|
schema: null,
|
||||||
|
// A human-readable title.
|
||||||
|
title: "My Package Title",
|
||||||
|
// A text description. Markdown is encouraged.
|
||||||
|
description: "# My Package description\nAll about my package.",
|
||||||
|
// The home on the web that is related to this data package.
|
||||||
|
homepage: "http://example.com/",
|
||||||
|
// The raw sources for this resource.
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
title: "World Bank and OECD",
|
||||||
|
path: "http://data.worldbank.org/indicator/NY.GDP.MKTP.CD",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// The license(s) under which the resource is published.
|
||||||
|
licenses: [
|
||||||
|
{
|
||||||
|
name: "odc-pddl-1.0",
|
||||||
|
path: "http://opendatacommons.org/licenses/pddl/",
|
||||||
|
title: "Open Data Commons Public Domain Dedication and License v1.0",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// The file format of this resource.
|
||||||
|
format: "xls",
|
||||||
|
// The media type of this resource. Can be any valid media type listed with [IANA](https://www.iana.org/assignments/media-types/media-types.xhtml).
|
||||||
|
mediatype: "text/csv",
|
||||||
|
// The file encoding of this resource.
|
||||||
|
encoding: "utf-8",
|
||||||
|
// [example] encoding: "utf-8"
|
||||||
|
// The size of this resource in bytes.
|
||||||
|
bytes: 2082,
|
||||||
|
// The MD5 hash of this resource. Indicate other hashing algorithms with the {algorithm}:{hash} format.
|
||||||
|
hash: "d25c9c77f588f5dc32059d2da1136c02",
|
||||||
|
// [example] hash: "SHA256:5262f12512590031bbcc9a430452bfd75c2791ad6771320bb4b5728bfb78c4d0"
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### R
|
||||||
|
|
||||||
|
```
|
||||||
|
$ jsv data-resource.json --output r
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**
|
||||||
|
```r
|
||||||
|
# The profile of this descriptor.
|
||||||
|
profile <- "data-resource"
|
||||||
|
# [example] profile <- "tabular-data-package"
|
||||||
|
# [example] profile <- "http://example.com/my-profiles-json-schema.json"
|
||||||
|
# An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.
|
||||||
|
name <- "my-nice-name"
|
||||||
|
# A reference to the data for this resource, as either a path as a string, or an array of paths as strings. of valid URIs.
|
||||||
|
path <- ["file.csv","file2.csv"]
|
||||||
|
# [example] path <- ["http://example.com/file.csv","http://example.com/file2.csv"]
|
||||||
|
# [example] path <- "http://example.com/file.csv"
|
||||||
|
# Inline data for this resource.
|
||||||
|
data <- NA
|
||||||
|
# A schema for this resource.
|
||||||
|
schema <- NA
|
||||||
|
# A human-readable title.
|
||||||
|
title <- "My Package Title"
|
||||||
|
# A text description. Markdown is encouraged.
|
||||||
|
description <- "# My Package description\nAll about my package."
|
||||||
|
# The home on the web that is related to this data package.
|
||||||
|
homepage <- "http://example.com/"
|
||||||
|
# The raw sources for this resource.
|
||||||
|
sources <- [{"title":"World Bank and OECD","path":"http://data.worldbank.org/indicator/NY.GDP.MKTP.CD"}]
|
||||||
|
# The license(s) under which the resource is published.
|
||||||
|
licenses <- [{"name":"odc-pddl-1.0","path":"http://opendatacommons.org/licenses/pddl/","title":"Open Data Commons Public Domain Dedication and License v1.0"}]
|
||||||
|
# The file format of this resource.
|
||||||
|
format <- "xls"
|
||||||
|
# The media type of this resource. Can be any valid media type listed with [IANA](https://www.iana.org/assignments/media-types/media-types.xhtml).
|
||||||
|
mediatype <- "text/csv"
|
||||||
|
# The file encoding of this resource.
|
||||||
|
encoding <- "utf-8"
|
||||||
|
# [example] encoding <- "utf-8"
|
||||||
|
# The size of this resource in bytes.
|
||||||
|
bytes <- 2082L
|
||||||
|
# The MD5 hash of this resource. Indicate other hashing algorithms with the {algorithm}:{hash} format.
|
||||||
|
hash <- "d25c9c77f588f5dc32059d2da1136c02"
|
||||||
|
# [example] hash <- "SHA256:5262f12512590031bbcc9a430452bfd75c2791ad6771320bb4b5728bfb78c4d0"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
The client **should** use Frictionless formats by default for describing dataset and resource objects passed to client methods.
|
||||||
|
|
||||||
|
In addition, where more than metadata is needed (e.g., we need to access the data stream, or get the schema) we expect the _Dataset_ and _Resource_ objects to follow the [Frictionless Data Lib pattern](https://github.com/frictionlessdata/project/blob/master/rfcs/0004-frictionless-data-lib-pattern.md).
|
||||||
108
site/content/docs/dms/ckan-enterprise/index.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# CKAN Enterprise
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
CKAN Enterprise is our name for what we plan would become our standard "base" distribution for CKAN going forward:
|
||||||
|
|
||||||
|
* It is a CKAN standard code base with micro-services.
|
||||||
|
* Enterprise grade data catalog and portal targeted at Gov (open data portals) and Enterprise (Data Catalogs +).
|
||||||
|
* It is also known as [Datopian DMS](https://www.datopian.com/datopian-dms/).
|
||||||
|
|
||||||
|
## Roadmap 2021 and beyond
|
||||||
|
|
||||||
|
| | Current | CKAN Enterprise |
|
||||||
|
|-------------------|--------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
|
||||||
|
| Raw storage | Filestore | Giftless |
|
||||||
|
| Data Loader (db) | DataPusher extension | Aircan |
|
||||||
|
| Data Storage (db) | Postgres | Any database engine. By default, Postgres |
|
||||||
|
| Data API (read) | Built-in DataStore extension's API including SQL endpoint | GraphQL based standalone micro-service |
|
||||||
|
| Frontend (public) | Build-in frontend into CKAN Classic python app (some projects are using nodejs app) | PortalJS or nodejs app |
|
||||||
|
| Data Explorer | ReclineJS (some projects that uses nodejs app for frontend have React based Data Explorer) | GraphQL based Data Explorer |
|
||||||
|
| Auth | Traditional login/password + extendable with CKAN Classic extensions | SSO with default Google, Github, Facebook and Microsoft options |
|
||||||
|
| Permissions | CKAN Classic based permissions | Existing permissions exposed via JWT based authz API |
|
||||||
|
|
||||||
|
## Timeline 2021
|
||||||
|
|
||||||
|
To develop a base distribution of CKAN Enterprise, we want to build a demo project with the features from the roadmap. This way we can:
|
||||||
|
|
||||||
|
* understand its advantages/limitations;
|
||||||
|
* compare against other instances of CKAN;
|
||||||
|
* demonstrate for the potential clients.
|
||||||
|
|
||||||
|
High level overview of the planned features with ETA:
|
||||||
|
|
||||||
|
| Name | Description | Effort | ETA |
|
||||||
|
| ----------------------------- | ------------------------------------ | ------ | --- |
|
||||||
|
| [Init](#Init) | Select CKAN version and deploy to DX | xs | Q2 |
|
||||||
|
| [Blobstore](#Blobstore) | Integrate Giftless for raw storage | s | Q2 |
|
||||||
|
| [Versioning](#Versioning) | Develop/integrate new versioning sys | l | Q3 |
|
||||||
|
| [DataLoader](#DataLoader) | Develop/integrate Aircan | xl | Q3 |
|
||||||
|
| [Data API](#Data-API) | Integrate new Data API (read) | m | Q2 |
|
||||||
|
| [Frontend](#Frontend) | Build a theme using PortalJS | s | Q2 |
|
||||||
|
| [DataExplorer](#DataExplorer) | Integrate into PortalJS | s | Q2 |
|
||||||
|
| [Permissions](#Permissions) | Develop permissions in read frontend | m | Q4 |
|
||||||
|
| [Auth](#Auth) | Integrate | s | Q4 |
|
||||||
|
|
||||||
|
### Init
|
||||||
|
|
||||||
|
Initialize a new project for development of CKAN Enterprise.
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
|
||||||
|
* Boot project in Datopian-DX cluster
|
||||||
|
* Use CKAN v2.8.x (latest patch) or 2.9.x
|
||||||
|
* Don't setup DataPusher
|
||||||
|
* Namespace: `ckan-enterprise`
|
||||||
|
* Domain: `enterprise.ckan.datopian.com`
|
||||||
|
|
||||||
|
### Blobstore
|
||||||
|
|
||||||
|
See [blob storage](/docs/dms/blob-storage#ckan-v3)
|
||||||
|
|
||||||
|
### Versioning
|
||||||
|
|
||||||
|
See [versioning](/docs/dms/versioning#ckan-v3)
|
||||||
|
|
||||||
|
### DataLoader
|
||||||
|
|
||||||
|
See [DataLoader](/docs/dms/load)
|
||||||
|
|
||||||
|
### Data API
|
||||||
|
|
||||||
|
* Install new [Data API service](https://github.com/datopian/data-api) in the project
|
||||||
|
* Install Hasura service in the project
|
||||||
|
* Set it up to work with DB of CKAN Enterprise
|
||||||
|
* Read more about Data API [here](/docs/dms/data-api#read-api-3)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
* We could experiment and use various features of Hasura, eg:
|
||||||
|
* Setting up row/column limits per user role (permissions)
|
||||||
|
* Subscriptions to auto load new data rows
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
PortalJS for the read frontend of CKAN Enterprise. [Read more](/docs/dms/frontend/#frontend).
|
||||||
|
|
||||||
|
### DataExplorer
|
||||||
|
|
||||||
|
A new Data Explorer based on GraphQL API: https://github.com/datopian/data-explorer-graphql
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
|
||||||
|
See [permissions](/docs/dms/permissions#permissions-authorization).
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
|
||||||
|
Next generation, Kratos based, authentication (mostly SSO with no Traditional login by default) with following options out of the box:
|
||||||
|
|
||||||
|
* GitHub
|
||||||
|
* Google
|
||||||
|
* Facebook
|
||||||
|
* Microsoft
|
||||||
|
|
||||||
|
Easy to add:
|
||||||
|
|
||||||
|
* Discord
|
||||||
|
* GitLab
|
||||||
|
* Slack
|
||||||
365
site/content/docs/dms/ckan-v3/index.md
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
# CKAN v3
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
This document describes the architectures of CKAN v2 ("CKAN Classic"), CKAN v3 (also known as "CKAN Next Gen" for Next Generation), and CKAN v3 hybrid. The latter is an intermediate approach towards v3, where we still use CKAN v2 and common extensions, and only create microservices for new features.
|
||||||
|
|
||||||
|
You will also find out how to do common tasks such as theming or testing, in each of the architectures.
|
||||||
|
|
||||||
|
*Note: this blog post has an overview of the more decoupled, microservices approach at the core of v3: https://www.datopian.com/2021/05/17/a-more-decoupled-ckan/*
|
||||||
|
|
||||||
|
## CKAN v2, CKAN v3 and Why v3
|
||||||
|
|
||||||
|
In yellow, you see one single Python process:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph ckanclassic["CKAN Classic"]
|
||||||
|
ckancore["Core"]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
When you want to extend core functionality of CKAN v2 (Classic), you write a Python package that must be installed in CKAN. This way, the extension will also run in the same process as the core functionality. This is known as a monolithic architecture.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph ckanclassic["CKAN Classic"]
|
||||||
|
ckancore["Core"] --> ckanext["CKAN Extension 1"]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
When you start to add multiple features, through extensions, what you get is one single Python process running many non-related functionalities.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph ckanclassic["CKAN Classic"]
|
||||||
|
ckancore["Core"] --> ckanext["CKAN Extension 1"]
|
||||||
|
ckancore --> ckanext2["CKAN Extension 2"]
|
||||||
|
ckancore --> ckanext3["CKAN Extension 3"]
|
||||||
|
ckancore --> ckanext4["CKAN Extension 4"]
|
||||||
|
ckancore --> ckanext5["CKAN Extension 5"]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
This monolithic approach has advantages in terms of simplicity of development and deployment, especially when the system is small. However, as it grows in scale and scope, there are an increasing number of issues.
|
||||||
|
|
||||||
|
In this approach, an optional extension has the ability to crash the whole CKAN instance. Every new feature must be written in the same language and framework (e.g. Python, leveraging Flask or Django). And, perhaps most fundamentally, the overall system is highly coupled, making it complex and hard to understand, debug, extend, and evolve.
|
||||||
|
|
||||||
|
### Microservices and CKAN v3
|
||||||
|
|
||||||
|
The main way to address these problems while gaining extra benefits is to move to a microservices-based architecture.
|
||||||
|
|
||||||
|
Thus, we recommend building the next version of CKAN – CKAN v3 – on a microservices approach.
|
||||||
|
|
||||||
|
[!tip]CKAN v3 is sometimes also referred to as CKAN Next Gen(eration).
|
||||||
|
|
||||||
|
With microservices, each piece of functionality runs in its own service and process.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph ckanapi3["CKAN API 3"]
|
||||||
|
ckanapi31["API 3"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph ckanapi2["CKAN API 2"]
|
||||||
|
ckanapi21["API 2"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph ckanapi1["CKAN API 1"]
|
||||||
|
ckanapi11["API 1"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph ckanfrontend["CKAN frontend"]
|
||||||
|
ckanfrontend1["Frontend"]
|
||||||
|
end
|
||||||
|
|
||||||
|
ckanfrontend1 --> ckanapi11
|
||||||
|
ckanfrontend1 --> ckanapi21
|
||||||
|
ckanfrontend1 --> ckanapi31
|
||||||
|
```
|
||||||
|
|
||||||
|
### Incremental Evolution – Hybrid v3
|
||||||
|
|
||||||
|
One of the other advantages of the microservices approach is that it can also be used to extend and evolve current CKAN v2 solutions in an incremental way. We term these kinds of solutions "Hybrid v3," as they are a mix of v2 and v3 together.
|
||||||
|
|
||||||
|
For example, a Hybrid v3 data portal could use a new microservice written in Node for the frontend, and combine that with CKAN v2 (with v2 extensions).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph ckanapi3["CKAN API 3"]
|
||||||
|
ckanapi31["API 3"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph ckanapi2["CKAN API 2"]
|
||||||
|
ckanapi21["API 2"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph ckanapi1["CKAN API 1"]
|
||||||
|
ckanapi11["API 1"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph ckanfrontend["CKAN frontend"]
|
||||||
|
ckanfrontend1["Frontend"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph ckanclassic["CKAN Classic"]
|
||||||
|
ckancore["Core"] --> ckanext["CKAN Extension 1"]
|
||||||
|
ckancore --> ckanext2["CKAN Extension 2"]
|
||||||
|
end
|
||||||
|
|
||||||
|
ckanfrontend1 --> ckancore
|
||||||
|
ckanfrontend1 --> ckanapi11
|
||||||
|
ckanfrontend1 --> ckanapi21
|
||||||
|
ckanfrontend1 --> ckanapi31
|
||||||
|
```
|
||||||
|
|
||||||
|
The hybrid approach means we can evolve CKAN v2 "Classic" to CKAN v3 "Next Gen" incrementally. In particular, it allows people to keep using their existing v2 extensions, and upgrade them to new microservices gradually.
|
||||||
|
|
||||||
|
### Comparison of Approaches
|
||||||
|
|
||||||
|
| | CKAN v2 (Classic) | CKAN v3 (Next Gen) | CKAN v3 Hybrid |
|
||||||
|
| ------------ | ------------------| -------------------| ---------------|
|
||||||
|
| Architecture | Monolithic | Microservice | Microservice with v2 core |
|
||||||
|
| Language | Python | You can write services in any language you like.<br/><br/>Frontend default: JS.<br/>Backend default: Python | Python and any language you like for microservices. |
|
||||||
|
| Frontend (and theming) | Python with Python CKAN extension | Flexible. Default is modern JS/NodeJS based | Can use old frontend but default to new JS-based frontend. |
|
||||||
|
| Data Packages | Add-on, no integration | Default internal and external format | Data Packages with converter to old CKAN format. |
|
||||||
|
| Extension | Extensions are libraries that are added to core runtime. They must therefore be built in python and are loaded into the core process at build time. "Template/inheritance" model where hooks are in core and it is core that loads and calls plugins. This means that if a hook does not exist in core then the extension is stymied. | Extensions are microservices and can be written in any language. They are loaded into the url space via kubernetes routing manager. Extensions hook into "core" via APIs (rather than in code). Follows a "composition" model rather than inheritance model | Can use old style extensions or microservices. |
|
||||||
|
| Resource Scaling | You have a single application so scaling is of the core application. | You can scale individual microservices as needed. | Mix of v2 and v3 |
|
||||||
|
|
||||||
|
## Why v3: Long Version
|
||||||
|
|
||||||
|
What are the problems with CKAN v2's monolithic architecture in relation to microservices v3?
|
||||||
|
|
||||||
|
* **Poor Developer Experience (DX), innovability, and scalability due to coupling**. Monolithic means "one big system" => Coupling & Complexity => hard to understand, change and extend. Changes in one area can unexpectedly affect other areas.
|
||||||
|
* DX to develop a small new API requires wiring into CKAN core via an extension. Extensions can interact in unexpected ways.
|
||||||
|
* The core of people who fully understand CKAN has stayed small for a reason: there's a lot of understand.
|
||||||
|
* https://github.com/ckan/ckan/issues/5333 is an example of a small bug that's hard to track down due to various paths involved.
|
||||||
|
* Harder to make incremental changes due to coupling (e.g. Python 3 upgrade requires *everything* to be fixed at once - can't do rolling releases).
|
||||||
|
* **Stability**. One bad extension crashes or slows down the whole system
|
||||||
|
* **One language => Less developer flexibility (Poor DX)**. Have to write *everything* in Python, including the frontend. This is an issue especially for the frontend: almost all modern frontend development is heavily Javascript-based and theme is the #1 thing people want to customize in CKAN. At the moment, that requires installing *all* of CKAN core (using Docker) plus some familiarity with Python and Jinja templating. This is a big ask.
|
||||||
|
* **Extension stablity and testing**. Testing of extensions is painful (at least without careful factoring in a separate mini library) and are therefore often not tested; they don't have Continuous Integration (CI) or Continuous Deployment (CD). As an example, a highly experienced Python developer at Datopian was still struggling to get extension tests working 6 months into their CKAN work.
|
||||||
|
* **DX is poor especially when getting started**. Getting CKAN up and running requires multiple external services (database, Solr, Redis, etc.) making Docker the only viable way for bootstraping a local development environment. This makes getting started with CKAN daunting and painful.
|
||||||
|
* **Vertical scalability is poor**. Scaling the system is costly as you have to replicate the whole core process in every machine.
|
||||||
|
* **System is highly coupled.** Extensions b/c in process tend to end up with significant coupling to core which makes them brittle (has improved with plugins.toolkit)
|
||||||
|
* Upgrading core to Python 3 requires upgrading *all* extensions because they run in the same process.
|
||||||
|
* Search Index is not a separate API, but in Core. So replacing Solr is hard.
|
||||||
|
|
||||||
|
The top 2 customizations of CKAN are slow and painful and require deep knowledge of CKAN:
|
||||||
|
|
||||||
|
* Theming a site.
|
||||||
|
* Customizing the metadata.
|
||||||
|
|
||||||
|
## Architectures
|
||||||
|
|
||||||
|
### CKAN v2 (Classic)
|
||||||
|
|
||||||
|
This diagram is based on the file `docker-compose.yml` of [github.com/okfn/docker-ckan](https://github.com/okfn/docker-ckan) (`docker-compose.dev.yml` has the same components, but different configuration).
|
||||||
|
|
||||||
|
A difference from this diagram to the file is that we are not including DataPusher, as it is not a required dependency.
|
||||||
|
|
||||||
|
>[!tip]Databases may run as Docker containers, or rely on third-party services such as Amazon Relational Database Service (RDS).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
|
||||||
|
CKAN[CKAN web app]
|
||||||
|
|
||||||
|
CKAN --> DB[(Database)]
|
||||||
|
CKAN --> Solr[(Solr)]
|
||||||
|
CKAN --> Redis[(Redis)]
|
||||||
|
|
||||||
|
subgraph Docker container
|
||||||
|
CKAN
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Same setup showing some of the key extensions explicitly:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
core[CKAN Core] --> DB[(Database)]
|
||||||
|
datastore --> DB2[(Database - DataStore)]
|
||||||
|
core --> Solr[(Solr)]
|
||||||
|
core --> Redis[(Redis)]
|
||||||
|
|
||||||
|
subgraph Docker container
|
||||||
|
core
|
||||||
|
datastore
|
||||||
|
datapusher
|
||||||
|
imageview
|
||||||
|
...
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
CKAN ships with several core extensions that are built-in. Here, together with the list of main components, we list a couple of them:
|
||||||
|
|
||||||
|
Name | Type | Repository | Description
|
||||||
|
-----|------|------------|------------
|
||||||
|
CKAN | Application (API + Worker) | [Link](https://github.com/ckan/ckan) | Data management system (DMS) for powering data hubs and data portals. It's a monolithical web application that includes several built-in extensions and dependencies, such as a job queue service. In theory, it's possible to run it without any extensions.
|
||||||
|
datapusher | CKAN Extension | [Link](https://github.com/ckan/ckan/tree/master/ckanext/datapusher) | It could also be called "datapusher-connect." It's a glue code to connect with a separate microservice called DataPusher, which performs actions when new data arrives.
|
||||||
|
datastore | CKAN Extension | [Link](https://github.com/ckan/ckan/tree/master/ckanext/datastore) | The interface between CKAN and the structure database, the one receiving datasets and resources (CSVs). It includes an API for the database and an administrative UI.
|
||||||
|
imageview | CKAN Extension | [Link](https://github.com/ckan/ckan/tree/master/ckanext/imageview) | It provides an interface for creating HTML templates for image resources.
|
||||||
|
multilingual | CKAN Extension | [Link](https://github.com/ckan/ckan/tree/master/ckanext/multilingual) | It provides an interface for translation and localization.
|
||||||
|
Database | Database | | People tend to use a single PostgreSQL instance for this. Separated in multiple databases, it's the place where CKAN stores its own information (sometimes referred as "MetaStore" and "HubStore"), rows of resources (StructuredStore or DataStore), and raw datasets and resources ("BlobStore" or "FileStore"). The latter may store data in the local filesystem or cloud providers, via extensions.
|
||||||
|
Solr | Database | | It provides indexing and full-text search for CKAN.
|
||||||
|
Redis | Database | | Lightweight key-value store, used for caching and job queues.
|
||||||
|
|
||||||
|
### CKAN v3 (Next Gen)
|
||||||
|
|
||||||
|
CKAN Next Gen is still a DMS, as CKAN Classic; but rather than a monolithical architecture, it follows the microservices approach. CKAN Classic is not a dependency anymore, as we have smaller services providing functionality that we may or many not choose to include. This description is based on [Datopian's Technical Documentation](/docs/dms/ckan-v3/next-gen/#roadmap).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph api3["..."]
|
||||||
|
api31["API"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph api2["Administration"]
|
||||||
|
api21["API"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph api1["Authentication"]
|
||||||
|
api11["API"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph frontend["Frontend"]
|
||||||
|
frontendapi["API"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph storage["Raw Resources Storage"]
|
||||||
|
storageapi["API"]
|
||||||
|
end
|
||||||
|
|
||||||
|
storageapi --> cloudstorage[(Cloud Storage)]
|
||||||
|
|
||||||
|
frontendapi --> storageapi
|
||||||
|
frontendapi --> api11
|
||||||
|
frontendapi --> api21
|
||||||
|
frontendapi --> api31
|
||||||
|
```
|
||||||
|
|
||||||
|
At this moment, many important features are only available through CKAN extensions, so that brings us to the hybrid approach.
|
||||||
|
|
||||||
|
### CKAN Hybrid v3 (Next Gen)
|
||||||
|
|
||||||
|
We may sometimes make an explit distinction between CKAN v3 "hybrid" and "pure." The reason is because we want to ensure that we're not there yet – we have many opportunities to extract features out of CKAN and CKAN Extensions.
|
||||||
|
|
||||||
|
In this approach, we still rely on CKAN Classic and all its extensions. Many already had many tests and bugs fixed, so we can deliver more if not forced to rewrite everything from scratch.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph ckanapi3["CKAN API 3"]
|
||||||
|
ckanapi31["API 3"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph ckanapi2["CKAN API 2"]
|
||||||
|
ckanapi21["API 2"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph ckanapi1["CKAN API 1"]
|
||||||
|
ckanapi11["API 1"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph ckanfrontend["Frontend"]
|
||||||
|
ckanfrontend1["Frontend v2"]
|
||||||
|
theme["[Project-specific theme]"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph ckanclassic["CKAN Classic"]
|
||||||
|
ckancore["Core"] --> ckanext["CKAN Extension 1"]
|
||||||
|
ckancore --> ckanext2["[Project-specific extension]"]
|
||||||
|
end
|
||||||
|
|
||||||
|
ckanfrontend1 --> ckancore
|
||||||
|
ckanfrontend1 --> ckanapi11
|
||||||
|
ckanfrontend1 --> ckanapi21
|
||||||
|
ckanfrontend1 --> ckanapi31
|
||||||
|
```
|
||||||
|
|
||||||
|
Name | Type | Repository | Description
|
||||||
|
-----|------|------------|------------
|
||||||
|
Frontend v2 | Application | [Link](https://github.com/datopian/frontend-v2) | Node application for Data Portals. It communicates with a CKAN Classic instance, through its API, to get data and render HTML. It is written to be extensible, such as connecting to other applications and theming.
|
||||||
|
[Project-specific theme] | Frontend Theme | e.g., [Link](https://github.com/datopian/frontend-oddk) | Extension to Frontend v2 where you can personalize the interface, create different pages, and connect with other APIs.
|
||||||
|
[API 1] | Application | e.g., [Link](https://github.com/datopian/data-subscriptions) | Any application with an API to communicate with the user-facing Frontend v2 or to run tasks in background. Given the current architecture, often, this API is usually designed to work with CKAN interfaces. Over time, we may choose to make it more generic, and even replace CKAN Core with other applications.
|
||||||
|
|
||||||
|
## Job Stories
|
||||||
|
|
||||||
|
In this spreadsheet, you will find a list of common job stories in CKAN projects. Also, how you can accomplish them in CKAN v2, v3, and Hybrid v3.
|
||||||
|
|
||||||
|
https://docs.google.com/spreadsheets/d/1cLK8xylprmVsoQIbdphqz9-ccSpdDABQExvKdvNJqaQ/edit#gid=757361856
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
An HTTP API, usually following the REST style.
|
||||||
|
|
||||||
|
### Application
|
||||||
|
|
||||||
|
A Python package, an API, a worker... It may have other applications as dependencies.
|
||||||
|
|
||||||
|
### CKAN Extension
|
||||||
|
|
||||||
|
A Python package following specification from [CKAN Extending guide](https://docs.ckan.org/en/2.8/extensions/index.html).
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
An organized collection of data.
|
||||||
|
|
||||||
|
### Dataset
|
||||||
|
|
||||||
|
A group of resources made to be distributed together.
|
||||||
|
|
||||||
|
### Frontend Theme
|
||||||
|
|
||||||
|
A Node project specializing behavior present in [Frontend v2](https://github.com/datopian/frontend-v2).
|
||||||
|
|
||||||
|
### Resource
|
||||||
|
|
||||||
|
A data blob. Common formats are CSV, JSON, and PDF.
|
||||||
|
|
||||||
|
### System
|
||||||
|
|
||||||
|
A group of applications and databases that work together to accomplish a set of tasks.
|
||||||
|
|
||||||
|
### Worker
|
||||||
|
|
||||||
|
An application that runs tasks in background. They may run recurrently according to a given schedule, or as soon as it's requested by another application.
|
||||||
|
|
||||||
|
## Appendix
|
||||||
|
|
||||||
|
### Architecture - CKAN v2 with DataPusher
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph DataPusher
|
||||||
|
datapusherapi["DataPusher API"]
|
||||||
|
datapusherworker["CKAN Service Provider"]
|
||||||
|
SQLite[(SQLite)]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph CKAN
|
||||||
|
core
|
||||||
|
datapusher[datapusher ext]
|
||||||
|
datastore
|
||||||
|
...
|
||||||
|
end
|
||||||
|
|
||||||
|
core[CKAN Core] --> datastore
|
||||||
|
datastore --> DB[(Database)]
|
||||||
|
datapusherapi --> core
|
||||||
|
datapusher --> datapusherapi
|
||||||
|
```
|
||||||
|
|
||||||
|
Name | Type | Repository | Description
|
||||||
|
-----|------|------------|------------
|
||||||
|
DataPusher | System | [Link](https://github.com/ckan/datapusher) | Microservice that parses data files and uploads them to the datastore.
|
||||||
|
DataPusher API | API | [Link](https://github.com/ckan/datapusher) | HTTP API written in Flask. It is called from the built-in `datapusher` CKAN extension whenever a resource is created (and has the right type).
|
||||||
|
CKAN Service Provider | Worker | [Link](https://github.com/ckan/ckan-service-provider) | Library for making web services that make functions available as synchronous or asynchronous jobs.
|
||||||
|
SQLite | Database | | Unknown use. Possibly a worker dependency.
|
||||||
|
|
||||||
|
### Old Next Gen Page
|
||||||
|
|
||||||
|
Prior to this page, we had one called "Next Gen." It has intersections with this article, although it focuses more on the benefits of microservices. For the time being, the page still exists in [/ckan-v3/next-gen](/docs/dms/ckan-v3/next-gen), although it may get merged with this one in the future.
|
||||||
203
site/content/docs/dms/ckan-v3/next-gen.md
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
# Next Gen
|
||||||
|
|
||||||
|
“Next Gen” (NG) is our name for the evolution of CKAN from its current state as “CKAN Classic”.
|
||||||
|
|
||||||
|
Next Gen has a decoupled, microservice architecture in contrast to CKAN Classic's monolithic architecture. It is also built from the ground up on the Frictionless Data principles and specifications which provide a simple, well-defined and widely adopted set of core interfaces and tooling for managing data.
|
||||||
|
|
||||||
|
## Classic to Next Gen
|
||||||
|
|
||||||
|
CKAN classic: monolithic architecture -- everything is one big python application. Extension is done at code level and "compiled in" at compile/run-time (i.e. you end up with one big docker file).
|
||||||
|
|
||||||
|
CKAN Next Gen: decoupled, service-oriented -- services connected by network calls. Extension is done by adding new services,
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
|
||||||
|
subgraph "CKAN Classic"
|
||||||
|
plugins
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "CKAN Next Gen"
|
||||||
|
microservices
|
||||||
|
end
|
||||||
|
|
||||||
|
plugins --> microservices
|
||||||
|
```
|
||||||
|
|
||||||
|
You can read more about monolithic vs microservice architectures in the [Appendix below](#appendix-monolithic-vs-microservice-architecture).
|
||||||
|
|
||||||
|
|
||||||
|
## Next Gen lays the foundation for the future and brings major immediate benefits
|
||||||
|
|
||||||
|
Next Gen's new approach is important in several major ways.
|
||||||
|
|
||||||
|
### Microservices are the Future
|
||||||
|
|
||||||
|
First, decoupled microservices have become *the* way to design and deploy (web) applications after first being pioneered by the likes of Amazon in the early 2000s. And in the last five to ten years have brought microservices "for the masses" with relevant tooling and technology standardized, open-sourced and widely deployed -- not only with containerization such as Docker, Kubernetes but also in programming languages like (server-side) Javascript and Golang.
|
||||||
|
|
||||||
|
By adopting a microservice approach CKAN can reap the the benefits of what is becoming a mature and standard way to design and build (web) applications. This includes the immediate advantages of being aligned with the technical paradigm such as tooling and developer familiarity.
|
||||||
|
|
||||||
|
### Microservices bring Scalability, Reliability, Extensibility and Flexibility
|
||||||
|
|
||||||
|
In addition, and even more importantly, the microservices approach brings major benefits in:
|
||||||
|
|
||||||
|
1. **Scalability**: dramatically easier and cheaper to scale up -- and down -- in size *and* complexity. Size-wise this is because you can replicate individual services rather than the whole application. Complexity-wise this is because monolithic architectures tend to become "big" where service-oriented encourages smaller lightweight components with cleaner interfaces. This means you can have a much smaller core making it easier to install, setup and extend. It also means you can use what you need making solutions easier to maintain and upgrade.
|
||||||
|
2. **Reliability**: easier (and cheaper) to build highly reliable, high availability solutions because microservices make isolation and replication easier. For example, in a microservice architecture a problem in CKAN's harvester won't impact your main portal because they run in separate containers. Similarly, you can scale the harvester system separately from the web frontend.
|
||||||
|
3. **Extensibility**: much easier to create and maintain extensions because they are a decoupled service and interfaces are leaner and cleaner.
|
||||||
|
4. **Flexibility** aka "Bring your own tech": services can be written in any language so, for example, you can write your frontend in javascript and your backend in Python. In a monolithic architecture all parts must be written in the same language because everything is compiled together. This flexibility makes it easier to use the best tool for the job. It also makes it much easier for teams to collaborate and cooperate and fewer bottlenecks in development.
|
||||||
|
|
||||||
|
ASIDE: decoupled microservices reflect the "unix" way of building networked applications. As with the "unix way" in general, whilst this approach better -- and simpler -- in the long-run, in the short-run it often needs sustantial foundational work (those Unix authors were legends!). It may also be, at least initially, more resource intensive and more complex infrastructurally. Thus, whilst this approach is "better" it was not suprising that it was initially used for for complex and/or high end applications e.g. Amazon. This also explains why it took a while for this approach to get adoption -- it is only in the last few year that we have robust, lightweight, easy to use tooling and patterns for microservices -- "microservices for the masses" if you like.
|
||||||
|
|
||||||
|
In summary, the Next Gen approach provides an essential foundation for the continuing growth and evolution of CKAN as a platform for building world-class data portal and data management solutions.
|
||||||
|
|
||||||
|
## Evolution not Revolution: Next Gen Components Work with CKAN Classic
|
||||||
|
|
||||||
|
*Gradual evolution from CKAN classic (keep what is working, keep your investments, incremental change)*
|
||||||
|
|
||||||
|
Next Gen components are specifically designed to work with CKAN "Classic" in its current form. This means existing CKAN users can immediately benefit from Next Gen components and features whilst retaining the value of their existing investment. New (or existing) CKAN-based solutions can adopt a "hybrid" approach using components from both Classic and Next Gen. It also means that the owner of a CKAN-based solution can incrementally evolve from "Classic" to "Next Gen" by replacing one component one at a time, gaining new functionality without sacrificing existing work.
|
||||||
|
|
||||||
|
ASIDE: we're fortunate that CKAN Classic itself was ahead of its time in its level of "service-orientation". From the start, it had a very rich and robust API and it has continued to develop this with almost almost all functionality exposed via the API. It is this rich API and well factored design that makes it relatively straightforward to evolve CKAN in its current "Classic" form towards Next Gen.
|
||||||
|
|
||||||
|
## New Features plus Existing Functionality Improved
|
||||||
|
|
||||||
|
In addition to its architecture, Next Gen provides a variety of improvements and extensions to CKAN Classic's functionality. For example:
|
||||||
|
|
||||||
|
* Theming and Frontend Customization: theming and customizing CKAN's frontend has got radically easier and quicker. See [Frontend section »][frontend]
|
||||||
|
* DMS + CMS unified: integrate the full power of a modern CMS into your data portal and have one unified interface for data and content. See [Frontend section »][frontend]
|
||||||
|
* Data Explorer: the existing CKAN data preview/explorer has been completely rewritten in modern React-based Javascript (ReclineJS is now 7y old!). See [Data Explorer section »][explorer]
|
||||||
|
* Dashboards: build rich data-driven dashboards and integrate. See [Dashboards section »][dashboards]
|
||||||
|
* Harvesting: simpler, more powerful harvesting built on modern ETL. See [Harvesting section »][harvesting]
|
||||||
|
|
||||||
|
And each of these features is easily deployed into an existing CKAN solution!
|
||||||
|
|
||||||
|
[frontend]: /docs/dms/frontend
|
||||||
|
[explorer]: /docs/dms/data-explorer
|
||||||
|
[dashboards]: /docs/dms/dashboards
|
||||||
|
[harvesting]: /docs/dms/harvesting
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
The journey to Next Gen from Classic can proceed step by step -- it does not need to be a big bang. Like refurbishing and extending a house, we can add a room here or renovate a room there whilst continuing to live happily in the building (and benefitting as our new bathroom comes online, or we get a new conservatory!).
|
||||||
|
|
||||||
|
Here's an overview of the journey to Next Gen and current implementation status. More granular information on particular features may sometimes be found on the individual feature page, for example for [Harvesting here](/docs/dms/harvesting#design).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
|
||||||
|
start[Start]
|
||||||
|
themefe[Read Frontend]
|
||||||
|
authfe[Authentication in FE]
|
||||||
|
authzfe[Authorization in FE]
|
||||||
|
previews[Previews]
|
||||||
|
explorer[Explorer]
|
||||||
|
permsserv[Permissions Service]
|
||||||
|
orgs[Organizations]
|
||||||
|
|
||||||
|
|
||||||
|
subgraph Start
|
||||||
|
start
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Frontend
|
||||||
|
start --> themefe
|
||||||
|
themefe --> authfe
|
||||||
|
authfe --> authzfe
|
||||||
|
themefe --> revisioningfe[Revision UI]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Harvesting
|
||||||
|
start --> harvestetl[Harvesting ETL + Runner]
|
||||||
|
harvestetl --> harvestui[Harvest UI]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Admin UI"
|
||||||
|
managedataset[Manage Dataset]
|
||||||
|
manageorg[Manage Organization]
|
||||||
|
manageuser[Manage Users]
|
||||||
|
manageconfig[Manage Config]
|
||||||
|
|
||||||
|
start --> managedataset
|
||||||
|
start --> manageorg
|
||||||
|
managedataset --> manageconfig
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Backend (API)"
|
||||||
|
start --> permsserv
|
||||||
|
start --> revision[Backend Revisioning]
|
||||||
|
end
|
||||||
|
|
||||||
|
datastore[DataStore]
|
||||||
|
|
||||||
|
subgraph DataStore
|
||||||
|
start --> datastore
|
||||||
|
datastore --> dataload[Data Load]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Explorer
|
||||||
|
themefe --> previews
|
||||||
|
previews --> explorer
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Organizations
|
||||||
|
start --> orgs
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Key
|
||||||
|
done[Done]
|
||||||
|
nearlydone[Nearly Done]
|
||||||
|
inprogress[In Progress]
|
||||||
|
next[Next Up]
|
||||||
|
end
|
||||||
|
|
||||||
|
classDef done fill:#21bf73,stroke:#333,stroke-width:3px;
|
||||||
|
classDef nearlydone fill:lightgreen,stroke:#333,stroke-width:3px;
|
||||||
|
classDef inprogress fill:orange,stroke:#333,stroke-width:2px;
|
||||||
|
classDef next fill:pink,stroke:#333,stroke-width:1px;
|
||||||
|
|
||||||
|
class done,themefe,previews,explorer,harvestetl done;
|
||||||
|
class nearlydone,authfe,harvestui nearlydone;
|
||||||
|
class inprogress,dataload inprogress;
|
||||||
|
class next,permsserv next;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Appendix: Monolithic vs Microservice architecture
|
||||||
|
|
||||||
|
Monolithic: Libraries or modules communicate via function calls (inside one big application)
|
||||||
|
|
||||||
|
Microservices: Services communicate over a network
|
||||||
|
|
||||||
|
The best introduction and definition of microservices comes from Martin Fowler https://martinfowler.com/microservices/
|
||||||
|
|
||||||
|
> Microservice architectures will use libraries, but their primary way of componentizing their own software is by breaking down into services. We define libraries as components that are linked into a program and called using in-memory function calls, while services are out-of-process components who communicate with a mechanism such as a web service request, or remote procedure call. https://martinfowler.com/articles/microservices.html
|
||||||
|
|
||||||
|
### Monolithic
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
|
||||||
|
subgraph "Monolithic - all inside"
|
||||||
|
a
|
||||||
|
b
|
||||||
|
c
|
||||||
|
end
|
||||||
|
|
||||||
|
a --in-memory function all--> b
|
||||||
|
a --in-memory function all--> c
|
||||||
|
```
|
||||||
|
|
||||||
|
### Microservice
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph "A Container"
|
||||||
|
a
|
||||||
|
end
|
||||||
|
subgraph "B Container"
|
||||||
|
b
|
||||||
|
end
|
||||||
|
subgraph "C Container"
|
||||||
|
c
|
||||||
|
end
|
||||||
|
a -.network call.-> b
|
||||||
|
a -.network call.-> c
|
||||||
|
```
|
||||||
23
site/content/docs/dms/ckan.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
sidebar: auto
|
||||||
|
---
|
||||||
|
|
||||||
|
# CKAN Classic
|
||||||
|
|
||||||
|
CKAN (Classic) already has great documentation at: https://docs.ckan.org/
|
||||||
|
|
||||||
|
This material is a complement to those docs as well as details of our particular setup. Here, among other things, you'll learn how to:
|
||||||
|
|
||||||
|
* [Get Started with CKAN for Development -- install and run CKAN on your local machine](/docs/dms/ckan/getting-started)
|
||||||
|
* [Play around with a CKAN instance including importing and visualising data](/docs/dms/ckan/play-around)
|
||||||
|
* [Install Extensions](/docs/dms/ckan/install-extension)
|
||||||
|
* [Create Your Own Extension](/docs/dms/ckan/create-extension)
|
||||||
|
* [Client Guide](/docs/dms/ckan-client-guide)
|
||||||
|
* [FAQ](/docs/dms/ckan/faq)
|
||||||
|
|
||||||
|
[start]: /docs/dms/ckan/getting-started
|
||||||
|
[play]: /docs/dms/ckan/play-around
|
||||||
|
|
||||||
|
[CKAN]: https://ckan.org/
|
||||||
|
[docs]: https://docs.ckan.org/
|
||||||
|
|
||||||
162
site/content/docs/dms/ckan/create-extension.md
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
---
|
||||||
|
sidebar: auto
|
||||||
|
---
|
||||||
|
|
||||||
|
# Introduction
|
||||||
|
A CKAN extension is a Python package that modifies or extends CKAN. Each extension contains one or more plugins that must be added to your CKAN config file to activate the extension’s features.
|
||||||
|
|
||||||
|
## Creating and Installing extensions
|
||||||
|
1. Boot up your docker compose
|
||||||
|
```
|
||||||
|
docker-compose -f docker-compose.dev.yml up
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
2. To create an extension template using this docker composition execute:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose -f docker-compose.dev.yml exec ckan-dev /bin/bash -c "paster --plugin=ckan create -t ckanext ckanext-example_extension -o /srv/app/src_extensions"
|
||||||
|
```
|
||||||
|
|
||||||
|
This command will create an extension template in your local `./src` folder that is mounted inside the containers in the `/srv/app/src_extension` directory. Any extension cloned on the `src` folder will be installed in the CKAN container when booting up Docker Compose (`docker-compose up`). This includes installing any requirements listed in a `requirements.txt` (or `pip-requirements.txt`) file and running `python setup.py develop`.
|
||||||
|
|
||||||
|
|
||||||
|
3. Add the plugin to the `CKAN__PLUGINS` setting in your `.env` file.
|
||||||
|
|
||||||
|
```
|
||||||
|
CKAN__PLUGINS=stats text_view recline_view example_extension
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
4. Restart your docker-compose:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Shut down your instance with crtl+c and then run it again with:
|
||||||
|
docker-compose -f docker-compose.dev.yml up
|
||||||
|
```
|
||||||
|
> [!tip]CKAN will be started running on the paster development server with the '--reload' option to watch changes in the extension files.
|
||||||
|
|
||||||
|
You should see the following output in the console:
|
||||||
|
|
||||||
|
```
|
||||||
|
...
|
||||||
|
ckan-dev_1 | Installed /srv/app/src_extensions/ckanext-example_extension
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Edit the extension
|
||||||
|
|
||||||
|
Let's edit a template to change the way CKAN is displayed to the user!
|
||||||
|
|
||||||
|
1. First you will need write permissions to the extension folder since it was created by the user running docker. Replace `your_username` and execute the following command:
|
||||||
|
|
||||||
|
> [!tip]You can find out your current username by typing 'echo $USER' in the terminal.
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo chown -R <your_username>:<your_username> src/ckanext-example_extension
|
||||||
|
```
|
||||||
|
|
||||||
|
2. The previous comamand creates all the files and folder structure needed for our extension. Open `src/ckanext-example_extension/ckanext/example_extension/plugin.py` to see the main file of our extension that we will edit to add custom functionality:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import ckan.plugins as plugins
|
||||||
|
import ckan.plugins.toolkit as toolkit
|
||||||
|
|
||||||
|
|
||||||
|
class Example_ExtensionPlugin(plugins.SingletonPlugin):
|
||||||
|
plugins.implements(plugins.IConfigurer)
|
||||||
|
|
||||||
|
# IConfigurer
|
||||||
|
|
||||||
|
def update_config(self, config_):
|
||||||
|
toolkit.add_template_directory(config_, 'templates')
|
||||||
|
toolkit.add_public_directory(config_, 'public')
|
||||||
|
toolkit.add_resource('fanstatic', 'example_theme')
|
||||||
|
```
|
||||||
|
|
||||||
|
3. We will create a custom Flask Blueprint to extend our CKAN instance with more endpoints. In order to create a new blueprint and add an endpoint we need to:
|
||||||
|
- Import Blueprint and render_template from the flask module.
|
||||||
|
- Create the functions that will be used as endpoints
|
||||||
|
- Implement the IBlueprint interface in our plugin and add the new endpoint.
|
||||||
|
|
||||||
|
4. From flask import Blueprint and render_template,
|
||||||
|
|
||||||
|
```python
|
||||||
|
import ckan.plugins as plugins
|
||||||
|
import ckan.plugins.toolkit as toolkit
|
||||||
|
|
||||||
|
from flask import Blueprint, render_template
|
||||||
|
|
||||||
|
class Example_ExtensionPlugin(plugins.SingletonPlugin):
|
||||||
|
plugins.implements(plugins.IConfigurer)
|
||||||
|
|
||||||
|
# IConfigurer
|
||||||
|
|
||||||
|
def update_config(self, config_):
|
||||||
|
toolkit.add_template_directory(config_, 'templates')
|
||||||
|
toolkit.add_public_directory(config_, 'public')
|
||||||
|
toolkit.add_resource('fanstatic', 'example_extension')
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Create a new function: hello_plugin
|
||||||
|
```python
|
||||||
|
import ckan.plugins as plugins
|
||||||
|
import ckan.plugins.toolkit as toolkit
|
||||||
|
|
||||||
|
from flask import Blueprint, render_template
|
||||||
|
|
||||||
|
def hello_plugin():
|
||||||
|
u'''A simple view function'''
|
||||||
|
return u'Hello World, this is served from an extension'
|
||||||
|
|
||||||
|
class Example_ExtensionPlugin(plugins.SingletonPlugin):
|
||||||
|
plugins.implements(plugins.IConfigurer)
|
||||||
|
|
||||||
|
# IConfigurer
|
||||||
|
|
||||||
|
def update_config(self, config_):
|
||||||
|
toolkit.add_template_directory(config_, 'templates')
|
||||||
|
toolkit.add_public_directory(config_, 'public')
|
||||||
|
toolkit.add_resource('fanstatic', 'example_extension')
|
||||||
|
```
|
||||||
|
6. Implement the IBlueprint interface in our plugin and add the new endpoint.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import ckan.plugins as plugins
|
||||||
|
import ckan.plugins.toolkit as toolkit
|
||||||
|
|
||||||
|
from flask import Blueprint, render_template
|
||||||
|
|
||||||
|
def hello_plugin():
|
||||||
|
u'''A simple view function'''
|
||||||
|
return u'Hello World, this is served from an extension'
|
||||||
|
|
||||||
|
class Example_ExtensionPlugin(plugins.SingletonPlugin):
|
||||||
|
plugins.implements(plugins.IConfigurer)
|
||||||
|
plugins.implements(plugins.IBlueprint)
|
||||||
|
|
||||||
|
# IConfigurer
|
||||||
|
|
||||||
|
def update_config(self, config_):
|
||||||
|
toolkit.add_template_directory(config_, 'templates')
|
||||||
|
toolkit.add_public_directory(config_, 'public')
|
||||||
|
toolkit.add_resource('fanstatic', 'example_extension')
|
||||||
|
|
||||||
|
# IBlueprint
|
||||||
|
|
||||||
|
def get_blueprint(self):
|
||||||
|
u'''Return a Flask Blueprint object to be registered by the app.'''
|
||||||
|
# Create Blueprint for plugin
|
||||||
|
blueprint = Blueprint(self.name, self.__module__)
|
||||||
|
blueprint.template_folder = u'templates'
|
||||||
|
# Add plugin url rules to Blueprint object
|
||||||
|
blueprint.add_url_rule('/hello_plugin', '/hello_plugin', hello_plugin)
|
||||||
|
return blueprint
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Go back to the browser and navigate to http://ckan:5000/hello_plugin. You should see the value returned by our view!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Now that you have added a new view and endpoint to your plugin you are ready for the next step of the tutorial! You can also check the complete code of this plugin in the [ckan repository](https://github.com/ckan/ckan/tree/master/ckanext/example_flask_iblueprint).
|
||||||
110
site/content/docs/dms/ckan/faq.md
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
sidebar: auto
|
||||||
|
---
|
||||||
|
|
||||||
|
# FAQ
|
||||||
|
|
||||||
|
This page provides answers to some frequently asked questions.
|
||||||
|
|
||||||
|
## How to create an extension template in my local machine
|
||||||
|
|
||||||
|
You can use the `paster` command in the same way as a source install. To create an extension execute the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose -f docker-compose.dev.yml exec ckan-dev /bin/bash -c "paster --plugin=ckan create -t ckanext ckanext-myext -o /srv/app/src_extensions"
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create an extension template inside the container's folder `/srv/app/src_extensions` which is mapped to your local `src/` folder.
|
||||||
|
|
||||||
|
Now you can navigate to your local folder `src/` and see the extension created by the previous command and open the project in your favorite IDE.
|
||||||
|
|
||||||
|
|
||||||
|
## How to separate that extension in a new git repository so I can have the independence to install it in other instances
|
||||||
|
|
||||||
|
Crucial thing is to understand that extensions get their repositories on GitHub (or elsewhere). You can first create a repository for extension and later clone in `src/` or do opposite as following:
|
||||||
|
|
||||||
|
* Create the Extension, for example: `ckanext-myext`.
|
||||||
|
```
|
||||||
|
docker-compose -f docker-compose.dev.yml exec ckan-dev /bin/bash -c "paster --plugin=ckan create -t ckanext ckanext-myext -o /srv/app/src_extensions"
|
||||||
|
```
|
||||||
|
|
||||||
|
* Init your new git repository into the extension folder `src/ckanext-myext`
|
||||||
|
```
|
||||||
|
cd src/ckanext-myext
|
||||||
|
git init
|
||||||
|
```
|
||||||
|
* Configure remote/origin
|
||||||
|
```
|
||||||
|
git remote add origin <remote_repository_url>
|
||||||
|
```
|
||||||
|
* Add your files and push the first commit
|
||||||
|
```
|
||||||
|
git add .
|
||||||
|
git commit -m 'Initial Commit'
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** The `src/` folder is gitignored in `okfn/docker-ckan` repository, so initializing new git repositories inside is ok.
|
||||||
|
|
||||||
|
## How to quickly refresh the changes in my extension into the dockerized environment so I can have quick feedback of my changes
|
||||||
|
|
||||||
|
This docker-compose setup for dev environment is already configured so that it sets `debug=True` inside configuration file and auto reloads on python and templates related changes. You do not have to reload when making changes to HTML, javascript or configuration files - you just need to refresh the page in the browser.
|
||||||
|
|
||||||
|
See the CKAN images section of the [repository documentation](https://github.com/okfn/docker-ckan#ckan-images) for more detail
|
||||||
|
|
||||||
|
## How to run tests for my extension in the dockerized environment so I can have a quick test-development cycle
|
||||||
|
|
||||||
|
We write and store unit tests inside the `ckanext/myext/tests` directory. To run unit tests you need to be running the `ckan-dev` service of this docker-compose setup.
|
||||||
|
|
||||||
|
* Once running, in another terminal window run the test command:
|
||||||
|
```
|
||||||
|
docker-compose -f docker-compose.dev.yml exec ckan-dev nosetests --ckan-dev --nologcapture --reset-db -s -v --with-pylons=/srv/app/src_extensions/ckanext-myext/test.ini /srv/app/src_extensions/ckanext-myext/
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also pass nosetest arguments to debug
|
||||||
|
```
|
||||||
|
--ipdb --ipdb-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Right now all tests will be run, it is not possible to choose a specific file or test.
|
||||||
|
|
||||||
|
## How to debug my methods in the dockerized environment so I can have a better understanding of whats going on with my logic
|
||||||
|
|
||||||
|
To run a container and be able to add a breakpoint with `pdb`, run the `ckan-dev` container with the `--service-ports` option:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose -f docker-compose.dev.yml run --service-ports ckan-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start a new container, displaying the standard output in your terminal. If you add a breakpoint in a source file in the `src` folder (`import pdb; pdb.set_trace()`) you will be able to inspect it in this terminal next time the code is executed.
|
||||||
|
|
||||||
|
## How to debug core CKAN code
|
||||||
|
|
||||||
|
Currently, this docker-compose setup doesn't allow us to debug core CKAN code since it lives inside the container. However, we can do some hacks so the container uses a local clone of the CKAN core hosted in our machine. To do it:
|
||||||
|
|
||||||
|
- Create a new folder called `ckan_src` in this `docker-ckan` folder at the same level of the `src/`
|
||||||
|
- Clone ckan and checkout the version you want to debug/edit
|
||||||
|
|
||||||
|
```
|
||||||
|
git https://github.com/ckan/ckan/ ckan_src
|
||||||
|
cd ckan_src
|
||||||
|
git checkout ckan-2.8.3
|
||||||
|
```
|
||||||
|
|
||||||
|
- Edit `docker-compose.dev.yml` and add an entry to ckan-dev's and ckan-worker-dev's volumes. This will allow the docker container to access the CKAN code hosted in our machine.
|
||||||
|
|
||||||
|
```
|
||||||
|
- ./ckan_src:/srv/app/ckan_src
|
||||||
|
```
|
||||||
|
|
||||||
|
- Create a script in `ckan/docker-entrypoint.d/z_install_ckan.sh` to install CKAN inside the container from the cloned repository (instead of the one installed in the Dockerfile)
|
||||||
|
|
||||||
|
```
|
||||||
|
#!/bin/bash
|
||||||
|
echo "*********************************************"
|
||||||
|
echo "overriding with ckan installation with ckan_src"
|
||||||
|
pip install -e /srv/app/ckan_src
|
||||||
|
echo "*********************************************"
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it. This will install CKAN inside the container in development mode, from the shared folder. Now you can open the `ckan_src/` folder from your favorite IDE and start working on CKAN.
|
||||||
77
site/content/docs/dms/ckan/getting-started.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# CKAN: Getting Started for Development
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
CKAN has a rich tech stack so we have opted to standardize our instructions with Docker Compose, which will help you spin up every service in a few commands.
|
||||||
|
|
||||||
|
If you already have Docker-compose, you are ready to go!
|
||||||
|
|
||||||
|
If not, please, follow instructions on [how to install docker-compose](https://docs.docker.com/compose/install/).
|
||||||
|
|
||||||
|
On Ubuntu you can run:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install docker-compose
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cloning the repo
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://github.com/okfn/docker-ckan
|
||||||
|
# or git clone git@github.com:okfn/docker-ckan.git
|
||||||
|
cd docker-ckan
|
||||||
|
```
|
||||||
|
|
||||||
|
## Booting CKAN
|
||||||
|
|
||||||
|
Create a local environment file:
|
||||||
|
|
||||||
|
```
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Build and Run the instances:
|
||||||
|
|
||||||
|
> [!tip]'docker-compose' must be run with 'sudo'. If you want to change this, you can follow the steps below. NOTE: The 'docker' group grants privileges equivalent to the 'root' user.
|
||||||
|
|
||||||
|
Create the `docker` group: `sudo groupadd docker`
|
||||||
|
|
||||||
|
Add your user to the `docker` group: `sudo usermod -aG docker $USER`
|
||||||
|
|
||||||
|
Change the storage directory ownership from `root` to `ckan` by adding the commads below to the `ckan/Dockerfile.dev`
|
||||||
|
|
||||||
|
```
|
||||||
|
RUN mkdir -p /var/lib/ckan/storage/uploads
|
||||||
|
RUN chown -R ckan:ckan /var/lib/ckan/storage
|
||||||
|
```
|
||||||
|
|
||||||
|
At this point, you can log out and log back in for these changes to apply. You can also use the command `newgrp docker` to temporarily enable the new group for the current terminal session.
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose -f docker-compose.dev.yml up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
When you see this log message:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can navigate to `http://localhost:5000`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
and log in with the credentials that docker-compose setup created for you [user: `ckan_admin` password:`test1234`].
|
||||||
|
|
||||||
|
>[!tip]To learn key concepts about CKAN, including what it is and how it works, you can read the User Guide.
|
||||||
|
[CKAN User Guide](https://docs.ckan.org/en/2.8/user-guide.html).
|
||||||
|
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
[Play around with CKAN portal](/docs/dms/ckan/play-around).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
Login / Logout button breaks the experience:
|
||||||
|
|
||||||
|
- Change the URL from `http://ckan:5000` to `http://localhost:5000`. A complete fix is described in the [Play around with CKAN portal](/docs/dms/ckan/play-around). (Your next step. ;))
|
||||||
76
site/content/docs/dms/ckan/install-extension.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
sidebar: auto
|
||||||
|
---
|
||||||
|
|
||||||
|
# Installing extensions
|
||||||
|
|
||||||
|
A CKAN extension is a Python package that modifies or extends CKAN. Each extension contains one or more plugins that must be added to your CKAN config file to activate the extension’s features.
|
||||||
|
|
||||||
|
In this sections we will teach you only how to install existing extensions. See [next steps](/docs/dms/ckan/create-extension) in case you need to create or modify extensions
|
||||||
|
|
||||||
|
## Add new extension
|
||||||
|
|
||||||
|
Lets install [Hello World](https://github.com/rclark/ckanext-helloworld) on the portal. For that we need to do 2 thing:
|
||||||
|
|
||||||
|
1. Install extension when building docker image
|
||||||
|
2. Add new extension to CKAN plugins
|
||||||
|
|
||||||
|
### Install extension on docker build
|
||||||
|
|
||||||
|
For this we need to modify Dockerfile for ckan service. Let's edit it:
|
||||||
|
|
||||||
|
```
|
||||||
|
vi ckan/Dockerfile.dev
|
||||||
|
|
||||||
|
# Add following
|
||||||
|
RUN pip install -e git+https://github.com/rclark/ckanext-helloworld.git#egg=ckanext-helloworld
|
||||||
|
```
|
||||||
|
|
||||||
|
*Note:* In this example we use vi editor, but you can choose any of your choice.
|
||||||
|
|
||||||
|
### Add new extension to plugins
|
||||||
|
|
||||||
|
We need to modify .env file for that - Search for `CKAN_PLUGINS` and add new extension to the existing list:
|
||||||
|
|
||||||
|
```
|
||||||
|
vi .env
|
||||||
|
|
||||||
|
CKAN__PLUGINS=helloworld envvars image_view text_view recline_view datastore datapusher
|
||||||
|
```
|
||||||
|
|
||||||
|
## Check extension is installed
|
||||||
|
|
||||||
|
After modifying configuration files you will need to restart the portal. If your CKAN protal is up and running bring it down and re-start
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose -f docker-compose.dev.yml stop
|
||||||
|
docker-compose -f docker-compose.dev.yml up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check what extensions you already have:
|
||||||
|
|
||||||
|
http://ckan:5000/api/3/action/status_show
|
||||||
|
|
||||||
|
Response should include list of all extensions including `helloworld` in it.
|
||||||
|
|
||||||
|
```
|
||||||
|
"extensions": [
|
||||||
|
"envvars",
|
||||||
|
"helloworld",
|
||||||
|
"image_view",
|
||||||
|
"text_view",
|
||||||
|
"recline_view",
|
||||||
|
"datastore",
|
||||||
|
"datapusher"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check the extension is actually working
|
||||||
|
|
||||||
|
This extension simply adds new route `/hello/world/name` to the base ckan and says hello
|
||||||
|
|
||||||
|
http://ckan:5000/hello/world/John-Doe
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
[Create your own extension](/docs/dms/ckan/create-extension)
|
||||||
285
site/content/docs/dms/ckan/play-around.md
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
---
|
||||||
|
sidebar: auto
|
||||||
|
---
|
||||||
|
|
||||||
|
# How to play around with CKAN
|
||||||
|
|
||||||
|
In this section, we are going to show some basic functionality of CKAN focused on the API.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- We assume you've already completed the [Getting Started Guide](/docs/dms/ckan/getting-started).
|
||||||
|
- You have a basic understanding of Key data portal concepts:
|
||||||
|
|
||||||
|
CKAN is a tool for making data portals to manage and publish datasets. You can read about the key concepts such as Datasets and Organizations in the User Guide -- or you can just dive in and play around!
|
||||||
|
|
||||||
|
https://docs.ckan.org/en/2.9/user-guide.html
|
||||||
|
|
||||||
|
>[!tip]
|
||||||
|
Install a [JSON formatter plugin for Chrome](https://chrome.google.com/webstore/detail/json-formatter/bcjindcccaagfpapjjmafapmmgkkhgoa?hl=en) or browser of your choice.
|
||||||
|
|
||||||
|
If you are familiar with the command line tool `curl`, you can use that.
|
||||||
|
|
||||||
|
In this tutorial, we will be using `curl`, but for most of the commands, you can paste a link in your browser. For POST commands, you can use [Postman](https://www.getpostman.com/) or [Google Chrome Plugin](https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop).
|
||||||
|
|
||||||
|
|
||||||
|
## First steps
|
||||||
|
|
||||||
|
>[!tip]
|
||||||
|
By default the portal is accessible on http://localhost:5000. Let's update your `/etc/hosts` to access it on http://ckan:5000:
|
||||||
|
|
||||||
|
```
|
||||||
|
vi /etc/hosts # You can use the editor of your choice
|
||||||
|
# add following
|
||||||
|
127.0.0.1 ckan
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
At this point, you should be able to access the portal on http://ckan:5000.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Let's add some fixtures to it. For software, a fixture is something used consistently (in this case, data for you to play around with). Run the following from your terminal (do NOT cut the previous docker process as this one depends on the already launched docker, run in another terminal):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker-compose -f docker-compose.dev.yml exec ckan-dev ckan seed basic
|
||||||
|
```
|
||||||
|
|
||||||
|
Optionally you can `exec` into a running container using
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker exec -it [name of container] sh
|
||||||
|
```
|
||||||
|
|
||||||
|
and run the `ckan` command there
|
||||||
|
```sh
|
||||||
|
ckan seed basic
|
||||||
|
```
|
||||||
|
|
||||||
|
You should be able to see 2 new datasets on home page:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
To get more details on ckan commands please visit [CKAN Commands Reference](https://docs.ckan.org/en/2.9/maintaining/cli.html#ckan-commands-reference).
|
||||||
|
|
||||||
|
### Check CKAN API
|
||||||
|
|
||||||
|
This tutorial focuses on the CKAN API as that is central to development work and requires more guidance. We also invite you to explore the user interface which you can do directly yourself by visiting http://ckan:5000/.
|
||||||
|
|
||||||
|
#### Let's check the portal status
|
||||||
|
|
||||||
|
Go to http://ckan:5000/api/3/action/status_show.
|
||||||
|
|
||||||
|
You should see something like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"help": "https://ckan:5000/api/3/action/help_show?name=status_show",
|
||||||
|
"success": true,
|
||||||
|
"result": {
|
||||||
|
"ckan_version": "2.9.x",
|
||||||
|
"site_url": "https://ckan:5000",
|
||||||
|
"site_description": "Testing",
|
||||||
|
"site_title": "CKAN Demo",
|
||||||
|
"error_emails_to": null,
|
||||||
|
"locale_default": "en",
|
||||||
|
"extensions": [
|
||||||
|
"envvars",
|
||||||
|
...
|
||||||
|
"demo"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This means everything is OK: the CKAN portal is up and running, the API is working as expected. In case you see an internal server error, please check the logs in your terminal.
|
||||||
|
|
||||||
|
### A Few useful API endpoints to start with
|
||||||
|
|
||||||
|
CKAN's Action API is a powerful, RPC-style API that exposes all of CKAN's core features to API clients. All of a CKAN website's core functionality (everything you can do with the web interface and more) can be used by external code that calls the CKAN API.
|
||||||
|
|
||||||
|
#### Get a list of all datasets on the portal
|
||||||
|
|
||||||
|
http://ckan:5000/api/3/action/package_list
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"help": "http://ckan:5000/api/3/action/help_show?name=package_list",
|
||||||
|
"success": true,
|
||||||
|
"result": ["annakarenina", "warandpeace"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Search for a dataset
|
||||||
|
|
||||||
|
http://ckan:5000/api/3/action/package_search?q=russian
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"help": "http://ckan:5000/api/3/action/help_show?name=package_search",
|
||||||
|
"success": true,
|
||||||
|
"result": {
|
||||||
|
"count": 2,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get dataset details
|
||||||
|
|
||||||
|
http://ckan:5000/api/3/action/package_show?id=annakarenina
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"help": "http://ckan:5000/api/3/action/help_show?name=package_show",
|
||||||
|
"success": true,
|
||||||
|
"result": {
|
||||||
|
"license_title": "Other (Open)",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Search for a resource
|
||||||
|
|
||||||
|
http://ckan:5000/api/3/action/resource_search?query=format:plain%20text
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"help": "http://ckan:5000/api/3/action/help_show?name=resource_search",
|
||||||
|
"success": true,
|
||||||
|
"result": {
|
||||||
|
"count": 1,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"mimetype": null,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get resource details
|
||||||
|
|
||||||
|
http://ckan:5000/api/3/action/resource_show?id=288455e8-c09c-4360-b73a-8b55378c474a
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"help": "http://ckan:5000/api/3/action/help_show?name=resource_show",
|
||||||
|
"success": true,
|
||||||
|
"result": {
|
||||||
|
"mimetype": null,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
*Note:* These are only a few examples. You can find a full list of API actions in the [CKAN API guide](https://docs.ckan.org/en/2.9/api/#action-api-reference).
|
||||||
|
|
||||||
|
### Create Organizations, Datasets and Resources
|
||||||
|
|
||||||
|
There are 4 steps:
|
||||||
|
|
||||||
|
- Get an API key;
|
||||||
|
- Create an organization;
|
||||||
|
- Create dataset inside an organization (you can't create a dataset without a parent organization);
|
||||||
|
- And add resources to the dataset.
|
||||||
|
|
||||||
|
#### Get a Sysadmin Key
|
||||||
|
|
||||||
|
To create your first dataset, you need an API key.
|
||||||
|
|
||||||
|
You can see sysadmin credentials in the file `.env`. By default, they should be
|
||||||
|
|
||||||
|
- Username: `ckan_admin`
|
||||||
|
- Password: `test1234`
|
||||||
|
|
||||||
|
1. Navigate to http://ckan:5000/user/login and login.
|
||||||
|
2. Click on your username (`ckan_admin`) in the upright corner.
|
||||||
|
3. Scroll down until you see `API Key` on the left side of the screen and copy its value. It should look similar to `c7325sd4-7sj3-543a-90df-kfifsdk335`.
|
||||||
|
|
||||||
|
#### Create Organization
|
||||||
|
|
||||||
|
You can create an organization from the browser easily, but let's use [CKAN API](https://docs.ckan.org/en/2.9/api/#ckan.logic.action.create.organization_create) to do so.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -X POST http://ckan:5000/api/3/action/organization_create -H "Authorization: 9c04a69d-79f4-4b4b-b4e1-f2ac31ed961c" -d '{
|
||||||
|
"name": "demo-organization",
|
||||||
|
"title": "Demo Organization",
|
||||||
|
"description": "This is my awesome organization"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"help": "http://ckan:5000/api/3/action/help_show?name=organization_create",
|
||||||
|
"success": true,
|
||||||
|
"result": {"users": [
|
||||||
|
{
|
||||||
|
"email_hash":
|
||||||
|
...
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create Dataset
|
||||||
|
|
||||||
|
Now, we are ready to create our first dataset.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -X POST http://ckan:5000/api/3/action/package_create -H "Authorization: 9c04a69d-79f4-4b4b-b4e1-f2ac31ed961c" -d '{
|
||||||
|
"name": "my-first-dataset",
|
||||||
|
"title": "My First Dataset",
|
||||||
|
"description": "This is my first dataset!",
|
||||||
|
"owner_org": "demo-organization"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"help": "http://ckan:5000/api/3/action/help_show?name=package_create",
|
||||||
|
"success": true,
|
||||||
|
"result": {
|
||||||
|
"license_title": null,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create an empty (draft) dataset.
|
||||||
|
|
||||||
|
#### Add a resource to it
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -X POST http://ckan:5000/api/3/action/resource_create -H "Authorization: 9c04a69d-79f4-4b4b-b4e1-f2ac31ed961c" -d '{
|
||||||
|
"package_id": "my-first-dataset",
|
||||||
|
"url": "https://raw.githubusercontent.com/frictionlessdata/test-data/master/files/csv/100kb.csv",
|
||||||
|
"description": "This is the best resource ever!" ,
|
||||||
|
"name": "brand-new-resource"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"help": "http://ckan:5000/api/3/action/help_show?name=resource_create",
|
||||||
|
"success": true,
|
||||||
|
"result": {
|
||||||
|
"cache_last_updated": null,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it! Now you should be able to see your dataset on the portal at http://ckan:5000/dataset/my-first-dataset.
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
* [Install Extensions](/docs/dms/ckan/install-extension).
|
||||||
81
site/content/docs/dms/cms-for-data-portals.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
sidebar: auto
|
||||||
|
---
|
||||||
|
|
||||||
|
# Content Management System (CMS) for Data Portals
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
When selecting a CMS solution for Data Portals, we always recommend using headless CMS solution as it provides full flexibility when building your system. Headless CMS means only content (no HTML, CSS, JS) is created in the CMS backend and delivered to Frontend via API.
|
||||||
|
|
||||||
|
> The traditional CMS approach to managing content put everything in one big bucket — content, images, HTML, CSS. This made it impossible to reuse the content because it was commingled with code. Read more - https://www.contentful.com/r/knowledgebase/what-is-headless-cms/.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Core features:
|
||||||
|
|
||||||
|
* Create and manage blog posts (or news), e.g., `/news/abcd`
|
||||||
|
* Create and manage static pages, e.g., `/about`, `/privacy` etc.
|
||||||
|
|
||||||
|
Important features:
|
||||||
|
|
||||||
|
* User management, e.g., ability to manage editors so that multiple users can edit content.
|
||||||
|
* User roles, e.g., ability to assign different roles for users so that we can have admins, editors, reviewers.
|
||||||
|
* Draft content, e.g., useful when working on content development for review/feedback loop. However, this is not essential if you have multiple environments.
|
||||||
|
* A syntax for writing content with text formatting, multi-level headings, links, images, videos, bullet points. For example, markdown.
|
||||||
|
* User-friendly interface (text editor) to write content.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
|
||||||
|
CMS -.-> Blog["Blog or news section"]
|
||||||
|
CMS -.-> IndBlog["Individual blog post"]
|
||||||
|
CMS -.-> About["About page content"]
|
||||||
|
CMS -.-> TC["Terms and conditions page content"]
|
||||||
|
CMS -.-> Privacy["Privacy policy"]
|
||||||
|
CMS -.-> Other["Other static pages"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
Headless CMS options:
|
||||||
|
|
||||||
|
* WordPress (headless option)
|
||||||
|
* Drupal (headless option)
|
||||||
|
* TinaCMS - https://tina.io/
|
||||||
|
* Git-based CMS - custom soltion based on Git repository.
|
||||||
|
* Strapi - https://docs.strapi.io/developer-docs/latest/getting-started/introduction.html
|
||||||
|
* Ghost - https://ghost.org/docs/
|
||||||
|
* CKAN Pages (built-in CMS option) - https://github.com/ckan/ckanext-pages
|
||||||
|
|
||||||
|
*Note, there are loads of CMS available both in open-source and proprietary software. We are only considering few of them in this article and our requirement is that we should be able to fetch content via API (headless CMS). Readers are welcome to add more options into the list.*
|
||||||
|
|
||||||
|
Comparison criteria:
|
||||||
|
|
||||||
|
* Self-hosting (note this isn't criteria for most of projects and using managed hosting is a better option sometimes)
|
||||||
|
* Free and open source
|
||||||
|
* Multi language posts (unnecessary if your portal is single language)
|
||||||
|
|
||||||
|
Comparison:
|
||||||
|
|
||||||
|
| Options | Hosting | Free | Multi language |
|
||||||
|
| -------- | -------- | -------- | -------------- |
|
||||||
|
| Drupal | Tedious | Yes | Not straigtforward|
|
||||||
|
| WordPress| Tedious | Yes | Terrible UX |
|
||||||
|
| TinaCMS | Medium | Yes | Limited |
|
||||||
|
| Git-based| Easy | Yes | Custom |
|
||||||
|
| Strapi | Medium | Yes | Simple |
|
||||||
|
| Ghost | Medium | Yes | Simple |
|
||||||
|
| CKAN Pages| Easy | Yes | ? |
|
||||||
|
|
||||||
|
|
||||||
|
## Conclusion and recommendation
|
||||||
|
|
||||||
|
Final decision should be made based on the following items:
|
||||||
|
|
||||||
|
* How often editors will create content? E.g., daily, weekly, monthly, occasionally.
|
||||||
|
* How much content you already have and need to migrate?
|
||||||
|
* How many content editors you are planning to have? What are their technical expertise?
|
||||||
|
* Is there any specific requirements, e.g., you must host in your cloud?
|
||||||
|
|
||||||
|
By default, we would recommend considering options such as Strapi, TinaCMS and Git-based CMS. We can even start with simple CKAN's built-in Pages and only move to sophisticated CMS once it is required.
|
||||||
163
site/content/docs/dms/dashboards.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# Dashboards
|
||||||
|
|
||||||
|
## What you can do?
|
||||||
|
|
||||||
|
* Describe vizualizations in JSON and create interactive widgets
|
||||||
|
* Customize dashboard layout using well-known HTML
|
||||||
|
* Style dashboard design with TailwindCSS utility classes
|
||||||
|
* Rapidly create basic charts using "simple" graphing specification
|
||||||
|
* Create advanced widgets by utilizing "vega" visualization grammar
|
||||||
|
|
||||||
|
## How?
|
||||||
|
|
||||||
|
To create a dashboard you need to have some basic knowledge of:
|
||||||
|
|
||||||
|
* git
|
||||||
|
* JSON
|
||||||
|
* HTML
|
||||||
|
|
||||||
|
Before proceeding further, make sure you have forked the dashboards repository - https://github.com/datopian/dashboards.
|
||||||
|
|
||||||
|
### Create a directory for your dashboard
|
||||||
|
|
||||||
|
In the root of the project, create a directory for your dashboard. Name of this directory is the name of your dashboard so make it short and meaningful. Here is some good examples:
|
||||||
|
|
||||||
|
* population
|
||||||
|
* environment
|
||||||
|
* housing
|
||||||
|
|
||||||
|
So that your dashboard will be available at https://domain.com/dashboards/your-dashboard-name.
|
||||||
|
|
||||||
|
Note that your dashboard directory will contain 2 files:
|
||||||
|
|
||||||
|
* `index.html` - [HTML template](#Set-up-your-layout)
|
||||||
|
* `config.json` - [configurations for widgets](#Configure-vizualizations)
|
||||||
|
|
||||||
|
### Set up your layout
|
||||||
|
|
||||||
|
You need to prepare HTML template for your dashboard. No need to create entire HTML page but only snippet that is needed to inject the widgets:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<h1>My example dashboard</h1>
|
||||||
|
<div id="widget1"></div>
|
||||||
|
<div id="widget2"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
In the example above, we've created 2 div elements that we can reference by id when configuring vizualizations.
|
||||||
|
|
||||||
|
Note that you can add any HTML tags and make your layout stand out. In the next section we'll explain how you do some stylings.
|
||||||
|
|
||||||
|
### Style it
|
||||||
|
|
||||||
|
This step is optional but if you have a dashboard with lots of widgets and metadata, you might want to style it so it appears nicely:
|
||||||
|
|
||||||
|
* Use TailwindCSS utility classes **(recommended)**
|
||||||
|
* Official docs - https://tailwindcss.com/
|
||||||
|
* Cheat sheet - https://nerdcave.com/tailwind-cheat-sheet
|
||||||
|
* Add inline CSS
|
||||||
|
|
||||||
|
Example of using TailwindCSS utility classes:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<h1 class="text-gray-700 text-lg">My example dashboard</h1>
|
||||||
|
<div class="inline-block bg-gray-200 m-10" id="widget1"></div>
|
||||||
|
<div class="inline-block bg-gray-200 m-10" id="widget2"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configure vizualizations
|
||||||
|
|
||||||
|
In your config file `config.json` you can describe your dashboard in the following way:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"widgets": [],
|
||||||
|
"datasets": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* `widgets` - a list of objects where each object contains information about where a widget should be injected and how it should look like (see below for examples).
|
||||||
|
* `datasets` - a list of dataset URLs.
|
||||||
|
|
||||||
|
Example of a minimal widget object:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"elementId": "widget1",
|
||||||
|
"view": {
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"datasetId": "",
|
||||||
|
"name": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"specType": "",
|
||||||
|
"spec": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
where:
|
||||||
|
|
||||||
|
* `elementId` - is "id" of the HTML tag you want to use as a container of your widget. See [how we defined it here](#Set-up-your-layout).
|
||||||
|
* `view` - descriptor of a vizualization (widget).
|
||||||
|
* `resources` - a list of resources needed for a widget and required manipulations (transformations).
|
||||||
|
* `datasetId` - the id (name) of the dataset from which the resource is extracted.
|
||||||
|
* `name` - name of the resource.
|
||||||
|
* `transform` - transformations required for a resource (optional). If you want to learn more about transforms:
|
||||||
|
* Filtering data and applying formula: https://datahub.io/examples/transform-examples-on-co2-fossil-global#readme
|
||||||
|
* Sampling: https://datahub.io/examples/example-sample-transform-on-currency-codes#readme
|
||||||
|
* Aggregating data: https://datahub.io/examples/transform-example-gdp-uk#readme
|
||||||
|
* `specType` - type of a widget, e.g., `simple`, `vega` or `figure`.
|
||||||
|
* `spec` - specification for selected widget type. See below for examples.
|
||||||
|
* `title`, `legend`, `footer` - these are optional metadata for a widget. All must be a string.
|
||||||
|
|
||||||
|
#### Basic charts
|
||||||
|
|
||||||
|
Simple graph spec is the easiest and quickest way to specify a vizualization. Using simple graph spec you can generate line and bar charts:
|
||||||
|
|
||||||
|
https://frictionlessdata.io/specs/views/#simple-graph-spec
|
||||||
|
|
||||||
|
#### Advanced vizualizations
|
||||||
|
|
||||||
|
Please check this instructions to create advanced graphs via Vega specification:
|
||||||
|
|
||||||
|
https://frictionlessdata.io/specs/views/#vega-spec
|
||||||
|
|
||||||
|
#### Figure widget
|
||||||
|
|
||||||
|
The figure widget is used to display a single value from a dataset. For example, you might want to show latest unemployment rate in your dashboard so that it indicates current status of your cities economy. See left-hand side widgets here - https://london.datahub.io/.
|
||||||
|
|
||||||
|
A specification for the figure widget would have the following structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"fieldName": "",
|
||||||
|
"suffix": "",
|
||||||
|
"prefix": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
where "fieldName" attribute will be used to extract specific value from a row. The "suffix" and "prefix" attributes are optional strings that is used to surround a figure, e.g., you can prepend a percent sign to indicate the number's value.
|
||||||
|
|
||||||
|
Note that the first row of the data is used which means you need to transform data to show the relevant value. See this example for details - https://github.com/datopian/dashboard-js/blob/master/example/script.js#L12-L22.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
Check out carbon emission per capita dashboard as an example of creating advanced vizualizations:
|
||||||
|
|
||||||
|
https://github.com/datopian/dashboards/tree/master/co2-emission-by-nation
|
||||||
|
|
||||||
|
## Share it with the world!
|
||||||
|
|
||||||
|
To make your dashboard live on the data portal, you need to:
|
||||||
|
|
||||||
|
1. Simply create a pull request
|
||||||
|
2. Wait until your work gets reviewed and merged into "master" branch.
|
||||||
|
3. Implement any requested changes in your work.
|
||||||
|
4. Done! Your dashboard is now available at https://domain.com/dashboards/your-dashboard-name
|
||||||
|
|
||||||
|
|
||||||
|
## Research
|
||||||
|
|
||||||
|
* http://dashing.io/ - no longer maintained as of 2016
|
||||||
|
* Replaced by https://smashing.github.io/
|
||||||
358
site/content/docs/dms/dashboards/hdx-dashboards-notes.md
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
# HDX Technical Architecture for Quick Dashboards
|
||||||
|
|
||||||
|
Notes from analysis and discussion in 2018.
|
||||||
|
|
||||||
|
# Concepts
|
||||||
|
|
||||||
|
* Bite (View): a description of an individual chart / map / fact and its data (source)
|
||||||
|
* bite (for Simon): title, desc, data (compiled), uniqueid, map join info
|
||||||
|
* view (Data Package views): title, desc, data sources (on parent data package), transforms, ...
|
||||||
|
* compiled view: title, desc, data (compiled)
|
||||||
|
* Data source:
|
||||||
|
* Single HXL file (Currently, Simon's approach requires that all the data is in a single table so there is always a single data source.)
|
||||||
|
* Data Package(s)
|
||||||
|
* Creator / Editor: creating and editing the dashboard (given the source datasets)
|
||||||
|
* Renderer: given dashboard config render the dashboard
|
||||||
|
|
||||||
|
# Dashboard Creator
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
|
||||||
|
datahxl[data+hxl]
|
||||||
|
layouts[Layout options]
|
||||||
|
dashboard["Dashboard (config)<br/><br/>(Layout, Data Sources, Selected Bites)"]
|
||||||
|
editor[Editor]
|
||||||
|
bites[Bites<br /><em>potential charts, maps etc</em>]
|
||||||
|
|
||||||
|
datahxl --suggester--> bites
|
||||||
|
bites --> editor
|
||||||
|
layouts --> editor
|
||||||
|
editor --save--> dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Bite generation
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
|
||||||
|
data[data with hxl] --> inferbites(("Iterate Recipes<br/>and see what<br/>matches"))
|
||||||
|
inferbites --> possmatches[List of potential bites]
|
||||||
|
possmatches --no map info--> done[Bite finished]
|
||||||
|
possmatches --lat+lon--> done
|
||||||
|
possmatches --geo info--> maplink(("Check pcodes<br/> and link<br/>map server url"))
|
||||||
|
maplink -.-> fuzzy((Fuzzy Matcher))
|
||||||
|
fuzzy --> done
|
||||||
|
maplink --> done
|
||||||
|
maplink --error--> nobite[No Bite]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extending to non-HXL data
|
||||||
|
|
||||||
|
It is easy to extend this to non-HXL data by using base HXL types and inference e.g.
|
||||||
|
|
||||||
|
```
|
||||||
|
date => #date
|
||||||
|
geo => #geo+lon
|
||||||
|
geo => #geo+lat
|
||||||
|
string/category => #indicator
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
|
||||||
|
data[data + syntax]
|
||||||
|
datahxl[data+hxl]
|
||||||
|
layouts[layout options]
|
||||||
|
dashboard["Dashboard (config)"]
|
||||||
|
editor[Editor]
|
||||||
|
bites[Bites<br /><em>potential charts, maps etc</em>]
|
||||||
|
|
||||||
|
data --infer--> datahxl
|
||||||
|
datahxl --suggester--> bites
|
||||||
|
bites --> editor
|
||||||
|
layouts --> editor
|
||||||
|
editor --save--> dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
# Dashboard Renderer
|
||||||
|
|
||||||
|
Rendering the dashboard involves:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
|
||||||
|
bites[Compiled Bites/Views]
|
||||||
|
renderer["Renderer<br/>(Layout + charting / mapping libs)"]
|
||||||
|
data[Data]
|
||||||
|
|
||||||
|
subgraph Dashboard Config
|
||||||
|
bitesconf[Bites/Views Config]
|
||||||
|
layoutconf[Layout Config]
|
||||||
|
end
|
||||||
|
|
||||||
|
bitecompiler[Bite/View Compiler]
|
||||||
|
bitecompiler --> bites
|
||||||
|
|
||||||
|
bitesconf --> bitecompiler
|
||||||
|
data --> bitecompiler
|
||||||
|
|
||||||
|
layoutconf --> renderer
|
||||||
|
bites --> renderer
|
||||||
|
|
||||||
|
renderer --> dashboard[HTML Dashboard]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Compiled View generation
|
||||||
|
|
||||||
|
See https://docs.datahub.io/developers/views/
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
# Architecture Proposal
|
||||||
|
|
||||||
|
* data loader library
|
||||||
|
* File: rows, fields (rows, columns)
|
||||||
|
* type inference (?)
|
||||||
|
* syntax: table schema infer
|
||||||
|
* semantics (not now)
|
||||||
|
* data transform library (include hxl support)
|
||||||
|
* suggester library
|
||||||
|
* renderer library
|
||||||
|
|
||||||
|
Interfaces / Objects
|
||||||
|
|
||||||
|
* File
|
||||||
|
* (Dataset)
|
||||||
|
* Transform
|
||||||
|
* Algorithm / Recipe
|
||||||
|
* Bite / View
|
||||||
|
* Ordered Set of Bites
|
||||||
|
* Dashboard
|
||||||
|
|
||||||
|
## File (and Dataset)
|
||||||
|
|
||||||
|
http://okfnlabs.org/blog/2018/02/15/design-pattern-for-a-core-data-library.html
|
||||||
|
|
||||||
|
https://github.com/datahq/data.js
|
||||||
|
|
||||||
|
File
|
||||||
|
rows
|
||||||
|
descriptor
|
||||||
|
schema
|
||||||
|
schema
|
||||||
|
|
||||||
|
## Recipe
|
||||||
|
|
||||||
|
```json=
|
||||||
|
{
|
||||||
|
'id':'chart0001',
|
||||||
|
'type':'chart',
|
||||||
|
'subType':'row',
|
||||||
|
'ingredients':[{'name':'what','tags':['#activity-code-id','#sector']}],
|
||||||
|
'criteria':['what > 4', 'what < 11'],
|
||||||
|
'variables': ['what', 'count()'],
|
||||||
|
'chart':'',
|
||||||
|
'title':'Count of {1}',
|
||||||
|
'priority': 8,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bite / Compiled View
|
||||||
|
|
||||||
|
```json=
|
||||||
|
{
|
||||||
|
bite: array [...data for chart...],
|
||||||
|
id: string "...chart bite ID...",
|
||||||
|
priority: number,
|
||||||
|
subtype: string "...bite subtype - row, pie...",
|
||||||
|
title: string "...title of bite...",
|
||||||
|
type: string "...bite type...",
|
||||||
|
uniqueID: string "...unique ID combining bite and data structure",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
=>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Dashboard
|
||||||
|
|
||||||
|
```json=
|
||||||
|
{
|
||||||
|
"title":"",
|
||||||
|
"subtext":"",
|
||||||
|
"filtersOn":true,
|
||||||
|
"filters":[],
|
||||||
|
"headlinefigures":0,
|
||||||
|
"headlinefigurecharts":[
|
||||||
|
],
|
||||||
|
"grid":"grid5",
|
||||||
|
"charts":[
|
||||||
|
{
|
||||||
|
"data":"https://proxy.hxlstandard.org/data.json?filter01=append&append-dataset01-01=https%3A%2F%2Fdocs.google.com%2Fspreadsheets%2Fd%2F1FLLwP6nxERjo1xLygV7dn7DVQwQf0_5tIdzrX31HjBA%2Fedit%23gid%3D0&filter02=select&select-query02-01=%23status%3DFunctional&url=https%3A%2F%2Fdocs.google.com%2Fspreadsheets%2Fd%2F1R9zfMTk7SQB8VoEp4XK0xAWtlsQcHgEvYiswZsj9YA4%2Fedit%23gid%3D0",
|
||||||
|
"chartID":""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":"https://proxy.hxlstandard.org/data.json?filter01=append&append-dataset01-01=https%3A%2F%2Fdocs.google.com%2Fspreadsheets%2Fd%2F1FLLwP6nxERjo1xLygV7dn7DVQwQf0_5tIdzrX31HjBA%2Fedit%23gid%3D0&filter02=select&select-query02-01=%23status%3DFunctional&url=https%3A%2F%2Fdocs.google.com%2Fspreadsheets%2Fd%2F1R9zfMTk7SQB8VoEp4XK0xAWtlsQcHgEvYiswZsj9YA4%2Fedit%23gid%3D0",
|
||||||
|
"chartID":""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
var config = {
|
||||||
|
layout: 2x2 // in city-indicators dashboard is handcrafted in layout
|
||||||
|
widgets: [
|
||||||
|
{
|
||||||
|
elementId / data-id: ...
|
||||||
|
view: {
|
||||||
|
metadata: { title, sources: "World Bank"}
|
||||||
|
resources: rule for creating compiled list of resources. [ { datasetId: ..., resourceId: ..., transform: ...} ]
|
||||||
|
specType:
|
||||||
|
viewspec:
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
},
|
||||||
|
]
|
||||||
|
datasets: [
|
||||||
|
list of data package urls ...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Simon's example
|
||||||
|
|
||||||
|
https://simonbjohnson.github.io/hdx-iom-dtm/
|
||||||
|
|
||||||
|
```javascript=
|
||||||
|
{
|
||||||
|
// metadata for dashboard
|
||||||
|
"title":"IOM DTM Example",
|
||||||
|
"subtext":" ....",
|
||||||
|
"headlinefigures": 3,
|
||||||
|
"grid": "grid5", // user chosen layout for dashboard. Choice of 10 grids
|
||||||
|
"headlinefigurecharts": [ //widgets - headline widget
|
||||||
|
{
|
||||||
|
"data": "https://beta.proxy.hxlstandard.org/data/1d0a79/download/africa-dtm-baseline-assessments-topline.csv",
|
||||||
|
"chartID": "text0013/#country+name/1" // bite Id
|
||||||
|
// elementId: ... // implicit from order in grid ...
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": "https://beta.proxy.hxlstandard.org/data/1d0a79/download/africa-dtm-baseline-assessments-topline.csv",
|
||||||
|
"chartID": "text0012/#affected+hh+idps/5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": "https://beta.proxy.hxlstandard.org/data/1d0a79/download/africa-dtm-baseline-assessments-topline.csv",
|
||||||
|
"chartID":"text0012/#affected+idps+ind/6"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"charts": [ // chart widgets
|
||||||
|
{
|
||||||
|
"data": "https://beta.proxy.hxlstandard.org/data/1d0a79/download/africa-dtm-baseline-assessments-topline.csv",
|
||||||
|
"chartID": "map0002/#adm1+code/4/#affected+idps+ind/6",
|
||||||
|
"scale":"log" // chart config ...
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": "https://beta.proxy.hxlstandard.org/data/1d0a79/download/africa-dtm-baseline-assessments-topline.csv",
|
||||||
|
"chartID": "chart0009/#country+name/1/#affected+idps+ind/6",
|
||||||
|
"sort":"descending"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Algorithm
|
||||||
|
|
||||||
|
1. Extract the data references to a common list of datasets and fetch them
|
||||||
|
2. You generate compiled data via hxl.js plus own code transforming to final data for charting etc
|
||||||
|
|
||||||
|
```
|
||||||
|
function transformChart(rawSourceData (csv parsed), bite) => [ [ ...], [...]] - data for chart
|
||||||
|
|
||||||
|
hxl.js
|
||||||
|
custom code
|
||||||
|
|
||||||
|
function transformMap
|
||||||
|
|
||||||
|
function transformText ...
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
https://github.com/SimonbJohnson/hxlbites.js
|
||||||
|
|
||||||
|
https://github.com/SimonbJohnson/hxlbites.js/blob/master/hxlBites.js#L957
|
||||||
|
|
||||||
|
```
|
||||||
|
hb.reverse(bite) => compiled bite (see above) (data, chartConfig)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. generate dashboard html and compute element ids in actual page element ids computed from grid setup
|
||||||
|
4. Now have a final dashboard config
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
widgets: [
|
||||||
|
{
|
||||||
|
data: [ [...], [...]]
|
||||||
|
widgetType: text, chart, map ...
|
||||||
|
elementId: // element to bind to ...
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
5. Now use specific renderer libraries e.g. leaflet, plotly/chartist etc to render out into page
|
||||||
|
|
||||||
|
https://github.com/SimonbJohnson/hxldash/blob/master/js/site.js#L294
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
"Source" version of dashboard with data uncompiled.
|
||||||
|
|
||||||
|
Compiled version of dashboard with final data inline ...
|
||||||
|
|
||||||
|
hxl.js takes an array of arrays ... and outputs array of arrays ...
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
schema: [...]
|
||||||
|
data: [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Renderer
|
||||||
|
|
||||||
|
* Renderer for the dashboard
|
||||||
|
* Renderer for each widget
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
function createChart(bite, elementId) => svg in bite
|
||||||
|
```
|
||||||
|
|
||||||
|
## Charts
|
||||||
|
|
||||||
|
* Data Package View => svg/png etc
|
||||||
|
* plotly
|
||||||
|
* vega (d3)
|
||||||
|
* https://github.com/frictionlessdata/datapackage-render-js
|
||||||
|
* chartist
|
||||||
|
* react-charts
|
||||||
|
|
||||||
|
## Map
|
||||||
|
|
||||||
|
* Leaflet
|
||||||
|
* react-leaflet
|
||||||
|
|
||||||
|
## Tables
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||