Compare commits
58 Commits
@portaljs/
...
@portaljs/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
539fffeb55 | ||
|
|
0d276535bd | ||
|
|
38dd7103a3 | ||
|
|
48cd812a48 | ||
|
|
7bba10714d | ||
|
|
de2c1e5b48 | ||
|
|
57952e0817 | ||
|
|
df9664624f | ||
|
|
2ea185b710 | ||
|
|
b859d48f17 | ||
|
|
3d73ac422e | ||
|
|
059ffe4e34 | ||
|
|
0aed7dce77 | ||
|
|
c202d6cfc4 | ||
|
|
d9c20528c5 | ||
|
|
b7ee5a1869 | ||
|
|
4b5d549190 | ||
|
|
e6f0ab4ec8 | ||
|
|
22038fbd4f | ||
|
|
8b292a9bf2 | ||
|
|
cda3d335f1 | ||
|
|
fe97cc87f4 | ||
|
|
88f6199d18 | ||
|
|
852cf60abc | ||
|
|
704be0d5a7 | ||
|
|
fb3598fa49 | ||
|
|
d898b5a833 | ||
|
|
3aac4dabf9 | ||
|
|
a044f56e3c | ||
|
|
1b58c311eb | ||
|
|
ed9ac2c263 | ||
|
|
42c72e5afd | ||
|
|
9e1a324fa1 | ||
|
|
90178af8f2 | ||
|
|
00e61e104c | ||
|
|
f7f03fddca | ||
|
|
0891dfde2d | ||
|
|
c904e3731b | ||
|
|
86a2945ee6 | ||
|
|
09daa98b28 | ||
|
|
b511c9f71b | ||
|
|
464cda6db8 | ||
|
|
2bbf313489 | ||
|
|
c26b76368d | ||
|
|
af11f0cfd5 | ||
|
|
9ae2b31113 | ||
|
|
2bffd130c8 | ||
|
|
058d23678a | ||
|
|
540a08934c | ||
|
|
7d010cfee4 | ||
|
|
dd79da1c6b | ||
|
|
a58e2b81f7 | ||
|
|
6d7acd27ed | ||
|
|
7c30842c7d | ||
|
|
35ca1d6dfd | ||
|
|
a7e90b64af | ||
|
|
26dcffc279 | ||
|
|
9e73410b17 |
8
.vscode/extensions.json
vendored
8
.vscode/extensions.json
vendored
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"nrwl.angular-console",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"firsttris.vscode-jest-runner",
|
|
||||||
"dbaeumer.vscode-eslint"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
77
README.md
77
README.md
@@ -1,31 +1,56 @@
|
|||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
🌀 Portal.JS
|
<a href="https://datahub.io/">
|
||||||
<br />
|
<img alt="datahub" src="http://datahub.io/datahub-cube.svg" width="146">
|
||||||
Rapidly build rich data portals using a modern frontend framework
|
</a>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
* [What is Portal.JS ?](#What-is-Portal.JS)
|
<p align="center">
|
||||||
* [Features](#Features)
|
Bugs, issues and suggestions re DataHub Cloud ☁️ and DataHub OpenSource 🌀
|
||||||
* [For developers](#For-developers)
|
<br />
|
||||||
* [Docs](#Docs)
|
<br /><a href="https://discord.gg/xfFDMPU9dC"><img src="https://dcbadge.vercel.app/api/server/xfFDMPU9dC" /></a>
|
||||||
* [Community](#Community)
|
</p>
|
||||||
* [Appendix](#Appendix)
|
|
||||||
* [What happened to Recline?](#What-happened-to-Recline?)
|
|
||||||
|
|
||||||
# What is Portal.JS
|
## DataHub
|
||||||
|
|
||||||
🌀 Portal.JS is a framework for rapidly building rich data portal frontends using a modern frontend approach. Portal.JS can be used to present a single dataset or build a full-scale data catalog/portal.
|
This repo and issue tracker are for
|
||||||
|
|
||||||
Built in JavaScript and React on top of the popular [Next.js](https://nextjs.com/) framework. Portal.JS assumes a "decoupled" approach where the frontend is a separate service from the backend and interacts with backend(s) via an API. It can be used with any backend and has out of the box support for [CKAN](https://ckan.org/).
|
- DataHub Cloud ☁️ - https://datahub.io/
|
||||||
|
- DataHub 🌀 - https://datahub.io/opensource
|
||||||
|
|
||||||
## Features
|
### Issues
|
||||||
|
|
||||||
|
Found a bug: 👉 https://github.com/datopian/datahub/issues/new
|
||||||
|
|
||||||
|
### Discussions
|
||||||
|
|
||||||
|
Got a suggestion, a question, want some support or just want to shoot the breeze 🙂
|
||||||
|
|
||||||
|
Head to the discussion forum: 👉 https://github.com/datopian/datahub/discussions
|
||||||
|
|
||||||
|
### Chat on Discord
|
||||||
|
|
||||||
|
If you would prefer to get help via live chat check out our discord 👉
|
||||||
|
|
||||||
|
[Discord](https://discord.gg/xfFDMPU9dC)
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
|
||||||
|
https://datahub.io/docs
|
||||||
|
|
||||||
|
## DataHub OpenSource 🌀
|
||||||
|
|
||||||
|
DataHub 🌀 is a platform for rapidly creating rich data portal and publishing systems using a modern frontend approach. Datahub can be used to publish a single dataset or build a full-scale data catalog/portal.
|
||||||
|
|
||||||
|
DataHub is built in JavaScript and React on top of the popular [Next.js](https://nextjs.com/) framework. DataHub assumes a "decoupled" approach where the frontend is a separate service from the backend and interacts with backend(s) via an API. It can be used with any backend and has out of the box support for [CKAN](https://ckan.org/), GitHub, Frictionless Data Packages and more.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
- 🗺️ Unified sites: present data and content in one seamless site, pulling datasets from a DMS (e.g. CKAN) and content from a CMS (e.g. Wordpress) with a common internal API.
|
- 🗺️ Unified sites: present data and content in one seamless site, pulling datasets from a DMS (e.g. CKAN) and content from a CMS (e.g. Wordpress) with a common internal API.
|
||||||
- 👩💻 Developer friendly: built with familiar frontend tech (JavaScript, React, Next.js).
|
- 👩💻 Developer friendly: built with familiar frontend tech (JavaScript, React, Next.js).
|
||||||
- 🔋 Batteries included: full set of portal components out of the box e.g. catalog search, dataset showcase, blog, etc.
|
- 🔋 Batteries included: full set of portal components out of the box e.g. catalog search, dataset showcase, blog, etc.
|
||||||
- 🎨 Easy to theme and customize: installable themes, use standard CSS and React+CSS tooling. Add new routes quickly.
|
- 🎨 Easy to theme and customize: installable themes, use standard CSS and React+CSS tooling. Add new routes quickly.
|
||||||
- 🧱 Extensible: quickly extend and develop/import your own React components
|
- 🧱 Extensible: quickly extend and develop/import your own React components
|
||||||
- 📝 Well documented: full set of documentation plus the documentation of Next.js and Apollo.
|
- 📝 Well documented: full set of documentation plus the documentation of Next.js.
|
||||||
|
|
||||||
### For developers
|
### For developers
|
||||||
|
|
||||||
@@ -33,25 +58,3 @@ Built in JavaScript and React on top of the popular [Next.js](https://nextjs.com
|
|||||||
- 🚀 Next.js framework: so everything in Next.js for free: Server Side Rendering, Static Site Generation, huge number of examples and integrations, etc.
|
- 🚀 Next.js framework: so everything in Next.js for free: Server Side Rendering, Static Site Generation, huge number of examples and integrations, etc.
|
||||||
- Server Side Rendering (SSR) => Unlimited number of pages, SEO and more whilst still using React.
|
- Server Side Rendering (SSR) => Unlimited number of pages, SEO and more whilst still using React.
|
||||||
- Static Site Generation (SSG) => Ultra-simple deployment, great performance, great lighthouse scores and more (good for small sites)
|
- Static Site Generation (SSG) => Ultra-simple deployment, great performance, great lighthouse scores and more (good for small sites)
|
||||||
|
|
||||||
#### **Check out the [Portal.JS website](https://portaljs.org/) for a gallery of live portals**
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
# Docs
|
|
||||||
|
|
||||||
Access the Portal.JS documentation at:
|
|
||||||
|
|
||||||
https://portaljs.org/docs
|
|
||||||
|
|
||||||
- [Examples](https://portaljs.org/docs#examples)
|
|
||||||
|
|
||||||
# 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/EeyfGrGu4U).
|
|
||||||
|
|
||||||
# Appendix
|
|
||||||
|
|
||||||
## What happened to Recline?
|
|
||||||
|
|
||||||
Portal.JS used to be Recline(JS). If you are looking for the old Recline codebase it still exists: see the [`recline` branch](https://github.com/datopian/portal.js/tree/recline). If you want context for the rename see [this issue](https://github.com/datopian/portal.js/issues/520).
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
# PortalJS Demo replicating the FiveThirtyEight data portal
|
||||||
|
|
||||||
|
## 👉 https://fivethirtyeight.portaljs.org 👈
|
||||||
|
|
||||||
|
Here's a blog post we wrote about it: https://www.datopian.com/blog/fivethirtyeight-replica
|
||||||
|
|
||||||
This is a replica of the awesome data.fivethirtyeight.com using PortalJS.
|
This is a replica of the awesome data.fivethirtyeight.com using PortalJS.
|
||||||
|
|
||||||
You might be asking why we did that, there are three main reasons:
|
You might be asking why we did that, there are three main reasons:
|
||||||
|
|||||||
3520
package-lock.json
generated
3520
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,47 @@
|
|||||||
# @portaljs/components
|
# @portaljs/components
|
||||||
|
|
||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
### Major Changes
|
||||||
|
|
||||||
|
- [#1103](https://github.com/datopian/datahub/pull/1103) [`48cd812a`](https://github.com/datopian/datahub/commit/48cd812a488a069a419d8ecc67f24f94d4d1d1d6) Thanks [@demenech](https://github.com/demenech)! - Components API tidying up and storybook docs improvements.
|
||||||
|
|
||||||
|
## 0.6.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [`a044f56e`](https://github.com/datopian/portaljs/commit/a044f56e3cbe0519ddf9d24d78b0bb7eac917e1c) Thanks [@luccasmmg](https://github.com/luccasmmg)! - Added plotly components
|
||||||
|
|
||||||
|
## 0.5.10
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#1083](https://github.com/datopian/portaljs/pull/1083) [`86a2945e`](https://github.com/datopian/portaljs/commit/86a2945ee68dfcea0299984ca9cc9070d68fe1c2) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Created integration with datastore api for table component
|
||||||
|
|
||||||
|
## 0.5.9
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#1081](https://github.com/datopian/portaljs/pull/1081) [`2bbf3134`](https://github.com/datopian/portaljs/commit/2bbf3134896df3ecc66560bdf95bece143614c7b) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Fixed error to remove anchor from document
|
||||||
|
|
||||||
|
## 0.5.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#1079](https://github.com/datopian/portaljs/pull/1079) [`058d2367`](https://github.com/datopian/portaljs/commit/058d23678a024890f8a6d909ded9fc8fc11cf145) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Changed the download behaviour of the bucket viewer component and removed loading component while downloading
|
||||||
|
|
||||||
|
## 0.5.7
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#1077](https://github.com/datopian/portaljs/pull/1077) [`6d7acd27`](https://github.com/datopian/portaljs/commit/6d7acd27ed9299cbcc14eab906f2f0eb414656b8) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Created property to present a component while is loading the download of the file and fixed download bug on pagination
|
||||||
|
|
||||||
|
## 0.5.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#1075](https://github.com/datopian/portaljs/pull/1075) [`26dcffc2`](https://github.com/datopian/portaljs/commit/26dcffc279057f80a579134e862085ba042c06c3) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Fixed problem presenting the download component in the first load of the bucket viewer
|
||||||
|
|
||||||
## 0.5.5
|
## 0.5.5
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@portaljs/components",
|
"name": "@portaljs/components",
|
||||||
"version": "0.5.5",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "https://portaljs.org",
|
"description": "https://portaljs.org",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -40,11 +40,13 @@
|
|||||||
"ol": "^7.4.0",
|
"ol": "^7.4.0",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"pdfjs-dist": "2.15.349",
|
"pdfjs-dist": "2.15.349",
|
||||||
|
"plotly.js": "^2.30.1",
|
||||||
"postcss-url": "^10.1.3",
|
"postcss-url": "^10.1.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-leaflet": "^4.2.1",
|
"react-leaflet": "^4.2.1",
|
||||||
|
"react-plotly.js": "^2.6.0",
|
||||||
"react-query": "^3.39.3",
|
"react-query": "^3.39.3",
|
||||||
"react-vega": "^7.6.0",
|
"react-vega": "^7.6.0",
|
||||||
"vega": "5.25.0",
|
"vega": "5.25.0",
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { CSSProperties, ReactNode, useEffect, useState } from 'react';
|
import { CSSProperties, ReactNode, useEffect, useState } from 'react';
|
||||||
import LoadingSpinner from './LoadingSpinner';
|
import LoadingSpinner from './LoadingSpinner';
|
||||||
|
|
||||||
export interface BucketViewerFilterSearchedDataEvent {
|
export interface BucketViewerFilterSearchedDataEvent {
|
||||||
startDate?: Date;
|
startDate?: Date;
|
||||||
endDate?: Date;
|
endDate?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BucketViewerProps {
|
export interface BucketViewerProps {
|
||||||
onLoadTotalNumberOfItems?: (total: number) => void;
|
onLoadTotalNumberOfItems?: (total: number) => void;
|
||||||
domain: string;
|
domain: string;
|
||||||
|
downloadConfig?: {
|
||||||
|
hoverOfTheFileComponent?: ReactNode;
|
||||||
|
};
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
downloadComponent?: ReactNode;
|
|
||||||
paginationConfig?: BucketViewerPaginationConfig;
|
paginationConfig?: BucketViewerPaginationConfig;
|
||||||
filterState?: BucketViewerFilterSearchedDataEvent;
|
filterState?: BucketViewerFilterSearchedDataEvent;
|
||||||
dataMapperFn: (rawData: Response) => Promise<BucketViewerData[]>;
|
dataMapperFn: (rawData: Response) => Promise<BucketViewerData[]>;
|
||||||
@@ -34,20 +36,20 @@ export interface BucketViewerData {
|
|||||||
|
|
||||||
export function BucketViewer({
|
export function BucketViewer({
|
||||||
domain,
|
domain,
|
||||||
downloadComponent,
|
|
||||||
suffix,
|
suffix,
|
||||||
dataMapperFn,
|
dataMapperFn,
|
||||||
className,
|
className,
|
||||||
filterState,
|
filterState,
|
||||||
paginationConfig,
|
paginationConfig,
|
||||||
onLoadTotalNumberOfItems
|
downloadConfig,
|
||||||
|
onLoadTotalNumberOfItems,
|
||||||
}: BucketViewerProps) {
|
}: BucketViewerProps) {
|
||||||
|
|
||||||
suffix = suffix ?? '/';
|
suffix = suffix ?? '/';
|
||||||
downloadComponent = downloadComponent ?? <></>;
|
|
||||||
|
|
||||||
|
const { hoverOfTheFileComponent } = downloadConfig ?? {};
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [showDownloadComponentOnLine, setShowDownloadComponentOnLine] = useState(0);
|
const [showDownloadComponentOnLine, setShowDownloadComponentOnLine] =
|
||||||
|
useState(-1);
|
||||||
const [currentPage, setCurrentPage] = useState<number>(0);
|
const [currentPage, setCurrentPage] = useState<number>(0);
|
||||||
const [lastPage, setLastPage] = useState<number>(0);
|
const [lastPage, setLastPage] = useState<number>(0);
|
||||||
const [bucketFiles, setBucketFiles] = useState<BucketViewerData[]>([]);
|
const [bucketFiles, setBucketFiles] = useState<BucketViewerData[]>([]);
|
||||||
@@ -65,56 +67,59 @@ export function BucketViewer({
|
|||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
}, [domain, suffix]);
|
}, [domain, suffix]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
() => {
|
if (paginationConfig) {
|
||||||
if(paginationConfig) {
|
const startIndex = paginationConfig
|
||||||
const startIndex = paginationConfig
|
? currentPage * paginationConfig.itemsPerPage
|
||||||
? currentPage * paginationConfig.itemsPerPage
|
: 0;
|
||||||
: 0;
|
|
||||||
|
|
||||||
const endIndex = paginationConfig
|
const endIndex = paginationConfig
|
||||||
? startIndex + paginationConfig.itemsPerPage
|
? startIndex + paginationConfig.itemsPerPage
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
setLastPage(Math.ceil(filteredData.length / paginationConfig.itemsPerPage) - 1);
|
setLastPage(
|
||||||
setPaginatedData(filteredData.slice(startIndex, endIndex));
|
Math.ceil(filteredData.length / paginationConfig.itemsPerPage) - 1
|
||||||
}
|
);
|
||||||
},
|
setPaginatedData(filteredData.slice(startIndex, endIndex));
|
||||||
[currentPage, filteredData]
|
}
|
||||||
);
|
}, [currentPage, filteredData]);
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() => {
|
|
||||||
if(onLoadTotalNumberOfItems) onLoadTotalNumberOfItems(filteredData.length);
|
|
||||||
},
|
|
||||||
[filteredData]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(!filterState) return;
|
if (onLoadTotalNumberOfItems) onLoadTotalNumberOfItems(filteredData.length);
|
||||||
|
}, [filteredData]);
|
||||||
|
|
||||||
if (filterState.startDate && filterState.endDate) {
|
useEffect(() => {
|
||||||
setFilteredData(bucketFiles.filter(({ dateProps }) =>
|
if (!filterState) return;
|
||||||
dateProps
|
|
||||||
?
|
if (filterState.startDate && filterState.endDate) {
|
||||||
dateProps.date.getTime() >= filterState.startDate.getTime()
|
setFilteredData(
|
||||||
&& dateProps.date.getTime() <= filterState.endDate.getTime()
|
bucketFiles.filter(({ dateProps }) =>
|
||||||
|
dateProps
|
||||||
|
? dateProps.date.getTime() >= filterState.startDate.getTime() &&
|
||||||
|
dateProps.date.getTime() <= filterState.endDate.getTime()
|
||||||
: true
|
: true
|
||||||
));
|
)
|
||||||
} else if(filterState.startDate) {
|
);
|
||||||
setFilteredData(bucketFiles.filter(({ dateProps }) =>
|
} else if (filterState.startDate) {
|
||||||
dateProps ? dateProps.date.getTime() >= filterState.startDate.getTime() : true
|
setFilteredData(
|
||||||
));
|
bucketFiles.filter(({ dateProps }) =>
|
||||||
} else if(filterState.endDate) {
|
dateProps
|
||||||
setFilteredData(bucketFiles.filter(({ dateProps }) =>
|
? dateProps.date.getTime() >= filterState.startDate.getTime()
|
||||||
dateProps ? dateProps.date.getTime() <= filterState.endDate.getTime() : true
|
: true
|
||||||
));
|
)
|
||||||
} else {
|
);
|
||||||
setFilteredData(bucketFiles);
|
} else if (filterState.endDate) {
|
||||||
}
|
setFilteredData(
|
||||||
},
|
bucketFiles.filter(({ dateProps }) =>
|
||||||
[filterState]
|
dateProps
|
||||||
)
|
? dateProps.date.getTime() <= filterState.endDate.getTime()
|
||||||
|
: true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setFilteredData(bucketFiles);
|
||||||
|
}
|
||||||
|
}, [filterState]);
|
||||||
|
|
||||||
return isLoading ? (
|
return isLoading ? (
|
||||||
<div className="w-full flex items-center justify-center h-[300px]">
|
<div className="w-full flex items-center justify-center h-[300px]">
|
||||||
@@ -122,67 +127,61 @@ export function BucketViewer({
|
|||||||
</div>
|
</div>
|
||||||
) : bucketFiles ? (
|
) : bucketFiles ? (
|
||||||
<>
|
<>
|
||||||
{...(paginationConfig && bucketFiles
|
{...(paginationConfig && bucketFiles ? paginatedData : filteredData)?.map(
|
||||||
? paginatedData
|
(data, i) => (
|
||||||
: filteredData
|
<ul
|
||||||
)?.map((data, i) => (
|
onClick={() => {
|
||||||
<ul
|
const a: HTMLAnchorElement = document.createElement('a');
|
||||||
onClick={() => {
|
a.href = data.downloadFileUri;
|
||||||
const anchorId = `download_anchor_${i}`;
|
a.target = `_blank`;
|
||||||
const a: HTMLAnchorElement =
|
a.download = data.fileName;
|
||||||
(document.getElementById(anchorId) as HTMLAnchorElement | null) ??
|
document.body.appendChild(a);
|
||||||
document.createElement('a');
|
a.click();
|
||||||
a.id = anchorId;
|
document.body.removeChild(a);
|
||||||
if (a.download) a.click();
|
}}
|
||||||
else {
|
key={i}
|
||||||
setIsLoading(true);
|
onMouseEnter={() => setShowDownloadComponentOnLine(i)}
|
||||||
fetch(data.downloadFileUri)
|
onMouseLeave={() => setShowDownloadComponentOnLine(undefined)}
|
||||||
.then((res) => res.blob())
|
className={`${
|
||||||
.then((res) => {
|
className ??
|
||||||
a.href = URL.createObjectURL(res);
|
'mb-2 border-b-[2px] border-b-[red] hover:cursor-pointer'
|
||||||
a.download = res.name ?? data.fileName;
|
}`}
|
||||||
document.body.appendChild(a);
|
>
|
||||||
a.click();
|
{hoverOfTheFileComponent && showDownloadComponentOnLine === i ? (
|
||||||
})
|
hoverOfTheFileComponent
|
||||||
.finally(() => setIsLoading(false));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
key={i}
|
|
||||||
onMouseEnter={() => setShowDownloadComponentOnLine(i)}
|
|
||||||
onMouseLeave={() => setShowDownloadComponentOnLine(undefined)}
|
|
||||||
className={`${
|
|
||||||
className ??
|
|
||||||
'mb-2 border-b-[2px] border-b-[red] hover:cursor-pointer'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
downloadComponent && showDownloadComponentOnLine === i
|
|
||||||
? downloadComponent
|
|
||||||
: <></>
|
|
||||||
}
|
|
||||||
<div>
|
|
||||||
<li>{data.fileName}</li>
|
|
||||||
{data.dateProps && data.dateProps.dateFormatter ? (
|
|
||||||
<li>{data.dateProps.dateFormatter(data.dateProps.date)}</li>
|
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
</div>
|
<div className="flex justify-between w-full items-center">
|
||||||
</ul>
|
<div>
|
||||||
))}
|
<li>{data.fileName}</li>
|
||||||
|
{data.dateProps && data.dateProps.dateFormatter ? (
|
||||||
|
<li>{data.dateProps.dateFormatter(data.dateProps.date)}</li>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
)}
|
||||||
{paginationConfig ? (
|
{paginationConfig ? (
|
||||||
<ul className={
|
<ul
|
||||||
paginationConfig.containerClassName
|
className={
|
||||||
? paginationConfig.containerClassName
|
paginationConfig.containerClassName
|
||||||
: "flex justify-end gap-x-[0.5rem] w-full"
|
? paginationConfig.containerClassName
|
||||||
}
|
: 'flex justify-end gap-x-[0.5rem] w-full'
|
||||||
style={paginationConfig.containerStyles ?? {}}
|
}
|
||||||
>
|
style={paginationConfig.containerStyles ?? {}}
|
||||||
|
>
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
className="hover:cursor-pointer hover:disabled:cursor-not-allowed"
|
className="hover:cursor-pointer hover:disabled:cursor-not-allowed"
|
||||||
disabled={currentPage === 0}
|
disabled={currentPage === 0}
|
||||||
onClick={() => setCurrentPage(0)}>First</button>
|
onClick={() => setCurrentPage(0)}
|
||||||
|
>
|
||||||
|
First
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
@@ -193,9 +192,7 @@ export function BucketViewer({
|
|||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<label>
|
<label>{currentPage + 1}</label>
|
||||||
{currentPage + 1}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ export function Catalog({
|
|||||||
datasets,
|
datasets,
|
||||||
facets,
|
facets,
|
||||||
}: {
|
}: {
|
||||||
datasets: any[];
|
datasets: {
|
||||||
|
_id: string | number;
|
||||||
|
metadata: { title: string; [k: string]: string | number };
|
||||||
|
url_path: string;
|
||||||
|
[k: string]: any;
|
||||||
|
}[];
|
||||||
facets: string[];
|
facets: string[];
|
||||||
}) {
|
}) {
|
||||||
const [indexFilter, setIndexFilter] = useState('');
|
const [indexFilter, setIndexFilter] = useState('');
|
||||||
@@ -56,7 +61,7 @@ export function Catalog({
|
|||||||
//Then check if the selectedValue for the given facet is included in the dataset metadata
|
//Then check if the selectedValue for the given facet is included in the dataset metadata
|
||||||
.filter((dataset) => {
|
.filter((dataset) => {
|
||||||
//Avoids a server rendering breakage
|
//Avoids a server rendering breakage
|
||||||
if (!watch() || Object.keys(watch()).length === 0) return true
|
if (!watch() || Object.keys(watch()).length === 0) return true;
|
||||||
//This will filter only the key pairs of the metadata values that were selected as facets
|
//This will filter only the key pairs of the metadata values that were selected as facets
|
||||||
const datasetFacets = Object.entries(dataset.metadata).filter((entry) =>
|
const datasetFacets = Object.entries(dataset.metadata).filter((entry) =>
|
||||||
facets.includes(entry[0])
|
facets.includes(entry[0])
|
||||||
@@ -86,9 +91,7 @@ export function Catalog({
|
|||||||
className="p-2 ml-1 text-sm shadow border border-block"
|
className="p-2 ml-1 text-sm shadow border border-block"
|
||||||
{...register(elem[0] + '.selectedValue')}
|
{...register(elem[0] + '.selectedValue')}
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">Filter by {elem[0]}</option>
|
||||||
Filter by {elem[0]}
|
|
||||||
</option>
|
|
||||||
{(elem[1] as { possibleValues: string[] }).possibleValues.map(
|
{(elem[1] as { possibleValues: string[] }).possibleValues.map(
|
||||||
(val) => (
|
(val) => (
|
||||||
<option
|
<option
|
||||||
@@ -102,10 +105,10 @@ export function Catalog({
|
|||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
))}
|
))}
|
||||||
<ul className='mb-5 pl-6 mt-5 list-disc'>
|
<ul className="mb-5 pl-6 mt-5 list-disc">
|
||||||
{filteredDatasets.map((dataset) => (
|
{filteredDatasets.map((dataset) => (
|
||||||
<li className='py-2' key={dataset._id}>
|
<li className="py-2" key={dataset._id}>
|
||||||
<a className='font-medium underline' href={dataset.url_path}>
|
<a className="font-medium underline" href={dataset.url_path}>
|
||||||
{dataset.metadata.title
|
{dataset.metadata.title
|
||||||
? dataset.metadata.title
|
? dataset.metadata.title
|
||||||
: dataset.url_path}
|
: dataset.url_path}
|
||||||
@@ -116,4 +119,3 @@ export function Catalog({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import { read, utils } from 'xlsx';
|
|||||||
import { AgGridReact } from 'ag-grid-react';
|
import { AgGridReact } from 'ag-grid-react';
|
||||||
import 'ag-grid-community/styles/ag-grid.css';
|
import 'ag-grid-community/styles/ag-grid.css';
|
||||||
import 'ag-grid-community/styles/ag-theme-alpine.css';
|
import 'ag-grid-community/styles/ag-theme-alpine.css';
|
||||||
|
import { Data } from '../types/properties';
|
||||||
|
|
||||||
export type ExcelProps = {
|
export type ExcelProps = {
|
||||||
url: string;
|
data: Required<Pick<Data, 'url'>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Excel({ url }: ExcelProps) {
|
export function Excel({ data }: ExcelProps) {
|
||||||
|
const url = data.url;
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [activeSheetName, setActiveSheetName] = useState<string>();
|
const [activeSheetName, setActiveSheetName] = useState<string>();
|
||||||
const [workbook, setWorkbook] = useState<any>();
|
const [workbook, setWorkbook] = useState<any>();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
|
|||||||
import Papa from 'papaparse';
|
import Papa from 'papaparse';
|
||||||
import { Grid } from '@githubocto/flat-ui';
|
import { Grid } from '@githubocto/flat-ui';
|
||||||
import LoadingSpinner from './LoadingSpinner';
|
import LoadingSpinner from './LoadingSpinner';
|
||||||
|
import { Data } from '../types/properties';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
@@ -36,30 +37,25 @@ export async function parseCsv(file: string, parsingConfig): Promise<any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FlatUiTableProps {
|
export interface FlatUiTableProps {
|
||||||
url?: string;
|
data: Data;
|
||||||
data?: { [key: string]: number | string }[];
|
uniqueId?: number;
|
||||||
rawCsv?: string;
|
|
||||||
randomId?: number;
|
|
||||||
bytes: number;
|
bytes: number;
|
||||||
parsingConfig: any;
|
parsingConfig: any;
|
||||||
}
|
}
|
||||||
export const FlatUiTable: React.FC<FlatUiTableProps> = ({
|
export const FlatUiTable: React.FC<FlatUiTableProps> = ({
|
||||||
url,
|
|
||||||
data,
|
data,
|
||||||
rawCsv,
|
uniqueId,
|
||||||
bytes = 5132288,
|
bytes = 5132288,
|
||||||
parsingConfig = {},
|
parsingConfig = {},
|
||||||
}) => {
|
}) => {
|
||||||
const randomId = Math.random();
|
uniqueId = uniqueId ?? Math.random();
|
||||||
return (
|
return (
|
||||||
// Provide the client to your App
|
// Provide the client to your App
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<TableInner
|
<TableInner
|
||||||
bytes={bytes}
|
bytes={bytes}
|
||||||
url={url}
|
|
||||||
data={data}
|
data={data}
|
||||||
rawCsv={rawCsv}
|
uniqueId={uniqueId}
|
||||||
randomId={randomId}
|
|
||||||
parsingConfig={parsingConfig}
|
parsingConfig={parsingConfig}
|
||||||
/>
|
/>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
@@ -67,33 +63,32 @@ export const FlatUiTable: React.FC<FlatUiTableProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TableInner: React.FC<FlatUiTableProps> = ({
|
const TableInner: React.FC<FlatUiTableProps> = ({
|
||||||
url,
|
|
||||||
data,
|
data,
|
||||||
rawCsv,
|
uniqueId,
|
||||||
randomId,
|
|
||||||
bytes,
|
bytes,
|
||||||
parsingConfig,
|
parsingConfig,
|
||||||
}) => {
|
}) => {
|
||||||
if (data) {
|
const url = data.url;
|
||||||
|
const csv = data.csv;
|
||||||
|
const values = data.values;
|
||||||
|
|
||||||
|
if (values) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full" style={{ height: '500px' }}>
|
<div className="w-full" style={{ height: '500px' }}>
|
||||||
<Grid data={data} />
|
<Grid data={values} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { data: csvString, isLoading: isDownloadingCSV } = useQuery(
|
const { data: csvString, isLoading: isDownloadingCSV } = useQuery(
|
||||||
['dataCsv', url, randomId],
|
['dataCsv', url, uniqueId],
|
||||||
() => getCsv(url as string, bytes),
|
() => getCsv(url as string, bytes),
|
||||||
{ enabled: !!url }
|
{ enabled: !!url }
|
||||||
);
|
);
|
||||||
const { data: parsedData, isLoading: isParsing } = useQuery(
|
const { data: parsedData, isLoading: isParsing } = useQuery(
|
||||||
['dataPreview', csvString, randomId],
|
['dataPreview', csvString, uniqueId],
|
||||||
() =>
|
() =>
|
||||||
parseCsv(
|
parseCsv(csv ? (csv as string) : (csvString as string), parsingConfig),
|
||||||
rawCsv ? (rawCsv as string) : (csvString as string),
|
{ enabled: csv ? true : !!csvString }
|
||||||
parsingConfig
|
|
||||||
),
|
|
||||||
{ enabled: rawCsv ? true : !!csvString }
|
|
||||||
);
|
);
|
||||||
if (isParsing || isDownloadingCSV)
|
if (isParsing || isDownloadingCSV)
|
||||||
<div className="w-full flex justify-center items-center h-[500px]">
|
<div className="w-full flex justify-center items-center h-[500px]">
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { CSSProperties } from "react";
|
import { CSSProperties } from 'react';
|
||||||
|
import { Data } from '../types/properties';
|
||||||
|
|
||||||
export interface IframeProps {
|
export interface IframeProps {
|
||||||
url: string;
|
data: Required<Pick<Data, 'url'>>;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Iframe({
|
export function Iframe({ data, style }: IframeProps) {
|
||||||
url, style
|
const url = data.url;
|
||||||
}: IframeProps) {
|
|
||||||
return (
|
return (
|
||||||
<iframe src={url} style={style ?? { width: `100%`, height: `100%` }}></iframe>
|
<iframe
|
||||||
|
src={url}
|
||||||
|
style={style ?? { width: `100%`, height: `100%` }}
|
||||||
|
></iframe>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,31 +2,33 @@ import { useEffect, useState } from 'react';
|
|||||||
import LoadingSpinner from './LoadingSpinner';
|
import LoadingSpinner from './LoadingSpinner';
|
||||||
import { VegaLite } from './VegaLite';
|
import { VegaLite } from './VegaLite';
|
||||||
import loadData from '../lib/loadData';
|
import loadData from '../lib/loadData';
|
||||||
|
import { Data } from '../types/properties';
|
||||||
|
|
||||||
type AxisType = 'quantitative' | 'temporal';
|
type AxisType = 'quantitative' | 'temporal';
|
||||||
type TimeUnit = 'year' | undefined; // or ...
|
type TimeUnit = 'year' | undefined; // or ...
|
||||||
|
|
||||||
export type LineChartProps = {
|
export type LineChartProps = {
|
||||||
data: Array<Array<string | number>> | string | { x: string; y: number }[];
|
data: Omit<Data, 'csv'>;
|
||||||
title?: string;
|
title?: string;
|
||||||
xAxis?: string;
|
xAxis: string;
|
||||||
xAxisType?: AxisType;
|
xAxisType?: AxisType;
|
||||||
xAxisTimeUnit: TimeUnit;
|
xAxisTimeUnit?: TimeUnit;
|
||||||
yAxis?: string;
|
yAxis: string;
|
||||||
yAxisType?: AxisType;
|
yAxisType?: AxisType;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LineChart({
|
export function LineChart({
|
||||||
data = [],
|
data,
|
||||||
fullWidth = false,
|
|
||||||
title = '',
|
title = '',
|
||||||
xAxis = 'x',
|
xAxis,
|
||||||
xAxisType = 'temporal',
|
xAxisType = 'temporal',
|
||||||
xAxisTimeUnit = 'year', // TODO: defaults to undefined would probably work better... keeping it as it's for compatibility purposes
|
xAxisTimeUnit = 'year', // TODO: defaults to undefined would probably work better... keeping it as it's for compatibility purposes
|
||||||
yAxis = 'y',
|
yAxis,
|
||||||
yAxisType = 'quantitative',
|
yAxisType = 'quantitative',
|
||||||
}: LineChartProps) {
|
}: LineChartProps) {
|
||||||
|
const url = data.url;
|
||||||
|
const values = data.values;
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
// By default, assumes data is an Array...
|
// By default, assumes data is an Array...
|
||||||
@@ -64,13 +66,12 @@ export function LineChart({
|
|||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If data is string, assume it's a URL
|
if (url) {
|
||||||
if (typeof data === 'string') {
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// Manualy loading the data allows us to do other kinds
|
// Manualy loading the data allows us to do other kinds
|
||||||
// of stuff later e.g. load a file partially
|
// of stuff later e.g. load a file partially
|
||||||
loadData(data).then((res: any) => {
|
loadData(url).then((res: any) => {
|
||||||
setSpecData({ values: res, format: { type: 'csv' } });
|
setSpecData({ values: res, format: { type: 'csv' } });
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
@@ -78,12 +79,8 @@ export function LineChart({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
var vegaData = {};
|
var vegaData = {};
|
||||||
if (Array.isArray(data)) {
|
if (values) {
|
||||||
var dataObj;
|
vegaData = { table: values };
|
||||||
dataObj = data.map((r) => {
|
|
||||||
return { x: r[0], y: r[1] };
|
|
||||||
});
|
|
||||||
vegaData = { table: dataObj };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return isLoading ? (
|
return isLoading ? (
|
||||||
@@ -91,6 +88,6 @@ export function LineChart({
|
|||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<VegaLite fullWidth={fullWidth} data={vegaData} spec={spec} />
|
<VegaLite data={vegaData} spec={spec} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { CSSProperties, useEffect, useState } from 'react';
|
|||||||
import LoadingSpinner from './LoadingSpinner';
|
import LoadingSpinner from './LoadingSpinner';
|
||||||
import loadData from '../lib/loadData';
|
import loadData from '../lib/loadData';
|
||||||
import chroma from 'chroma-js';
|
import chroma from 'chroma-js';
|
||||||
|
import { GeospatialData } from '../types/properties';
|
||||||
import {
|
import {
|
||||||
MapContainer,
|
MapContainer,
|
||||||
TileLayer,
|
TileLayer,
|
||||||
@@ -14,26 +15,25 @@ import * as L from 'leaflet';
|
|||||||
|
|
||||||
export type MapProps = {
|
export type MapProps = {
|
||||||
layers: {
|
layers: {
|
||||||
data: string | GeoJSON.GeoJSON;
|
data: GeospatialData;
|
||||||
name: string;
|
name: string;
|
||||||
colorScale?: {
|
colorScale?: {
|
||||||
starting: string;
|
starting: string;
|
||||||
ending: string;
|
ending: string;
|
||||||
};
|
};
|
||||||
tooltip?:
|
tooltip?:
|
||||||
| {
|
| {
|
||||||
propNames: string[];
|
propNames: string[];
|
||||||
}
|
}
|
||||||
| boolean;
|
| boolean;
|
||||||
_id?: number;
|
|
||||||
}[];
|
}[];
|
||||||
title?: string;
|
title?: string;
|
||||||
center?: { latitude: number | undefined; longitude: number | undefined };
|
center?: { latitude: number | undefined; longitude: number | undefined };
|
||||||
zoom?: number;
|
zoom?: number;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
autoZoomConfiguration?: {
|
autoZoomConfiguration?: {
|
||||||
layerName: string
|
layerName: string;
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Map({
|
export function Map({
|
||||||
@@ -56,17 +56,19 @@ export function Map({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadDataPromises = layers.map(async (layer) => {
|
const loadDataPromises = layers.map(async (layer) => {
|
||||||
|
const url = layer.data.url;
|
||||||
|
const geojson = layer.data.geojson;
|
||||||
let layerData: any;
|
let layerData: any;
|
||||||
|
|
||||||
if (typeof layer.data === 'string') {
|
if (url) {
|
||||||
// If "data" is string, assume it's a URL
|
// If "data" is string, assume it's a URL
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
layerData = await loadData(layer.data).then((res: any) => {
|
layerData = await loadData(url).then((res: any) => {
|
||||||
return JSON.parse(res);
|
return JSON.parse(res);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Else, expect raw GeoJSON
|
// Else, expect raw GeoJSON
|
||||||
layerData = layer.data;
|
layerData = geojson;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (layer.colorScale) {
|
if (layer.colorScale) {
|
||||||
@@ -111,23 +113,23 @@ export function Map({
|
|||||||
// Create the title box
|
// Create the title box
|
||||||
var info = new L.Control() as any;
|
var info = new L.Control() as any;
|
||||||
|
|
||||||
info.onAdd = function() {
|
info.onAdd = function () {
|
||||||
this._div = L.DomUtil.create('div', 'info');
|
this._div = L.DomUtil.create('div', 'info');
|
||||||
this.update();
|
this.update();
|
||||||
return this._div;
|
return this._div;
|
||||||
};
|
};
|
||||||
|
|
||||||
info.update = function() {
|
info.update = function () {
|
||||||
this._div.innerHTML = `<h4 style="font-weight: 600; background: #f9f9f9; padding: 5px; border-radius: 5px; color: #464646;">${title}</h4>`;
|
this._div.innerHTML = `<h4 style="font-weight: 600; background: #f9f9f9; padding: 5px; border-radius: 5px; color: #464646;">${title}</h4>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (title) info.addTo(map.target);
|
if (title) info.addTo(map.target);
|
||||||
if(!autoZoomConfiguration) return;
|
if (!autoZoomConfiguration) return;
|
||||||
|
|
||||||
let layerToZoomBounds = L.latLngBounds(L.latLng(0, 0), L.latLng(0, 0));
|
let layerToZoomBounds = L.latLngBounds(L.latLng(0, 0), L.latLng(0, 0));
|
||||||
|
|
||||||
layers.forEach((layer) => {
|
layers.forEach((layer) => {
|
||||||
if(layer.name === autoZoomConfiguration.layerName) {
|
if (layer.name === autoZoomConfiguration.layerName) {
|
||||||
const data = layersData.find(
|
const data = layersData.find(
|
||||||
(layerData) => layerData.name === layer.name
|
(layerData) => layerData.name === layer.name
|
||||||
)?.data;
|
)?.data;
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
// Core viewer
|
// Core viewer
|
||||||
import { Viewer, Worker, SpecialZoomLevel } from '@react-pdf-viewer/core';
|
import { Viewer, Worker, SpecialZoomLevel } from '@react-pdf-viewer/core';
|
||||||
import { defaultLayoutPlugin } from '@react-pdf-viewer/default-layout';
|
import { defaultLayoutPlugin } from '@react-pdf-viewer/default-layout';
|
||||||
|
import { Data } from '../types/properties';
|
||||||
|
|
||||||
// Import styles
|
// Import styles
|
||||||
import '@react-pdf-viewer/core/lib/styles/index.css';
|
import '@react-pdf-viewer/core/lib/styles/index.css';
|
||||||
import '@react-pdf-viewer/default-layout/lib/styles/index.css';
|
import '@react-pdf-viewer/default-layout/lib/styles/index.css';
|
||||||
|
|
||||||
export interface PdfViewerProps {
|
export interface PdfViewerProps {
|
||||||
url: string;
|
data: Required<Pick<Data, 'url'>>;
|
||||||
layout: boolean;
|
layout: boolean;
|
||||||
parentClassName?: string;
|
parentClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PdfViewer({
|
export function PdfViewer({
|
||||||
url,
|
data,
|
||||||
layout = false,
|
layout = false,
|
||||||
parentClassName,
|
parentClassName = 'h-screen',
|
||||||
}: PdfViewerProps) {
|
}: PdfViewerProps) {
|
||||||
|
const url = data.url;
|
||||||
const defaultLayoutPluginInstance = defaultLayoutPlugin();
|
const defaultLayoutPluginInstance = defaultLayoutPlugin();
|
||||||
return (
|
return (
|
||||||
<Worker workerUrl="https://unpkg.com/pdfjs-dist@2.15.349/build/pdf.worker.js">
|
<Worker workerUrl="https://unpkg.com/pdfjs-dist@2.15.349/build/pdf.worker.js">
|
||||||
|
|||||||
9
packages/components/src/components/Plotly.tsx
Normal file
9
packages/components/src/components/Plotly.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import Plot, { PlotParams } from "react-plotly.js";
|
||||||
|
|
||||||
|
export const Plotly: React.FC<PlotParams> = (props) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Plot {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
153
packages/components/src/components/PlotlyBarChart.tsx
Normal file
153
packages/components/src/components/PlotlyBarChart.tsx
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
|
||||||
|
import { Plotly } from './Plotly';
|
||||||
|
import Papa, { ParseConfig } from 'papaparse';
|
||||||
|
import LoadingSpinner from './LoadingSpinner';
|
||||||
|
import { Data } from '../types/properties';
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
async function getCsv(url: string, bytes: number) {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Range: `bytes=0-${bytes}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await response.text();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function parseCsv(
|
||||||
|
file: string,
|
||||||
|
parsingConfig: ParseConfig
|
||||||
|
): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Papa.parse(file, {
|
||||||
|
...parsingConfig,
|
||||||
|
header: true,
|
||||||
|
dynamicTyping: true,
|
||||||
|
skipEmptyLines: true,
|
||||||
|
transform: (value: string): string => {
|
||||||
|
return value.trim();
|
||||||
|
},
|
||||||
|
complete: (results: any) => {
|
||||||
|
return resolve(results);
|
||||||
|
},
|
||||||
|
error: (error: any) => {
|
||||||
|
return reject(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlotlyBarChartProps {
|
||||||
|
data: Data;
|
||||||
|
uniqueId?: number;
|
||||||
|
bytes?: number;
|
||||||
|
parsingConfig?: ParseConfig;
|
||||||
|
xAxis: string;
|
||||||
|
yAxis: string;
|
||||||
|
// TODO: commented out because this doesn't work. I believe
|
||||||
|
// this would only make any difference on charts with multiple
|
||||||
|
// traces.
|
||||||
|
// lineLabel?: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlotlyBarChart: React.FC<PlotlyBarChartProps> = ({
|
||||||
|
data,
|
||||||
|
bytes = 5132288,
|
||||||
|
parsingConfig = {},
|
||||||
|
xAxis,
|
||||||
|
yAxis,
|
||||||
|
// lineLabel,
|
||||||
|
title = '',
|
||||||
|
}) => {
|
||||||
|
const uniqueId = Math.random();
|
||||||
|
return (
|
||||||
|
// Provide the client to your App
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<PlotlyBarChartInner
|
||||||
|
data={data}
|
||||||
|
uniqueId={uniqueId}
|
||||||
|
bytes={bytes}
|
||||||
|
parsingConfig={parsingConfig}
|
||||||
|
xAxis={xAxis}
|
||||||
|
yAxis={yAxis}
|
||||||
|
// lineLabel={lineLabel ?? yAxis}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PlotlyBarChartInner: React.FC<PlotlyBarChartProps> = ({
|
||||||
|
data,
|
||||||
|
uniqueId,
|
||||||
|
bytes,
|
||||||
|
parsingConfig,
|
||||||
|
xAxis,
|
||||||
|
yAxis,
|
||||||
|
// lineLabel,
|
||||||
|
title,
|
||||||
|
}) => {
|
||||||
|
if (data.values) {
|
||||||
|
return (
|
||||||
|
<div className="w-full" style={{ height: '500px' }}>
|
||||||
|
<Plotly
|
||||||
|
layout={{
|
||||||
|
title,
|
||||||
|
}}
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
x: data.values.map((d) => d[xAxis]),
|
||||||
|
y: data.values.map((d) => d[yAxis]),
|
||||||
|
type: 'bar',
|
||||||
|
// name: lineLabel,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { data: csvString, isLoading: isDownloadingCSV } = useQuery(
|
||||||
|
['dataCsv', data.url, uniqueId],
|
||||||
|
() => getCsv(data.url as string, bytes ?? 5132288),
|
||||||
|
{ enabled: !!data.url }
|
||||||
|
);
|
||||||
|
const { data: parsedData, isLoading: isParsing } = useQuery(
|
||||||
|
['dataPreview', csvString, uniqueId],
|
||||||
|
() =>
|
||||||
|
parseCsv(
|
||||||
|
data.csv ? (data.csv as string) : (csvString as string),
|
||||||
|
parsingConfig ?? {}
|
||||||
|
),
|
||||||
|
{ enabled: data.csv ? true : !!csvString }
|
||||||
|
);
|
||||||
|
if (isParsing || isDownloadingCSV)
|
||||||
|
<div className="w-full flex justify-center items-center h-[500px]">
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>;
|
||||||
|
if (parsedData)
|
||||||
|
return (
|
||||||
|
<div className="w-full" style={{ height: '500px' }}>
|
||||||
|
<Plotly
|
||||||
|
layout={{
|
||||||
|
title,
|
||||||
|
}}
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
x: parsedData.data.map((d: any) => d[xAxis]),
|
||||||
|
y: parsedData.data.map((d: any) => d[yAxis]),
|
||||||
|
type: 'bar',
|
||||||
|
// name: lineLabel, TODO: commented out because this doesn't work
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className="w-full flex justify-center items-center h-[500px]">
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
155
packages/components/src/components/PlotlyLineChart.tsx
Normal file
155
packages/components/src/components/PlotlyLineChart.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
|
||||||
|
import { Plotly } from './Plotly';
|
||||||
|
import Papa, { ParseConfig } from 'papaparse';
|
||||||
|
import LoadingSpinner from './LoadingSpinner';
|
||||||
|
import { Data } from '../types/properties';
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
async function getCsv(url: string, bytes: number) {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Range: `bytes=0-${bytes}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await response.text();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function parseCsv(
|
||||||
|
file: string,
|
||||||
|
parsingConfig: ParseConfig
|
||||||
|
): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Papa.parse(file, {
|
||||||
|
...parsingConfig,
|
||||||
|
header: true,
|
||||||
|
dynamicTyping: true,
|
||||||
|
skipEmptyLines: true,
|
||||||
|
transform: (value: string): string => {
|
||||||
|
return value.trim();
|
||||||
|
},
|
||||||
|
complete: (results: any) => {
|
||||||
|
return resolve(results);
|
||||||
|
},
|
||||||
|
error: (error: any) => {
|
||||||
|
return reject(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlotlyLineChartProps {
|
||||||
|
data: Data;
|
||||||
|
bytes?: number;
|
||||||
|
parsingConfig?: ParseConfig;
|
||||||
|
xAxis: string;
|
||||||
|
yAxis: string;
|
||||||
|
lineLabel?: string;
|
||||||
|
title?: string;
|
||||||
|
uniqueId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlotlyLineChart: React.FC<PlotlyLineChartProps> = ({
|
||||||
|
data,
|
||||||
|
bytes = 5132288,
|
||||||
|
parsingConfig = {},
|
||||||
|
xAxis,
|
||||||
|
yAxis,
|
||||||
|
lineLabel,
|
||||||
|
title = '',
|
||||||
|
uniqueId,
|
||||||
|
}) => {
|
||||||
|
uniqueId = uniqueId ?? Math.random();
|
||||||
|
return (
|
||||||
|
// Provide the client to your App
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<LineChartInner
|
||||||
|
data={data}
|
||||||
|
uniqueId={uniqueId}
|
||||||
|
bytes={bytes}
|
||||||
|
parsingConfig={parsingConfig}
|
||||||
|
xAxis={xAxis}
|
||||||
|
yAxis={yAxis}
|
||||||
|
lineLabel={lineLabel ?? yAxis}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LineChartInner: React.FC<PlotlyLineChartProps> = ({
|
||||||
|
data,
|
||||||
|
uniqueId,
|
||||||
|
bytes,
|
||||||
|
parsingConfig,
|
||||||
|
xAxis,
|
||||||
|
yAxis,
|
||||||
|
lineLabel,
|
||||||
|
title,
|
||||||
|
}) => {
|
||||||
|
const values = data.values;
|
||||||
|
const url = data.url;
|
||||||
|
const csv = data.csv;
|
||||||
|
|
||||||
|
if (values) {
|
||||||
|
return (
|
||||||
|
<div className="w-full" style={{ height: '500px' }}>
|
||||||
|
<Plotly
|
||||||
|
layout={{
|
||||||
|
title,
|
||||||
|
}}
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
x: values.map((d) => d[xAxis]),
|
||||||
|
y: values.map((d) => d[yAxis]),
|
||||||
|
mode: 'lines',
|
||||||
|
name: lineLabel,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { data: csvString, isLoading: isDownloadingCSV } = useQuery(
|
||||||
|
['dataCsv', url, uniqueId],
|
||||||
|
() => getCsv(url as string, bytes ?? 5132288),
|
||||||
|
{ enabled: !!url }
|
||||||
|
);
|
||||||
|
const { data: parsedData, isLoading: isParsing } = useQuery(
|
||||||
|
['dataPreview', csvString, uniqueId],
|
||||||
|
() =>
|
||||||
|
parseCsv(
|
||||||
|
csv ? (csv as string) : (csvString as string),
|
||||||
|
parsingConfig ?? {}
|
||||||
|
),
|
||||||
|
{ enabled: csv ? true : !!csvString }
|
||||||
|
);
|
||||||
|
if (isParsing || isDownloadingCSV)
|
||||||
|
<div className="w-full flex justify-center items-center h-[500px]">
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>;
|
||||||
|
if (parsedData)
|
||||||
|
return (
|
||||||
|
<div className="w-full" style={{ height: '500px' }}>
|
||||||
|
<Plotly
|
||||||
|
layout={{
|
||||||
|
title,
|
||||||
|
}}
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
x: parsedData.data.map((d: any) => d[xAxis]),
|
||||||
|
y: parsedData.data.map((d: any) => d[yAxis]),
|
||||||
|
mode: 'lines',
|
||||||
|
name: lineLabel,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className="w-full flex justify-center items-center h-[500px]">
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
getFilteredRowModel,
|
getFilteredRowModel,
|
||||||
getPaginationRowModel,
|
getPaginationRowModel,
|
||||||
getSortedRowModel,
|
getSortedRowModel,
|
||||||
|
PaginationState,
|
||||||
|
Table as ReactTable,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
|
|
||||||
@@ -25,12 +27,19 @@ import DebouncedInput from './DebouncedInput';
|
|||||||
import loadData from '../lib/loadData';
|
import loadData from '../lib/loadData';
|
||||||
import LoadingSpinner from './LoadingSpinner';
|
import LoadingSpinner from './LoadingSpinner';
|
||||||
|
|
||||||
|
export type TableData = { cols: {key: string, name: string}[]; data: any[]; total: number };
|
||||||
|
|
||||||
export type TableProps = {
|
export type TableProps = {
|
||||||
data?: Array<{ [key: string]: number | string }>;
|
data?: Array<{ [key: string]: number | string }>;
|
||||||
cols?: Array<{ [key: string]: string }>;
|
cols?: Array<{ [key: string]: string }>;
|
||||||
csv?: string;
|
csv?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
|
datastoreConfig?: {
|
||||||
|
dataStoreURI: string;
|
||||||
|
rowsPerPage?: number;
|
||||||
|
dataMapperFn: (data) => Promise<TableData> | TableData;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Table = ({
|
export const Table = ({
|
||||||
@@ -39,8 +48,28 @@ export const Table = ({
|
|||||||
csv = '',
|
csv = '',
|
||||||
url = '',
|
url = '',
|
||||||
fullWidth = false,
|
fullWidth = false,
|
||||||
|
datastoreConfig,
|
||||||
}: TableProps) => {
|
}: TableProps) => {
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const [pageMap, setPageMap] = useState(new Map<number, boolean>());
|
||||||
|
const {
|
||||||
|
dataMapperFn,
|
||||||
|
dataStoreURI,
|
||||||
|
rowsPerPage = 10,
|
||||||
|
} = datastoreConfig ?? {};
|
||||||
|
|
||||||
|
const [globalFilter, setGlobalFilter] = useState('');
|
||||||
|
const [isLoadingPage, setIsLoadingPage] = useState<boolean>(false);
|
||||||
|
const [totalOfRows, setTotalOfRows] = useState<number>(0);
|
||||||
|
|
||||||
|
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: rowsPerPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [lastIndex, setLastIndex] = useState(pageSize);
|
||||||
|
const [startIndex, setStartIndex] = useState(0);
|
||||||
|
const [hasSorted, setHasSorted] = useState(false);
|
||||||
|
|
||||||
if (csv) {
|
if (csv) {
|
||||||
const out = parseCsv(csv);
|
const out = parseCsv(csv);
|
||||||
@@ -62,21 +91,56 @@ export const Table = ({
|
|||||||
);
|
);
|
||||||
}, [data, cols]);
|
}, [data, cols]);
|
||||||
|
|
||||||
const [globalFilter, setGlobalFilter] = useState('');
|
let table: ReactTable<unknown>;
|
||||||
|
|
||||||
const table = useReactTable({
|
if (datastoreConfig) {
|
||||||
data,
|
useEffect(() => {
|
||||||
columns: tableCols,
|
setIsLoading(true);
|
||||||
getCoreRowModel: getCoreRowModel(),
|
fetch(`${dataStoreURI}&limit=${rowsPerPage}&offset=0`)
|
||||||
state: {
|
.then((res) => res.json())
|
||||||
globalFilter,
|
.then(async (res) => {
|
||||||
},
|
const { data, cols, total } = await dataMapperFn(res);
|
||||||
globalFilterFn: globalFilterFn,
|
setData(data);
|
||||||
onGlobalFilterChange: setGlobalFilter,
|
setCols(cols);
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
setTotalOfRows(Math.ceil(total / rowsPerPage));
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
pageMap.set(0, true);
|
||||||
getSortedRowModel: getSortedRowModel(),
|
})
|
||||||
});
|
.finally(() => setIsLoading(false));
|
||||||
|
}, [dataStoreURI]);
|
||||||
|
|
||||||
|
table = useReactTable({
|
||||||
|
data,
|
||||||
|
pageCount: totalOfRows,
|
||||||
|
columns: tableCols,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
state: {
|
||||||
|
pagination: { pageIndex, pageSize },
|
||||||
|
},
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
manualPagination: true,
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasSorted) return;
|
||||||
|
queryDataByText(globalFilter);
|
||||||
|
}, [table.getState().sorting]);
|
||||||
|
} else {
|
||||||
|
table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns: tableCols,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
state: {
|
||||||
|
globalFilter,
|
||||||
|
},
|
||||||
|
globalFilterFn: globalFilterFn,
|
||||||
|
onGlobalFilterChange: setGlobalFilter,
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (url) {
|
if (url) {
|
||||||
@@ -91,6 +155,70 @@ export const Table = ({
|
|||||||
}
|
}
|
||||||
}, [url]);
|
}, [url]);
|
||||||
|
|
||||||
|
const queryDataByText = (filter) => {
|
||||||
|
setIsLoadingPage(true);
|
||||||
|
const sortedParam = getSortParam();
|
||||||
|
fetch(
|
||||||
|
`${dataStoreURI}&limit=${rowsPerPage}&offset=0&q=${filter}${sortedParam}`
|
||||||
|
)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(async (res) => {
|
||||||
|
const { data, total = 0 } = await dataMapperFn(res);
|
||||||
|
setTotalOfRows(Math.ceil(total / rowsPerPage));
|
||||||
|
setData(data);
|
||||||
|
const newMap = new Map();
|
||||||
|
newMap.set(0, true);
|
||||||
|
setPageMap(newMap);
|
||||||
|
table.setPageIndex(0);
|
||||||
|
setStartIndex(0);
|
||||||
|
setLastIndex(pageSize);
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoadingPage(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSortParam = () => {
|
||||||
|
const sort = table.getState().sorting;
|
||||||
|
return sort.length == 0
|
||||||
|
? ``
|
||||||
|
: '&sort=' +
|
||||||
|
sort
|
||||||
|
.map(
|
||||||
|
(x, i) =>
|
||||||
|
`${x.id}${
|
||||||
|
i === sort.length - 1 ? (x.desc ? ` desc` : ` asc`) : `,`
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
.reduce((x1, x2) => x1 + x2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryPaginatedData = (newPageIndex) => {
|
||||||
|
let newStartIndex = newPageIndex * pageSize;
|
||||||
|
setStartIndex(newStartIndex);
|
||||||
|
setLastIndex(newStartIndex + pageSize);
|
||||||
|
|
||||||
|
if (!pageMap.get(newPageIndex)) pageMap.set(newPageIndex, true);
|
||||||
|
else return;
|
||||||
|
|
||||||
|
const sortedParam = getSortParam();
|
||||||
|
|
||||||
|
setIsLoadingPage(true);
|
||||||
|
fetch(
|
||||||
|
`${dataStoreURI}&limit=${rowsPerPage}&offset=${
|
||||||
|
newStartIndex + pageSize
|
||||||
|
}&q=${globalFilter}${sortedParam}`
|
||||||
|
)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(async (res) => {
|
||||||
|
const { data: responseData } = await dataMapperFn(res);
|
||||||
|
responseData.forEach((e) => {
|
||||||
|
data[newStartIndex] = e;
|
||||||
|
newStartIndex++;
|
||||||
|
});
|
||||||
|
setData([...data]);
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoadingPage(false));
|
||||||
|
};
|
||||||
|
|
||||||
return isLoading ? (
|
return isLoading ? (
|
||||||
<div className="w-full h-full min-h-[500px] flex items-center justify-center">
|
<div className="w-full h-full min-h-[500px] flex items-center justify-center">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
@@ -99,7 +227,10 @@ export const Table = ({
|
|||||||
<div className={`${fullWidth ? 'w-[90vw] ml-[calc(50%-45vw)]' : 'w-full'}`}>
|
<div className={`${fullWidth ? 'w-[90vw] ml-[calc(50%-45vw)]' : 'w-full'}`}>
|
||||||
<DebouncedInput
|
<DebouncedInput
|
||||||
value={globalFilter ?? ''}
|
value={globalFilter ?? ''}
|
||||||
onChange={(value: any) => setGlobalFilter(String(value))}
|
onChange={(value: any) => {
|
||||||
|
if (datastoreConfig) queryDataByText(String(value));
|
||||||
|
setGlobalFilter(String(value));
|
||||||
|
}}
|
||||||
className="p-2 text-sm shadow border border-block"
|
className="p-2 text-sm shadow border border-block"
|
||||||
placeholder="Search all columns..."
|
placeholder="Search all columns..."
|
||||||
/>
|
/>
|
||||||
@@ -114,7 +245,10 @@ export const Table = ({
|
|||||||
className: h.column.getCanSort()
|
className: h.column.getCanSort()
|
||||||
? 'cursor-pointer select-none'
|
? 'cursor-pointer select-none'
|
||||||
: '',
|
: '',
|
||||||
onClick: h.column.getToggleSortingHandler(),
|
onClick: (v) => {
|
||||||
|
setHasSorted(true);
|
||||||
|
h.column.getToggleSortingHandler()(v);
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{flexRender(h.column.columnDef.header, h.getContext())}
|
{flexRender(h.column.columnDef.header, h.getContext())}
|
||||||
@@ -135,15 +269,28 @@ export const Table = ({
|
|||||||
))}
|
))}
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{table.getRowModel().rows.map((r) => (
|
{datastoreConfig && isLoadingPage ? (
|
||||||
<tr key={r.id} className="border-b border-b-slate-200">
|
<tr>
|
||||||
{r.getVisibleCells().map((c) => (
|
<td colSpan={cols.length} rowSpan={cols.length}>
|
||||||
<td key={c.id} className="py-2">
|
<div className="w-full h-full flex items-center justify-center pt-6">
|
||||||
{flexRender(c.column.columnDef.cell, c.getContext())}
|
<LoadingSpinner />
|
||||||
</td>
|
</div>
|
||||||
))}
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
) : (
|
||||||
|
(datastoreConfig
|
||||||
|
? table.getRowModel().rows.slice(startIndex, lastIndex)
|
||||||
|
: table.getRowModel().rows
|
||||||
|
).map((r) => (
|
||||||
|
<tr key={r.id} className="border-b border-b-slate-200">
|
||||||
|
{r.getVisibleCells().map((c) => (
|
||||||
|
<td key={c.id} className="py-2">
|
||||||
|
{flexRender(c.column.columnDef.cell, c.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div className="flex gap-2 items-center justify-center mt-10">
|
<div className="flex gap-2 items-center justify-center mt-10">
|
||||||
@@ -151,7 +298,10 @@ export const Table = ({
|
|||||||
className={`w-6 h-6 ${
|
className={`w-6 h-6 ${
|
||||||
!table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100'
|
!table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => table.setPageIndex(0)}
|
onClick={() => {
|
||||||
|
if (datastoreConfig) queryPaginatedData(0);
|
||||||
|
table.setPageIndex(0);
|
||||||
|
}}
|
||||||
disabled={!table.getCanPreviousPage()}
|
disabled={!table.getCanPreviousPage()}
|
||||||
>
|
>
|
||||||
<ChevronDoubleLeftIcon />
|
<ChevronDoubleLeftIcon />
|
||||||
@@ -160,7 +310,12 @@ export const Table = ({
|
|||||||
className={`w-6 h-6 ${
|
className={`w-6 h-6 ${
|
||||||
!table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100'
|
!table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => table.previousPage()}
|
onClick={() => {
|
||||||
|
if (datastoreConfig) {
|
||||||
|
queryPaginatedData(table.getState().pagination.pageIndex - 1);
|
||||||
|
}
|
||||||
|
table.previousPage();
|
||||||
|
}}
|
||||||
disabled={!table.getCanPreviousPage()}
|
disabled={!table.getCanPreviousPage()}
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon />
|
<ChevronLeftIcon />
|
||||||
@@ -176,7 +331,11 @@ export const Table = ({
|
|||||||
className={`w-6 h-6 ${
|
className={`w-6 h-6 ${
|
||||||
!table.getCanNextPage() ? 'opacity-25' : 'opacity-100'
|
!table.getCanNextPage() ? 'opacity-25' : 'opacity-100'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => table.nextPage()}
|
onClick={() => {
|
||||||
|
if (datastoreConfig)
|
||||||
|
queryPaginatedData(table.getState().pagination.pageIndex + 1);
|
||||||
|
table.nextPage();
|
||||||
|
}}
|
||||||
disabled={!table.getCanNextPage()}
|
disabled={!table.getCanNextPage()}
|
||||||
>
|
>
|
||||||
<ChevronRightIcon />
|
<ChevronRightIcon />
|
||||||
@@ -185,7 +344,11 @@ export const Table = ({
|
|||||||
className={`w-6 h-6 ${
|
className={`w-6 h-6 ${
|
||||||
!table.getCanNextPage() ? 'opacity-25' : 'opacity-100'
|
!table.getCanNextPage() ? 'opacity-25' : 'opacity-100'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
onClick={() => {
|
||||||
|
const pageIndexToNavigate = table.getPageCount() - 1;
|
||||||
|
if (datastoreConfig) queryPaginatedData(pageIndexToNavigate);
|
||||||
|
table.setPageIndex(pageIndexToNavigate);
|
||||||
|
}}
|
||||||
disabled={!table.getCanNextPage()}
|
disabled={!table.getCanNextPage()}
|
||||||
>
|
>
|
||||||
<ChevronDoubleRightIcon />
|
<ChevronDoubleRightIcon />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Wrapper for the Vega component
|
// Wrapper for the Vega component
|
||||||
import { Vega as VegaOg } from "react-vega";
|
import { Vega as VegaOg } from "react-vega";
|
||||||
|
import { VegaProps } from "react-vega/lib/Vega";
|
||||||
|
|
||||||
export function Vega(props) {
|
export function Vega(props: VegaProps) {
|
||||||
return <VegaOg {...props} />;
|
return <VegaOg {...props} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// Wrapper for the Vega Lite component
|
// Wrapper for the Vega Lite component
|
||||||
import { VegaLite as VegaLiteOg } from "react-vega";
|
import { VegaLite as VegaLiteOg } from 'react-vega';
|
||||||
import applyFullWidthDirective from "../lib/applyFullWidthDirective";
|
import { VegaLiteProps } from 'react-vega/lib/VegaLite';
|
||||||
|
import applyFullWidthDirective from '../lib/applyFullWidthDirective';
|
||||||
|
|
||||||
export function VegaLite(props) {
|
export function VegaLite(props: VegaLiteProps) {
|
||||||
const Component = applyFullWidthDirective({ Component: VegaLiteOg });
|
const Component = applyFullWidthDirective({ Component: VegaLiteOg });
|
||||||
|
|
||||||
return <Component {...props} />;
|
return <Component {...props} />;
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
export * from './components/Table';
|
|
||||||
export * from './components/Catalog';
|
export * from './components/Catalog';
|
||||||
export * from './components/LineChart';
|
export * from './components/LineChart';
|
||||||
export * from './components/Vega';
|
export * from './components/Vega';
|
||||||
export * from './components/VegaLite';
|
export * from './components/VegaLite';
|
||||||
export * from './components/FlatUiTable';
|
export * from './components/FlatUiTable';
|
||||||
export * from './components/OpenLayers/OpenLayers';
|
|
||||||
export * from './components/Map';
|
export * from './components/Map';
|
||||||
export * from './components/PdfViewer';
|
export * from './components/PdfViewer';
|
||||||
export * from "./components/Excel";
|
export * from "./components/Excel";
|
||||||
export * from "./components/BucketViewer";
|
|
||||||
export * from "./components/Iframe";
|
export * from "./components/Iframe";
|
||||||
|
export * from "./components/Plotly";
|
||||||
|
export * from "./components/PlotlyLineChart";
|
||||||
|
export * from "./components/PlotlyBarChart";
|
||||||
|
// NOTE: components that are hidden for now
|
||||||
|
// TODO: deprecate those components?
|
||||||
|
// export * from './components/Table';
|
||||||
|
// export * from "./components/BucketViewer";
|
||||||
|
// export * from './components/OpenLayers/OpenLayers';
|
||||||
|
|||||||
18
packages/components/src/types/properties.ts
Normal file
18
packages/components/src/types/properties.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* All components should use this interface for
|
||||||
|
* its data property.
|
||||||
|
* Based on vega.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
type URL = string; // Just in case we want to transform it into an object with configurations
|
||||||
|
export interface Data {
|
||||||
|
url?: URL;
|
||||||
|
values?: { [key: string]: number | string }[];
|
||||||
|
csv?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeospatialData {
|
||||||
|
url?: URL;
|
||||||
|
geojson?: GeoJSON.GeoJSON;
|
||||||
|
}
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
|
// NOTE: this component was renamed with .bkp so that it's hidden
|
||||||
|
// from the Storybook app
|
||||||
|
|
||||||
import { type Meta, type StoryObj } from '@storybook/react';
|
import { type Meta, type StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
import { BucketViewer, BucketViewerProps } from '../src/components/BucketViewer';
|
import {
|
||||||
|
BucketViewer,
|
||||||
|
BucketViewerProps,
|
||||||
|
} from '../src/components/BucketViewer';
|
||||||
import LoadingSpinner from '../src/components/LoadingSpinner';
|
import LoadingSpinner from '../src/components/LoadingSpinner';
|
||||||
|
|
||||||
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
||||||
@@ -10,19 +16,16 @@ const meta: Meta = {
|
|||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {
|
argTypes: {
|
||||||
domain: {
|
domain: {
|
||||||
description:
|
description: 'Bucket domain URI',
|
||||||
'Bucket domain URI',
|
|
||||||
},
|
},
|
||||||
suffix: {
|
suffix: {
|
||||||
description:
|
description: 'Suffix of bucket domain',
|
||||||
'Suffix of bucket domain',
|
|
||||||
},
|
},
|
||||||
downloadComponent: {
|
downloadConfig: {
|
||||||
description:
|
description: `Bucket file download configuration`,
|
||||||
'Component to be displayed on hover of each bucket data',
|
|
||||||
},
|
},
|
||||||
filterState: {
|
filterState: {
|
||||||
description: `State with values used to filter the bucket files`
|
description: `State with values used to filter the bucket files`,
|
||||||
},
|
},
|
||||||
paginationConfig: {
|
paginationConfig: {
|
||||||
description: `Configuration to show and stylise the pagination on the component`,
|
description: `Configuration to show and stylise the pagination on the component`,
|
||||||
@@ -42,17 +45,15 @@ export const Normal: Story = {
|
|||||||
suffix: '/',
|
suffix: '/',
|
||||||
dataMapperFn: async (rawData: Response) => {
|
dataMapperFn: async (rawData: Response) => {
|
||||||
const result = await rawData.json();
|
const result = await rawData.json();
|
||||||
return result.objects.map(
|
return result.objects.map((e) => ({
|
||||||
e => ({
|
downloadFileUri: e.downloadLink,
|
||||||
downloadFileUri: e.downloadLink,
|
fileName: e.key.replace(/^(\w+\/)/g, ''),
|
||||||
fileName: e.key.replace(/^(\w+\/)/g, '') ,
|
dateProps: {
|
||||||
dateProps: {
|
date: new Date(e.uploaded),
|
||||||
date: new Date(e.uploaded),
|
dateFormatter: (date) => date.toLocaleDateString(),
|
||||||
dateFormatter: (date) => date.toLocaleDateString()
|
},
|
||||||
}
|
}));
|
||||||
})
|
},
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,21 +63,19 @@ export const WithPagination: Story = {
|
|||||||
domain: 'https://ssen-smart-meter.datopian.workers.dev',
|
domain: 'https://ssen-smart-meter.datopian.workers.dev',
|
||||||
suffix: '/',
|
suffix: '/',
|
||||||
paginationConfig: {
|
paginationConfig: {
|
||||||
itemsPerPage: 3
|
itemsPerPage: 3,
|
||||||
},
|
},
|
||||||
dataMapperFn: async (rawData: Response) => {
|
dataMapperFn: async (rawData: Response) => {
|
||||||
const result = await rawData.json();
|
const result = await rawData.json();
|
||||||
return result.objects.map(
|
return result.objects.map((e) => ({
|
||||||
e => ({
|
downloadFileUri: e.downloadLink,
|
||||||
downloadFileUri: e.downloadLink,
|
fileName: e.key.replace(/^(\w+\/)/g, ''),
|
||||||
fileName: e.key.replace(/^(\w+\/)/g, '') ,
|
dateProps: {
|
||||||
dateProps: {
|
date: new Date(e.uploaded),
|
||||||
date: new Date(e.uploaded),
|
dateFormatter: (date) => date.toLocaleDateString(),
|
||||||
dateFormatter: (date) => date.toLocaleDateString()
|
},
|
||||||
}
|
}));
|
||||||
})
|
},
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,19 +84,17 @@ export const WithComponentOnHoverOfEachBucketFile: Story = {
|
|||||||
args: {
|
args: {
|
||||||
domain: 'https://ssen-smart-meter.datopian.workers.dev',
|
domain: 'https://ssen-smart-meter.datopian.workers.dev',
|
||||||
suffix: '/',
|
suffix: '/',
|
||||||
downloadComponent: LoadingSpinner(),
|
downloadConfig: { hoverOfTheFileComponent: `HOVER COMPONENT` },
|
||||||
dataMapperFn: async (rawData: Response) => {
|
dataMapperFn: async (rawData: Response) => {
|
||||||
const result = await rawData.json();
|
const result = await rawData.json();
|
||||||
return result.objects.map(
|
return result.objects.map((e) => ({
|
||||||
e => ({
|
downloadFileUri: e.downloadLink,
|
||||||
downloadFileUri: e.downloadLink,
|
fileName: e.key.replace(/^(\w+\/)/g, ''),
|
||||||
fileName: e.key.replace(/^(\w+\/)/g, '') ,
|
dateProps: {
|
||||||
dateProps: {
|
date: new Date(e.uploaded),
|
||||||
date: new Date(e.uploaded),
|
dateFormatter: (date) => date.toLocaleDateString(),
|
||||||
dateFormatter: (date) => date.toLocaleDateString()
|
},
|
||||||
}
|
}));
|
||||||
})
|
},
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -10,11 +10,14 @@ const meta: Meta = {
|
|||||||
argTypes: {
|
argTypes: {
|
||||||
datasets: {
|
datasets: {
|
||||||
description:
|
description:
|
||||||
'Lists of datasets to be displayed in the list, will usually be automatically available',
|
"Array of items to be displayed on the searchable list. Must have the following properties: \n\n \
|
||||||
|
`_id`: item's unique id \n\n \
|
||||||
|
`url_path`: href of the item \n\n \
|
||||||
|
`metadata`: object with a `title` property, that will be displayed as the title of the item, together with any other custom fields that might or not be faceted.",
|
||||||
},
|
},
|
||||||
facets: {
|
facets: {
|
||||||
description:
|
description:
|
||||||
'List of frontmatter fields that should be used as filters, needs to match exactly with the field name',
|
"Array of strings, which are name of properties in the datasets' `metadata`, which are going to be faceted.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -31,99 +34,35 @@ export const WithoutFacets: Story = {
|
|||||||
{
|
{
|
||||||
_id: '07026b22d49916754df1dc8ffb9ccd1c31878aae',
|
_id: '07026b22d49916754df1dc8ffb9ccd1c31878aae',
|
||||||
url_path: 'dataset-4',
|
url_path: 'dataset-4',
|
||||||
file_path: 'content/dataset-4/index.md',
|
|
||||||
metadata: {
|
metadata: {
|
||||||
title: 'Detecting Abusive Albanian',
|
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',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
_id: '42c86cf3c4fbbab11d91c2a7d6dcb8f750bc4e19',
|
_id: '42c86cf3c4fbbab11d91c2a7d6dcb8f750bc4e19',
|
||||||
url_path: 'dataset-1',
|
url_path: 'dataset-1',
|
||||||
file_path: 'content/dataset-1/index.md',
|
|
||||||
metadata: {
|
metadata: {
|
||||||
title: 'AbuseEval v1.0',
|
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.',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
_id: '80001dd32a752421fdcc64e91fbd237dc31d6bb3',
|
_id: '80001dd32a752421fdcc64e91fbd237dc31d6bb3',
|
||||||
url_path: 'dataset-2',
|
url_path: 'dataset-2',
|
||||||
file_path: 'content/dataset-2/index.md',
|
|
||||||
metadata: {
|
metadata: {
|
||||||
title:
|
title:
|
||||||
'Abusive Language Detection on Arabic Social Media (Al Jazeera)',
|
'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.',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
_id: '96649d05d8193f4333b10015af76c6562971bd8c',
|
_id: '96649d05d8193f4333b10015af76c6562971bd8c',
|
||||||
url_path: 'dataset-3',
|
url_path: 'dataset-3',
|
||||||
file_path: 'content/dataset-3/index.md',
|
|
||||||
metadata: {
|
metadata: {
|
||||||
title: 'CoRAL: a Context-aware Croatian Abusive Language Dataset',
|
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.',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
;
|
|
||||||
|
|
||||||
export const WithFacets: Story = {
|
export const WithFacets: Story = {
|
||||||
name: 'Catalog with facets',
|
name: 'Catalog with facets',
|
||||||
args: {
|
args: {
|
||||||
@@ -131,7 +70,6 @@ export const WithFacets: Story = {
|
|||||||
{
|
{
|
||||||
_id: '07026b22d49916754df1dc8ffb9ccd1c31878aae',
|
_id: '07026b22d49916754df1dc8ffb9ccd1c31878aae',
|
||||||
url_path: 'dataset-4',
|
url_path: 'dataset-4',
|
||||||
file_path: 'content/dataset-4/index.md',
|
|
||||||
metadata: {
|
metadata: {
|
||||||
title: 'Detecting Abusive Albanian',
|
title: 'Detecting Abusive Albanian',
|
||||||
'link-to-publication': 'https://arxiv.org/abs/2107.13592',
|
'link-to-publication': 'https://arxiv.org/abs/2107.13592',
|
||||||
@@ -220,7 +158,6 @@ export const WithFacets: Story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
facets: ['language', 'platform']
|
facets: ['language', 'platform'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
;
|
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { Excel, ExcelProps } from '../src/components/Excel';
|
|||||||
|
|
||||||
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title: 'Components/Excel',
|
title: 'Components/Tabular/Excel',
|
||||||
component: Excel,
|
component: Excel,
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {
|
argTypes: {
|
||||||
url: {
|
data: {
|
||||||
description:
|
description:
|
||||||
'Url of the file to be displayed e.g.: "https://url.to/data.csv"',
|
'Object with a `url` property pointing to the Excel file to be displayed, e.g.: `{ url: "https://url.to/data.csv" }`',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -22,13 +22,17 @@ type Story = StoryObj<ExcelProps>;
|
|||||||
export const SingleSheet: Story = {
|
export const SingleSheet: Story = {
|
||||||
name: 'Excel file with just one sheet',
|
name: 'Excel file with just one sheet',
|
||||||
args: {
|
args: {
|
||||||
url: 'https://sheetjs.com/pres.xlsx',
|
data: {
|
||||||
|
url: 'https://sheetjs.com/pres.xlsx',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MultipleSheet: Story = {
|
export const MultipleSheet: Story = {
|
||||||
name: 'Excel file with multiple sheets',
|
name: 'Excel file with multiple sheets',
|
||||||
args: {
|
args: {
|
||||||
url: 'https://storage.portaljs.org/IC-Gantt-Chart-Project-Template-8857.xlsx',
|
data: {
|
||||||
|
url: 'https://storage.portaljs.org/IC-Gantt-Chart-Project-Template-8857.xlsx',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,29 +4,31 @@ import { FlatUiTable, FlatUiTableProps } from '../src/components/FlatUiTable';
|
|||||||
|
|
||||||
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title: 'Components/FlatUiTable',
|
title: 'Components/Tabular/FlatUiTable',
|
||||||
component: FlatUiTable,
|
component: FlatUiTable,
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {
|
argTypes: {
|
||||||
data: {
|
data: {
|
||||||
description:
|
description:
|
||||||
'Data to be displayed in the table, must be setup as an array of key value pairs',
|
'Data to be displayed. \n\n \
|
||||||
},
|
Must be an object with one of the following properties: `url`, `values` or `csv` \n\n \
|
||||||
csv: {
|
`url`: URL pointing to a CSV file. \n\n \
|
||||||
description: 'CSV data as string.',
|
`values`: array of objects. \n\n \
|
||||||
},
|
`csv`: string with valid CSV. \n\n \
|
||||||
url: {
|
',
|
||||||
description:
|
|
||||||
'Fetch the data from a CSV file remotely. only the first 5MB of data will be displayed',
|
|
||||||
},
|
},
|
||||||
bytes: {
|
bytes: {
|
||||||
description:
|
description:
|
||||||
'Fetch the data from a CSV file remotely. only the first <bytes> of data will be displayed',
|
'Fetch the data from a CSV file remotely. Only the first <bytes> of data will be displayed. Defaults to 5MB.',
|
||||||
},
|
},
|
||||||
parsingConfig: {
|
parsingConfig: {
|
||||||
description:
|
description:
|
||||||
'Configuration for parsing the CSV data. See https://www.papaparse.com/docs#config for more details',
|
'Configuration for parsing the CSV data. See https://www.papaparse.com/docs#config for more details',
|
||||||
},
|
},
|
||||||
|
uniqueId: {
|
||||||
|
description:
|
||||||
|
'Provide a unique ID to help with cache revalidation of the fetched data.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,34 +38,40 @@ type Story = StoryObj<FlatUiTableProps>;
|
|||||||
|
|
||||||
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
|
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
|
||||||
export const FromColumnsAndData: Story = {
|
export const FromColumnsAndData: Story = {
|
||||||
name: 'Table data',
|
name: 'Table from array or objects',
|
||||||
args: {
|
args: {
|
||||||
data: [
|
data: {
|
||||||
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
|
values: [
|
||||||
{ id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
|
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
|
||||||
{ id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
|
{ id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
|
||||||
{ id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 },
|
{ id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
|
||||||
{ id: 7, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
|
{ id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 },
|
||||||
{ id: 8, lastName: 'Frances', firstName: 'Rossini', age: 36 },
|
{ id: 7, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
|
||||||
{ id: 9, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
|
{ id: 8, lastName: 'Frances', firstName: 'Rossini', age: 36 },
|
||||||
],
|
{ id: 9, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FromRawCSV: Story = {
|
export const FromRawCSV: Story = {
|
||||||
name: 'Table from raw CSV',
|
name: 'Table from inline CSV',
|
||||||
args: {
|
args: {
|
||||||
rawCsv: `
|
data: {
|
||||||
|
csv: `
|
||||||
Year,Temp Anomaly
|
Year,Temp Anomaly
|
||||||
1850,-0.418
|
1850,-0.418
|
||||||
2020,0.923
|
2020,0.923
|
||||||
`,
|
`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FromURL: Story = {
|
export const FromURL: Story = {
|
||||||
name: 'Table from URL',
|
name: 'Table from URL',
|
||||||
args: {
|
args: {
|
||||||
url: 'https://storage.openspending.org/alberta-budget/__os_imported__alberta_total.csv',
|
data: {
|
||||||
|
url: 'https://storage.openspending.org/alberta-budget/__os_imported__alberta_total.csv',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ import { type Meta, type StoryObj } from '@storybook/react';
|
|||||||
import { Iframe, IframeProps } from '../src/components/Iframe';
|
import { Iframe, IframeProps } from '../src/components/Iframe';
|
||||||
|
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title: 'Components/Iframe',
|
title: 'Components/Embedding/Iframe',
|
||||||
component: Iframe,
|
component: Iframe,
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {
|
argTypes: {
|
||||||
url: {
|
data: {
|
||||||
description:
|
description:
|
||||||
'Page to display inside of the component',
|
'Object with a `url` property pointing to the page to be embeded.',
|
||||||
},
|
},
|
||||||
style: {
|
style: {
|
||||||
description:
|
description:
|
||||||
'Style of the component',
|
'Style object of the component. See example at https://react.dev/learn#displaying-data. Defaults to `{ width: "100%", height: "100%" }`',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -25,7 +25,9 @@ type Story = StoryObj<IframeProps>;
|
|||||||
export const Normal: Story = {
|
export const Normal: Story = {
|
||||||
name: 'Iframe',
|
name: 'Iframe',
|
||||||
args: {
|
args: {
|
||||||
url: 'https://app.powerbi.com/view?r=eyJrIjoiYzBmN2Q2MzYtYzE3MS00ODkxLWE5OWMtZTQ2MjBlMDljMDk4IiwidCI6Ijk1M2IwZjgzLTFjZTYtNDVjMy04MmM5LTFkODQ3ZTM3MjMzOSIsImMiOjh9',
|
data: {
|
||||||
style: {width: `100%`, height: `100%`}
|
url: 'https://app.powerbi.com/view?r=eyJrIjoiYzBmN2Q2MzYtYzE3MS00ODkxLWE5OWMtZTQ2MjBlMDljMDk4IiwidCI6Ijk1M2IwZjgzLTFjZTYtNDVjMy04MmM5LTFkODQ3ZTM3MjMzOSIsImMiOjh9',
|
||||||
|
},
|
||||||
|
style: { width: `100%`, height: `100%` },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,37 +4,36 @@ import { LineChart, LineChartProps } from '../src/components/LineChart';
|
|||||||
|
|
||||||
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title: 'Components/LineChart',
|
title: 'Components/Charts/LineChart',
|
||||||
component: LineChart,
|
component: LineChart,
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {
|
argTypes: {
|
||||||
data: {
|
data: {
|
||||||
description:
|
description:
|
||||||
'Data to be displayed.\n\n E.g.: [["1990", 1], ["1991", 2]] \n\nOR\n\n "https://url.to/data.csv"',
|
'Data to be displayed. \n\n \
|
||||||
|
Must be an object with one of the following properties: `url` or `values` \n\n \
|
||||||
|
`url`: URL pointing to a CSV file. \n\n \
|
||||||
|
`values`: array of objects \n\n',
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
description: 'Title to display on the chart. Optional.',
|
description: 'Title to display on the chart.',
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
description:
|
description:
|
||||||
'Name of the X axis on the data. Required when the "data" parameter is an URL.',
|
'Name of the column header or object property that represents the X-axis on the data.',
|
||||||
},
|
},
|
||||||
xAxisType: {
|
xAxisType: {
|
||||||
description: 'Type of the X axis',
|
description: 'Type of the X-axis.',
|
||||||
},
|
},
|
||||||
xAxisTimeUnit: {
|
xAxisTimeUnit: {
|
||||||
description: 'Time unit of the X axis (optional)',
|
description: 'Time unit of the X-axis, in case its type is `temporal.`',
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
description:
|
description:
|
||||||
'Name of the Y axis on the data. Required when the "data" parameter is an URL.',
|
'Name of the column header or object property that represents the Y-axis on the data.',
|
||||||
},
|
},
|
||||||
yAxisType: {
|
yAxisType: {
|
||||||
description: 'Type of the Y axis',
|
description: 'Type of the Y-axis',
|
||||||
},
|
|
||||||
fullWidth: {
|
|
||||||
description:
|
|
||||||
'Whether the component should be rendered as full bleed or not',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -47,21 +46,27 @@ type Story = StoryObj<LineChartProps>;
|
|||||||
export const FromDataPoints: Story = {
|
export const FromDataPoints: Story = {
|
||||||
name: 'Line chart from array of data points',
|
name: 'Line chart from array of data points',
|
||||||
args: {
|
args: {
|
||||||
data: [
|
data: {
|
||||||
['1850', -0.41765878],
|
values: [
|
||||||
['1851', -0.2333498],
|
{ year: '1850', value: -0.41765878 },
|
||||||
['1852', -0.22939907],
|
{ year: '1851', value: -0.2333498 },
|
||||||
['1853', -0.27035445],
|
{ year: '1852', value: -0.22939907 },
|
||||||
['1854', -0.29163003],
|
{ year: '1853', value: -0.27035445 },
|
||||||
],
|
{ year: '1854', value: -0.29163003 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
xAxis: 'year',
|
||||||
|
yAxis: 'value',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FromURL: Story = {
|
export const FromURL: Story = {
|
||||||
name: 'Line chart from URL',
|
name: 'Line chart from URL',
|
||||||
args: {
|
args: {
|
||||||
|
data: {
|
||||||
|
url: 'https://raw.githubusercontent.com/datasets/oil-prices/main/data/wti-year.csv',
|
||||||
|
},
|
||||||
title: 'Oil Price x Year',
|
title: 'Oil Price x Year',
|
||||||
data: 'https://raw.githubusercontent.com/datasets/oil-prices/main/data/wti-year.csv',
|
|
||||||
xAxis: 'Date',
|
xAxis: 'Date',
|
||||||
yAxis: 'Price',
|
yAxis: 'Price',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,29 +4,34 @@ import { Map, MapProps } from '../src/components/Map';
|
|||||||
|
|
||||||
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title: 'Components/Map',
|
title: 'Components/Geospatial/Map',
|
||||||
component: Map,
|
component: Map,
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {
|
argTypes: {
|
||||||
layers: {
|
layers: {
|
||||||
description:
|
description:
|
||||||
'Data to be displayed.\n\n GeoJSON Object \n\nOR\n\n URL to GeoJSON Object',
|
'Array of layers to be displayed on the map. Should be an object with: \n\n \
|
||||||
|
`data`: object with either a `url` property pointing to a GeoJSON file or a `geojson` property with a GeoJSON object. \n\n \
|
||||||
|
`name`: name of the layer. \n\n \
|
||||||
|
`colorscale`: object with a `starting` and `ending` colors that will be used to create a gradient and color the map. \n\n \
|
||||||
|
`tooltip`: `true` to show all available features on the tooltip, object with a `propNames` property as an array of strings to choose which features to display. \n\n',
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
description: 'Title to display on the map. Optional.',
|
description: 'Title to display on the map.',
|
||||||
},
|
},
|
||||||
center: {
|
center: {
|
||||||
description: 'Initial coordinates of the center of the map',
|
description: 'Initial coordinates of the center of the map',
|
||||||
},
|
},
|
||||||
zoom: {
|
zoom: {
|
||||||
description: 'Zoom level',
|
description: 'Initial zoom level',
|
||||||
},
|
},
|
||||||
style: {
|
style: {
|
||||||
description: "Styles for the container"
|
description: "CSS styles to be applied to the map's container.",
|
||||||
},
|
},
|
||||||
autoZoomConfiguration: {
|
autoZoomConfiguration: {
|
||||||
description: "Configuration to auto zoom in the specified layer data"
|
description:
|
||||||
}
|
"Pass a layer's name to automatically zoom to the bounding area of a layer.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -40,7 +45,9 @@ export const GeoJSONPolygons: Story = {
|
|||||||
args: {
|
args: {
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
data: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_marine_polys.geojson',
|
data: {
|
||||||
|
url: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_marine_polys.geojson',
|
||||||
|
},
|
||||||
name: 'Polygons',
|
name: 'Polygons',
|
||||||
tooltip: { propNames: ['name'] },
|
tooltip: { propNames: ['name'] },
|
||||||
colorScale: {
|
colorScale: {
|
||||||
@@ -60,7 +67,9 @@ export const GeoJSONPoints: Story = {
|
|||||||
args: {
|
args: {
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
data: 'https://opendata.arcgis.com/datasets/9c58741995174fbcb017cf46c8a42f4b_25.geojson',
|
data: {
|
||||||
|
url: 'https://opendata.arcgis.com/datasets/9c58741995174fbcb017cf46c8a42f4b_25.geojson',
|
||||||
|
},
|
||||||
name: 'Points',
|
name: 'Points',
|
||||||
tooltip: { propNames: ['Location'] },
|
tooltip: { propNames: ['Location'] },
|
||||||
},
|
},
|
||||||
@@ -76,12 +85,16 @@ export const GeoJSONMultipleLayers: Story = {
|
|||||||
args: {
|
args: {
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
data: 'https://opendata.arcgis.com/datasets/9c58741995174fbcb017cf46c8a42f4b_25.geojson',
|
data: {
|
||||||
|
url: 'https://opendata.arcgis.com/datasets/9c58741995174fbcb017cf46c8a42f4b_25.geojson',
|
||||||
|
},
|
||||||
name: 'Points',
|
name: 'Points',
|
||||||
tooltip: true,
|
tooltip: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_marine_polys.geojson',
|
data: {
|
||||||
|
url: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_marine_polys.geojson',
|
||||||
|
},
|
||||||
name: 'Polygons',
|
name: 'Polygons',
|
||||||
tooltip: true,
|
tooltip: true,
|
||||||
colorScale: {
|
colorScale: {
|
||||||
@@ -94,19 +107,23 @@ export const GeoJSONMultipleLayers: Story = {
|
|||||||
center: { latitude: 45, longitude: 0 },
|
center: { latitude: 45, longitude: 0 },
|
||||||
zoom: 2,
|
zoom: 2,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const GeoJSONMultipleLayersWithAutoZoomInSpecifiedLayer: Story = {
|
export const GeoJSONMultipleLayersWithAutoZoomInSpecifiedLayer: Story = {
|
||||||
name: 'GeoJSON polygons and points map with auto zoom in the points layer',
|
name: 'GeoJSON polygons and points map with auto zoom in the points layer',
|
||||||
args: {
|
args: {
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
data: 'https://opendata.arcgis.com/datasets/9c58741995174fbcb017cf46c8a42f4b_25.geojson',
|
data: {
|
||||||
|
url: 'https://opendata.arcgis.com/datasets/9c58741995174fbcb017cf46c8a42f4b_25.geojson',
|
||||||
|
},
|
||||||
name: 'Points',
|
name: 'Points',
|
||||||
tooltip: true,
|
tooltip: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_marine_polys.geojson',
|
data: {
|
||||||
|
url: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_marine_polys.geojson',
|
||||||
|
},
|
||||||
name: 'Polygons',
|
name: 'Polygons',
|
||||||
tooltip: true,
|
tooltip: true,
|
||||||
colorScale: {
|
colorScale: {
|
||||||
@@ -119,7 +136,7 @@ export const GeoJSONMultipleLayersWithAutoZoomInSpecifiedLayer: Story = {
|
|||||||
center: { latitude: 45, longitude: 0 },
|
center: { latitude: 45, longitude: 0 },
|
||||||
zoom: 2,
|
zoom: 2,
|
||||||
autoZoomConfiguration: {
|
autoZoomConfiguration: {
|
||||||
layerName: 'Points'
|
layerName: 'Points',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// NOTE: this component was renamed with .bkp so that it's hidden
|
||||||
|
// from the Storybook app
|
||||||
|
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import OpenLayers from '../src/components/OpenLayers/OpenLayers';
|
import OpenLayers from '../src/components/OpenLayers/OpenLayers';
|
||||||
@@ -3,19 +3,21 @@ import type { Meta, StoryObj } from '@storybook/react';
|
|||||||
import { PdfViewer, PdfViewerProps } from '../src/components/PdfViewer';
|
import { PdfViewer, PdfViewerProps } from '../src/components/PdfViewer';
|
||||||
|
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title: 'Components/PdfViewer',
|
title: 'Components/Embedding/PdfViewer',
|
||||||
component: PdfViewer,
|
component: PdfViewer,
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {
|
argTypes: {
|
||||||
url: {
|
data: {
|
||||||
description: 'URL to PDF file',
|
description:
|
||||||
|
'Object with a `url` property pointing to the PDF file to be displayed, e.g.: `{ url: "https://cdn.filestackcontent.com/wcrjf9qPTCKXV3hMXDwK" }`.',
|
||||||
},
|
},
|
||||||
parentClassName: {
|
parentClassName: {
|
||||||
description: 'Classname for the parent div of the pdf viewer',
|
|
||||||
},
|
|
||||||
layour: {
|
|
||||||
description:
|
description:
|
||||||
'Set to true if you want to have a layout with zoom level, page count, printing button etc',
|
'HTML classes to be applied to the container of the PDF viewer. [Tailwind](https://tailwindcss.com/) classes, such as `h-96` to define the height of the component, can be used on this field.',
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
description:
|
||||||
|
'Set to `true` if you want to display a layout with zoom level, page count, printing button and other controls.',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -25,26 +27,23 @@ export default meta;
|
|||||||
|
|
||||||
type Story = StoryObj<PdfViewerProps>;
|
type Story = StoryObj<PdfViewerProps>;
|
||||||
|
|
||||||
export const PdfViewerStory: Story = {
|
export const PdfViewerStoryWithoutControlsLayout: Story = {
|
||||||
name: 'PdfViewer',
|
name: 'PDF Viewer without controls layout',
|
||||||
args: {
|
args: {
|
||||||
url: 'https://cdn.filestackcontent.com/wcrjf9qPTCKXV3hMXDwK',
|
data: {
|
||||||
},
|
url: 'https://cdn.filestackcontent.com/wcrjf9qPTCKXV3hMXDwK',
|
||||||
};
|
},
|
||||||
|
parentClassName: 'h-96',
|
||||||
export const PdfViewerStoryWithLayout: Story = {
|
},
|
||||||
name: 'PdfViewer with the default layout',
|
};
|
||||||
args: {
|
|
||||||
url: 'https://cdn.filestackcontent.com/wcrjf9qPTCKXV3hMXDwK',
|
export const PdfViewerStoryWithControlsLayout: Story = {
|
||||||
layout: true,
|
name: 'PdfViewer with controls layout',
|
||||||
},
|
args: {
|
||||||
};
|
data: {
|
||||||
|
url: 'https://cdn.filestackcontent.com/wcrjf9qPTCKXV3hMXDwK',
|
||||||
export const PdfViewerStoryWithHeight: Story = {
|
},
|
||||||
name: 'PdfViewer with a custom height',
|
layout: true,
|
||||||
args: {
|
|
||||||
url: 'https://cdn.filestackcontent.com/wcrjf9qPTCKXV3hMXDwK',
|
|
||||||
parentClassName: 'h-96',
|
parentClassName: 'h-96',
|
||||||
layout: true,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
49
packages/components/stories/Plotly.stories.ts
Normal file
49
packages/components/stories/Plotly.stories.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Plotly } from '../src/components/Plotly';
|
||||||
|
|
||||||
|
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'Components/Charts/Plotly',
|
||||||
|
component: Plotly,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
data: {
|
||||||
|
description:
|
||||||
|
"Plotly's `data` prop. You can find references on how to use these props at https://github.com/plotly/react-plotly.js/#basic-props.",
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
description:
|
||||||
|
"Plotly's `layout` prop. You can find references on how to use these props at https://github.com/plotly/react-plotly.js/#basic-props.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<any>;
|
||||||
|
|
||||||
|
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
|
||||||
|
export const Primary: Story = {
|
||||||
|
name: 'Line chart',
|
||||||
|
args: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
x: [1, 2, 3],
|
||||||
|
y: [2, 6, 3],
|
||||||
|
type: 'scatter',
|
||||||
|
mode: 'lines+markers',
|
||||||
|
marker: { color: 'red' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
layout: {
|
||||||
|
title: 'Chart built with Plotly',
|
||||||
|
xaxis: {
|
||||||
|
title: 'x Axis',
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
title: 'y Axis',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
102
packages/components/stories/PlotlyBarChart.stories.ts
Normal file
102
packages/components/stories/PlotlyBarChart.stories.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
PlotlyBarChart,
|
||||||
|
PlotlyBarChartProps,
|
||||||
|
} from '../src/components/PlotlyBarChart';
|
||||||
|
|
||||||
|
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'Components/Charts/PlotlyBarChart',
|
||||||
|
component: PlotlyBarChart,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
data: {
|
||||||
|
description:
|
||||||
|
'Data to be displayed. \n\n \
|
||||||
|
Must be an object with one of the following properties: `url`, `values` or `csv` \n\n \
|
||||||
|
`url`: URL pointing to a CSV file. \n\n \
|
||||||
|
`values`: array of objects (check out [this example](/?path=/story/components-plotlybarchart--from-data-points)) \n\n \
|
||||||
|
`csv`: string with valid CSV (check out [this example](/?path=/story/components-plotlybarchart--from-inline-csv)) \n\n \
|
||||||
|
',
|
||||||
|
},
|
||||||
|
bytes: {
|
||||||
|
// TODO: likely this should be an extra option on the data parameter,
|
||||||
|
// specific to URLs
|
||||||
|
description:
|
||||||
|
"How many bytes to read from the url so that the entire file doesn's have to be fetched.",
|
||||||
|
},
|
||||||
|
parsingConfig: {
|
||||||
|
description:
|
||||||
|
'If using URL or CSV, this parsing config will be used to parse the data. Check https://www.papaparse.com/ for more info.',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
description: 'Title to display on the chart.',
|
||||||
|
},
|
||||||
|
// TODO: commented out because this doesn't work
|
||||||
|
// lineLabel: {
|
||||||
|
// description:
|
||||||
|
// 'Label to display on the line, Optional, will use yAxis if not provided',
|
||||||
|
// },
|
||||||
|
xAxis: {
|
||||||
|
description:
|
||||||
|
'Name of the column header or object property that represents the X-axis on the data.',
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
description:
|
||||||
|
'Name of the column header or object property that represents the Y-axis on the data.',
|
||||||
|
},
|
||||||
|
uniqueId: {
|
||||||
|
description: 'Provide a unique ID to help with cache revalidation of the fetched data.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<PlotlyBarChartProps>;
|
||||||
|
|
||||||
|
export const FromDataPoints: Story = {
|
||||||
|
name: 'Bar chart from array of data points',
|
||||||
|
args: {
|
||||||
|
data: {
|
||||||
|
values: [
|
||||||
|
{ year: '1850', temperature: -0.41765878 },
|
||||||
|
{ year: '1851', temperature: -0.2333498 },
|
||||||
|
{ year: '1852', temperature: -0.22939907 },
|
||||||
|
{ year: '1853', temperature: -0.27035445 },
|
||||||
|
{ year: '1854', temperature: -0.29163003 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
xAxis: 'year',
|
||||||
|
yAxis: 'temperature',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FromURL: Story = {
|
||||||
|
name: 'Bar chart from URL',
|
||||||
|
args: {
|
||||||
|
title: 'Apple Stock Prices',
|
||||||
|
data: {
|
||||||
|
url: 'https://raw.githubusercontent.com/plotly/datasets/master/finance-charts-apple.csv',
|
||||||
|
},
|
||||||
|
xAxis: 'Date',
|
||||||
|
yAxis: 'AAPL.Open',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FromInlineCSV: Story = {
|
||||||
|
name: 'Bar chart from inline CSV',
|
||||||
|
args: {
|
||||||
|
title: 'Apple Stock Prices',
|
||||||
|
data: {
|
||||||
|
csv: `Date,AAPL.Open,AAPL.High,AAPL.Low,AAPL.Close,AAPL.Volume,AAPL.Adjusted,dn,mavg,up,direction
|
||||||
|
2015-02-17,127.489998,128.880005,126.919998,127.830002,63152400,122.905254,106.7410523,117.9276669,129.1142814,Increasing
|
||||||
|
2015-02-18,127.629997,128.779999,127.449997,128.720001,44891700,123.760965,107.842423,118.9403335,130.0382439,Increasing
|
||||||
|
2015-02-19,128.479996,129.029999,128.330002,128.449997,37362400,123.501363,108.8942449,119.8891668,130.8840887,Decreasing
|
||||||
|
2015-02-20,128.619995,129.5,128.050003,129.5,48948400,124.510914,109.7854494,120.7635001,131.7415509,Increasing`,
|
||||||
|
},
|
||||||
|
xAxis: 'Date',
|
||||||
|
yAxis: 'AAPL.Open',
|
||||||
|
},
|
||||||
|
};
|
||||||
101
packages/components/stories/PlotlyLineChart.stories.ts
Normal file
101
packages/components/stories/PlotlyLineChart.stories.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
PlotlyLineChart,
|
||||||
|
PlotlyLineChartProps,
|
||||||
|
} from '../src/components/PlotlyLineChart';
|
||||||
|
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'Components/Charts/PlotlyLineChart',
|
||||||
|
component: PlotlyLineChart,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
data: {
|
||||||
|
description:
|
||||||
|
'Data to be displayed. \n\n \
|
||||||
|
Must be an object with one of the following properties: `url`, `values` or `csv` \n\n \
|
||||||
|
`url`: URL pointing to a CSV file. \n\n \
|
||||||
|
`values`: array of objects. \n\n \
|
||||||
|
`csv`: string with valid CSV. \n\n \
|
||||||
|
',
|
||||||
|
},
|
||||||
|
bytes: {
|
||||||
|
// TODO: likely this should be an extra option on the data parameter,
|
||||||
|
// specific to URLs
|
||||||
|
description:
|
||||||
|
"How many bytes to read from the url so that the entire file doesn's have to be fetched.",
|
||||||
|
},
|
||||||
|
parsingConfig: {
|
||||||
|
description:
|
||||||
|
'If using URL or CSV, this parsing config will be used to parse the data. Check https://www.papaparse.com/ for more info',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
description: 'Title to display on the chart.',
|
||||||
|
},
|
||||||
|
lineLabel: {
|
||||||
|
description:
|
||||||
|
'Label to display on the line, will use yAxis if not provided',
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
description:
|
||||||
|
'Name of the column header or object property that represents the X-axis on the data.',
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
description:
|
||||||
|
'Name of the column header or object property that represents the Y-axis on the data.',
|
||||||
|
},
|
||||||
|
uniqueId: {
|
||||||
|
description:
|
||||||
|
'Provide a unique ID to help with cache revalidation of the fetched data.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<PlotlyLineChartProps>;
|
||||||
|
|
||||||
|
export const FromDataPoints: Story = {
|
||||||
|
name: 'Line chart from array of data points',
|
||||||
|
args: {
|
||||||
|
data: {
|
||||||
|
values: [
|
||||||
|
{ year: '1850', temperature: -0.41765878 },
|
||||||
|
{ year: '1851', temperature: -0.2333498 },
|
||||||
|
{ year: '1852', temperature: -0.22939907 },
|
||||||
|
{ year: '1853', temperature: -0.27035445 },
|
||||||
|
{ year: '1854', temperature: -0.29163003 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
xAxis: 'year',
|
||||||
|
yAxis: 'temperature',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FromURL: Story = {
|
||||||
|
name: 'Line chart from URL',
|
||||||
|
args: {
|
||||||
|
title: 'Oil Price x Year',
|
||||||
|
data: {
|
||||||
|
url: 'https://raw.githubusercontent.com/datasets/oil-prices/main/data/wti-year.csv',
|
||||||
|
},
|
||||||
|
xAxis: 'Date',
|
||||||
|
yAxis: 'Price',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FromInlineCSV: Story = {
|
||||||
|
name: 'Bar chart from inline CSV',
|
||||||
|
args: {
|
||||||
|
title: 'Apple Stock Prices',
|
||||||
|
data: {
|
||||||
|
csv: `Date,AAPL.Open,AAPL.High,AAPL.Low,AAPL.Close,AAPL.Volume,AAPL.Adjusted,dn,mavg,up,direction
|
||||||
|
2015-02-17,127.489998,128.880005,126.919998,127.830002,63152400,122.905254,106.7410523,117.9276669,129.1142814,Increasing
|
||||||
|
2015-02-18,127.629997,128.779999,127.449997,128.720001,44891700,123.760965,107.842423,118.9403335,130.0382439,Increasing
|
||||||
|
2015-02-19,128.479996,129.029999,128.330002,128.449997,37362400,123.501363,108.8942449,119.8891668,130.8840887,Decreasing
|
||||||
|
2015-02-20,128.619995,129.5,128.050003,129.5,48948400,124.510914,109.7854494,120.7635001,131.7415509,Increasing`,
|
||||||
|
},
|
||||||
|
xAxis: 'Date',
|
||||||
|
yAxis: 'AAPL.Open',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,25 +1,33 @@
|
|||||||
|
// NOTE: this component was renamed with .bkp so that it's hidden
|
||||||
|
// from the Storybook app
|
||||||
|
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
import { Table, TableProps } from '../src/components/Table';
|
import { Table, TableProps } from '../src/components/Table';
|
||||||
|
|
||||||
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title: 'Components/Table',
|
title: 'Components/Tabular/Table',
|
||||||
component: Table,
|
component: Table,
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {
|
argTypes: {
|
||||||
data: {
|
data: {
|
||||||
description: "Data to be displayed in the table, must also set \"cols\" to work."
|
description:
|
||||||
|
'Data to be displayed in the table, must also set "cols" to work.',
|
||||||
},
|
},
|
||||||
cols: {
|
cols: {
|
||||||
description: "Columns to be displayed in the table, must also set \"data\" to work."
|
description:
|
||||||
|
'Columns to be displayed in the table, must also set "data" to work.',
|
||||||
},
|
},
|
||||||
csv: {
|
csv: {
|
||||||
description: "CSV data as string.",
|
description: 'CSV data as string.',
|
||||||
},
|
},
|
||||||
url: {
|
url: {
|
||||||
description: "Fetch the data from a CSV file remotely."
|
description: 'Fetch the data from a CSV file remotely.',
|
||||||
}
|
},
|
||||||
|
datastoreConfig: {
|
||||||
|
description: `Configuration to use CKAN's datastore API extension integrated with the component`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,7 +37,7 @@ type Story = StoryObj<TableProps>;
|
|||||||
|
|
||||||
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
|
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
|
||||||
export const FromColumnsAndData: Story = {
|
export const FromColumnsAndData: Story = {
|
||||||
name: "Table from columns and data",
|
name: 'Table from columns and data',
|
||||||
args: {
|
args: {
|
||||||
data: [
|
data: [
|
||||||
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
|
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
|
||||||
@@ -49,21 +57,40 @@ export const FromColumnsAndData: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const WithDataStoreIntegration: Story = {
|
||||||
|
name: 'Table with datastore integration',
|
||||||
|
args: {
|
||||||
|
datastoreConfig: {
|
||||||
|
dataStoreURI: `https://www.civicdata.com/api/action/datastore_search?resource_id=46ec0807-31ff-497f-bfa0-f31c796cdee8`,
|
||||||
|
dataMapperFn: ({
|
||||||
|
result,
|
||||||
|
}: {
|
||||||
|
result: { fields: { id }[]; records: []; total: number };
|
||||||
|
}) => {
|
||||||
|
return {
|
||||||
|
data: result.records,
|
||||||
|
cols: result.fields.map((x) => ({ key: x.id, name: x.id })),
|
||||||
|
total: result.total,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const FromRawCSV: Story = {
|
export const FromRawCSV: Story = {
|
||||||
name: "Table from raw CSV",
|
name: 'Table from raw CSV',
|
||||||
args: {
|
args: {
|
||||||
csv: `
|
csv: `
|
||||||
Year,Temp Anomaly
|
Year,Temp Anomaly
|
||||||
1850,-0.418
|
1850,-0.418
|
||||||
2020,0.923
|
2020,0.923
|
||||||
`
|
`,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FromURL: Story = {
|
export const FromURL: Story = {
|
||||||
name: "Table from URL",
|
name: 'Table from URL',
|
||||||
args: {
|
args: {
|
||||||
url: "https://raw.githubusercontent.com/datasets/finance-vix/main/data/vix-daily.csv"
|
url: 'https://raw.githubusercontent.com/datasets/finance-vix/main/data/vix-daily.csv',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -4,9 +4,19 @@ import { Vega } from '../src/components/Vega';
|
|||||||
|
|
||||||
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title: 'Components/Vega',
|
title: 'Components/Charts/Vega',
|
||||||
component: Vega,
|
component: Vega,
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
data: {
|
||||||
|
description:
|
||||||
|
"Vega's `data` prop. You can find references on how to use this prop at https://vega.github.io/vega/docs/data/",
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
description:
|
||||||
|
"Vega's `spec` prop. You can find references on how to use this prop at https://vega.github.io/vega/docs/specification/",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@@ -15,7 +25,7 @@ type Story = StoryObj<any>;
|
|||||||
|
|
||||||
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
|
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
|
||||||
export const Primary: Story = {
|
export const Primary: Story = {
|
||||||
name: 'Chart built with Vega',
|
name: 'Bar chart',
|
||||||
args: {
|
args: {
|
||||||
data: {
|
data: {
|
||||||
table: [
|
table: [
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { VegaLite } from '../src/components/VegaLite';
|
|||||||
|
|
||||||
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title: 'Components/VegaLite',
|
title: 'Components/Charts/VegaLite',
|
||||||
component: VegaLite,
|
component: VegaLite,
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {
|
argTypes: {
|
||||||
@@ -25,7 +25,7 @@ type Story = StoryObj<any>;
|
|||||||
|
|
||||||
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
|
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
|
||||||
export const Primary: Story = {
|
export const Primary: Story = {
|
||||||
name: 'Chart built with Vega Lite',
|
name: 'Bar chart',
|
||||||
args: {
|
args: {
|
||||||
data: {
|
data: {
|
||||||
table: [
|
table: [
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ export const SiteToc: React.FC<Props> = ({ currentPath, nav }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav data-testid="lhs-sidebar" className="flex flex-col space-y-3 text-sm">
|
<nav data-testid="lhs-sidebar" className="flex flex-col space-y-3 text-sm">
|
||||||
{sortNavGroupChildren(nav).map((n) => (
|
{sortNavGroupChildren(nav).map((n, index) => (
|
||||||
<NavComponent item={n} isActive={false} />
|
<NavComponent key={index} item={n} isActive={false} />
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
@@ -96,8 +96,8 @@ const NavComponent: React.FC<{
|
|||||||
leaveTo="transform scale-95 opacity-0"
|
leaveTo="transform scale-95 opacity-0"
|
||||||
>
|
>
|
||||||
<Disclosure.Panel className="flex flex-col space-y-3 pl-5 mt-3">
|
<Disclosure.Panel className="flex flex-col space-y-3 pl-5 mt-3">
|
||||||
{sortNavGroupChildren(item.children).map((subItem) => (
|
{sortNavGroupChildren(item.children).map((subItem, index) => (
|
||||||
<NavComponent item={subItem} isActive={false} />
|
<NavComponent key={index} item={subItem} isActive={false} />
|
||||||
))}
|
))}
|
||||||
</Disclosure.Panel>
|
</Disclosure.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# @portaljs/remark-wiki-link
|
# @portaljs/remark-wiki-link
|
||||||
|
|
||||||
|
## 1.2.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#1084](https://github.com/datopian/datahub/pull/1084) [`57952e08`](https://github.com/datopian/datahub/commit/57952e0817770138881e7492dc9f43e9910b56a8) Thanks [@mohamedsalem401](https://github.com/mohamedsalem401)! - Add image resize feature
|
||||||
|
|
||||||
## 1.1.2
|
## 1.1.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@portaljs/remark-wiki-link",
|
"name": "@portaljs/remark-wiki-link",
|
||||||
"version": "1.1.2",
|
"version": "1.2.0",
|
||||||
"description": "Parse and render wiki-style links in markdown especially Obsidian style links.",
|
"description": "Parse and render wiki-style links in markdown especially Obsidian style links.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { isSupportedFileFormat } from "./isSupportedFileFormat";
|
import { isSupportedFileFormat } from './isSupportedFileFormat';
|
||||||
|
|
||||||
const defaultWikiLinkResolver = (target: string) => {
|
const defaultWikiLinkResolver = (target: string) => {
|
||||||
// for [[#heading]] links
|
// for [[#heading]] links
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
let permalink = target.replace(/\/index$/, "");
|
let permalink = target.replace(/\/index$/, '');
|
||||||
// TODO what to do with [[index]] link?
|
// TODO what to do with [[index]] link?
|
||||||
if (permalink.length === 0) {
|
if (permalink.length === 0) {
|
||||||
permalink = "/";
|
permalink = '/';
|
||||||
}
|
}
|
||||||
return [permalink];
|
return [permalink];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface FromMarkdownOptions {
|
export interface FromMarkdownOptions {
|
||||||
pathFormat?:
|
pathFormat?:
|
||||||
| "raw" // default; use for regular relative or absolute paths
|
| 'raw' // default; use for regular relative or absolute paths
|
||||||
| "obsidian-absolute" // use for Obsidian-style absolute paths (with no leading slash)
|
| 'obsidian-absolute' // use for Obsidian-style absolute paths (with no leading slash)
|
||||||
| "obsidian-short"; // use for Obsidian-style shortened paths (shortest path possible)
|
| 'obsidian-short'; // use for Obsidian-style shortened paths (shortest path possible)
|
||||||
permalinks?: string[]; // list of permalinks to match possible permalinks of a wiki link against
|
permalinks?: string[]; // list of permalinks to match possible permalinks of a wiki link against
|
||||||
wikiLinkResolver?: (name: string) => string[]; // function to resolve wiki links to an array of possible permalinks
|
wikiLinkResolver?: (name: string) => string[]; // function to resolve wiki links to an array of possible permalinks
|
||||||
newClassName?: string; // class name to add to links that don't have a matching permalink
|
newClassName?: string; // class name to add to links that don't have a matching permalink
|
||||||
@@ -25,14 +25,23 @@ export interface FromMarkdownOptions {
|
|||||||
hrefTemplate?: (permalink: string) => string; // function to generate the href attribute of a link
|
hrefTemplate?: (permalink: string) => string; // function to generate the href attribute of a link
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getImageSize(size: string) {
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
let [width, height] = size.split('x');
|
||||||
|
|
||||||
|
if (!height) height = width;
|
||||||
|
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
|
||||||
// mdas-util-from-markdown extension
|
// mdas-util-from-markdown extension
|
||||||
// https://github.com/syntax-tree/mdast-util-from-markdown#extension
|
// https://github.com/syntax-tree/mdast-util-from-markdown#extension
|
||||||
function fromMarkdown(opts: FromMarkdownOptions = {}) {
|
function fromMarkdown(opts: FromMarkdownOptions = {}) {
|
||||||
const pathFormat = opts.pathFormat || "raw";
|
const pathFormat = opts.pathFormat || 'raw';
|
||||||
const permalinks = opts.permalinks || [];
|
const permalinks = opts.permalinks || [];
|
||||||
const wikiLinkResolver = opts.wikiLinkResolver || defaultWikiLinkResolver;
|
const wikiLinkResolver = opts.wikiLinkResolver || defaultWikiLinkResolver;
|
||||||
const newClassName = opts.newClassName || "new";
|
const newClassName = opts.newClassName || 'new';
|
||||||
const wikiLinkClassName = opts.wikiLinkClassName || "internal";
|
const wikiLinkClassName = opts.wikiLinkClassName || 'internal';
|
||||||
const defaultHrefTemplate = (permalink: string) => permalink;
|
const defaultHrefTemplate = (permalink: string) => permalink;
|
||||||
|
|
||||||
const hrefTemplate = opts.hrefTemplate || defaultHrefTemplate;
|
const hrefTemplate = opts.hrefTemplate || defaultHrefTemplate;
|
||||||
@@ -44,9 +53,9 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
|
|||||||
function enterWikiLink(token) {
|
function enterWikiLink(token) {
|
||||||
this.enter(
|
this.enter(
|
||||||
{
|
{
|
||||||
type: "wikiLink",
|
type: 'wikiLink',
|
||||||
data: {
|
data: {
|
||||||
isEmbed: token.isType === "embed",
|
isEmbed: token.isType === 'embed',
|
||||||
target: null, // the target of the link, e.g. "Foo Bar#Heading" in "[[Foo Bar#Heading]]"
|
target: null, // the target of the link, e.g. "Foo Bar#Heading" in "[[Foo Bar#Heading]]"
|
||||||
alias: null, // the alias of the link, e.g. "Foo" in "[[Foo Bar|Foo]]"
|
alias: null, // the alias of the link, e.g. "Foo" in "[[Foo Bar|Foo]]"
|
||||||
permalink: null, // TODO shouldn't this be named just "link"?
|
permalink: null, // TODO shouldn't this be named just "link"?
|
||||||
@@ -80,18 +89,18 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
|
|||||||
} = wikiLink;
|
} = wikiLink;
|
||||||
// eslint-disable-next-line no-useless-escape
|
// eslint-disable-next-line no-useless-escape
|
||||||
const wikiLinkWithHeadingPattern = /^(.*?)(#.*)?$/u;
|
const wikiLinkWithHeadingPattern = /^(.*?)(#.*)?$/u;
|
||||||
const [, path, heading = ""] = target.match(wikiLinkWithHeadingPattern);
|
const [, path, heading = ''] = target.match(wikiLinkWithHeadingPattern);
|
||||||
|
|
||||||
const possibleWikiLinkPermalinks = wikiLinkResolver(path);
|
const possibleWikiLinkPermalinks = wikiLinkResolver(path);
|
||||||
|
|
||||||
const matchingPermalink = permalinks.find((e) => {
|
const matchingPermalink = permalinks.find((e) => {
|
||||||
return possibleWikiLinkPermalinks.find((p) => {
|
return possibleWikiLinkPermalinks.find((p) => {
|
||||||
if (pathFormat === "obsidian-short") {
|
if (pathFormat === 'obsidian-short') {
|
||||||
if (e === p || e.endsWith(p)) {
|
if (e === p || e.endsWith(p)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else if (pathFormat === "obsidian-absolute") {
|
} else if (pathFormat === 'obsidian-absolute') {
|
||||||
if (e === "/" + p) {
|
if (e === '/' + p) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -106,20 +115,19 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
|
|||||||
// TODO this is ugly
|
// TODO this is ugly
|
||||||
const link =
|
const link =
|
||||||
matchingPermalink ||
|
matchingPermalink ||
|
||||||
(pathFormat === "obsidian-absolute"
|
(pathFormat === 'obsidian-absolute'
|
||||||
? "/" + possibleWikiLinkPermalinks[0]
|
? '/' + possibleWikiLinkPermalinks[0]
|
||||||
: possibleWikiLinkPermalinks[0]) ||
|
: possibleWikiLinkPermalinks[0]) ||
|
||||||
"";
|
'';
|
||||||
|
|
||||||
wikiLink.data.exists = !!matchingPermalink;
|
wikiLink.data.exists = !!matchingPermalink;
|
||||||
wikiLink.data.permalink = link;
|
wikiLink.data.permalink = link;
|
||||||
|
|
||||||
// remove leading # if the target is a heading on the same page
|
// remove leading # if the target is a heading on the same page
|
||||||
const displayName = alias || target.replace(/^#/, "");
|
const displayName = alias || target.replace(/^#/, '');
|
||||||
const headingId = heading.replace(/\s+/g, "-").toLowerCase();
|
const headingId = heading.replace(/\s+/g, '-').toLowerCase();
|
||||||
let classNames = wikiLinkClassName;
|
let classNames = wikiLinkClassName;
|
||||||
if (!matchingPermalink) {
|
if (!matchingPermalink) {
|
||||||
classNames += " " + newClassName;
|
classNames += ' ' + newClassName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEmbed) {
|
if (isEmbed) {
|
||||||
@@ -127,44 +135,55 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
|
|||||||
if (!isSupportedFormat) {
|
if (!isSupportedFormat) {
|
||||||
// Temporarily render note transclusion as a regular wiki link
|
// Temporarily render note transclusion as a regular wiki link
|
||||||
if (!format) {
|
if (!format) {
|
||||||
wikiLink.data.hName = "a";
|
wikiLink.data.hName = 'a';
|
||||||
wikiLink.data.hProperties = {
|
wikiLink.data.hProperties = {
|
||||||
className: classNames + " " + "transclusion",
|
className: classNames + ' ' + 'transclusion',
|
||||||
href: hrefTemplate(link) + headingId,
|
href: hrefTemplate(link) + headingId,
|
||||||
};
|
};
|
||||||
wikiLink.data.hChildren = [{ type: "text", value: displayName }];
|
wikiLink.data.hChildren = [{ type: 'text', value: displayName }];
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
wikiLink.data.hName = "p";
|
wikiLink.data.hName = 'p';
|
||||||
wikiLink.data.hChildren = [
|
wikiLink.data.hChildren = [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: 'text',
|
||||||
value: `![[${target}]]`,
|
value: `![[${target}]]`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
} else if (format === "pdf") {
|
} else if (format === 'pdf') {
|
||||||
wikiLink.data.hName = "iframe";
|
wikiLink.data.hName = 'iframe';
|
||||||
wikiLink.data.hProperties = {
|
wikiLink.data.hProperties = {
|
||||||
className: classNames,
|
className: classNames,
|
||||||
width: "100%",
|
width: '100%',
|
||||||
src: `${hrefTemplate(link)}#toolbar=0`,
|
src: `${hrefTemplate(link)}#toolbar=0`,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
wikiLink.data.hName = "img";
|
const hasDimensions = alias && /^\d+(x\d+)?$/.test(alias);
|
||||||
wikiLink.data.hProperties = {
|
// Take the target as alt text except if alt name was provided [[target|alt text]]
|
||||||
className: classNames,
|
const altText = hasDimensions || !alias ? target : alias;
|
||||||
src: hrefTemplate(link),
|
|
||||||
alt: displayName,
|
wikiLink.data.hName = 'img';
|
||||||
};
|
wikiLink.data.hProperties = {
|
||||||
|
className: classNames,
|
||||||
|
src: hrefTemplate(link),
|
||||||
|
alt: altText
|
||||||
|
};
|
||||||
|
|
||||||
|
if (hasDimensions) {
|
||||||
|
const { width, height } = getImageSize(alias as string);
|
||||||
|
Object.assign(wikiLink.data.hProperties, {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
wikiLink.data.hName = "a";
|
wikiLink.data.hName = 'a';
|
||||||
wikiLink.data.hProperties = {
|
wikiLink.data.hProperties = {
|
||||||
className: classNames,
|
className: classNames,
|
||||||
href: hrefTemplate(link) + headingId,
|
href: hrefTemplate(link) + headingId,
|
||||||
};
|
};
|
||||||
wikiLink.data.hChildren = [{ type: "text", value: displayName }];
|
wikiLink.data.hChildren = [{ type: 'text', value: displayName }];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
import { isSupportedFileFormat } from "./isSupportedFileFormat";
|
import { getImageSize } from './fromMarkdown';
|
||||||
|
import { isSupportedFileFormat } from './isSupportedFileFormat';
|
||||||
|
|
||||||
const defaultWikiLinkResolver = (target: string) => {
|
const defaultWikiLinkResolver = (target: string) => {
|
||||||
// for [[#heading]] links
|
// for [[#heading]] links
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
let permalink = target.replace(/\/index$/, "");
|
let permalink = target.replace(/\/index$/, '');
|
||||||
// TODO what to do with [[index]] link?
|
// TODO what to do with [[index]] link?
|
||||||
if (permalink.length === 0) {
|
if (permalink.length === 0) {
|
||||||
permalink = "/";
|
permalink = '/';
|
||||||
}
|
}
|
||||||
return [permalink];
|
return [permalink];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface HtmlOptions {
|
export interface HtmlOptions {
|
||||||
pathFormat?:
|
pathFormat?:
|
||||||
| "raw" // default; use for regular relative or absolute paths
|
| 'raw' // default; use for regular relative or absolute paths
|
||||||
| "obsidian-absolute" // use for Obsidian-style absolute paths (with no leading slash)
|
| 'obsidian-absolute' // use for Obsidian-style absolute paths (with no leading slash)
|
||||||
| "obsidian-short"; // use for Obsidian-style shortened paths (shortest path possible)
|
| 'obsidian-short'; // use for Obsidian-style shortened paths (shortest path possible)
|
||||||
permalinks?: string[]; // list of permalinks to match possible permalinks of a wiki link against
|
permalinks?: string[]; // list of permalinks to match possible permalinks of a wiki link against
|
||||||
wikiLinkResolver?: (name: string) => string[]; // function to resolve wiki links to an array of possible permalinks
|
wikiLinkResolver?: (name: string) => string[]; // function to resolve wiki links to an array of possible permalinks
|
||||||
newClassName?: string; // class name to add to links that don't have a matching permalink
|
newClassName?: string; // class name to add to links that don't have a matching permalink
|
||||||
@@ -28,11 +29,11 @@ export interface HtmlOptions {
|
|||||||
// Micromark HtmlExtension
|
// Micromark HtmlExtension
|
||||||
// https://github.com/micromark/micromark#htmlextension
|
// https://github.com/micromark/micromark#htmlextension
|
||||||
function html(opts: HtmlOptions = {}) {
|
function html(opts: HtmlOptions = {}) {
|
||||||
const pathFormat = opts.pathFormat || "raw";
|
const pathFormat = opts.pathFormat || 'raw';
|
||||||
const permalinks = opts.permalinks || [];
|
const permalinks = opts.permalinks || [];
|
||||||
const wikiLinkResolver = opts.wikiLinkResolver || defaultWikiLinkResolver;
|
const wikiLinkResolver = opts.wikiLinkResolver || defaultWikiLinkResolver;
|
||||||
const newClassName = opts.newClassName || "new";
|
const newClassName = opts.newClassName || 'new';
|
||||||
const wikiLinkClassName = opts.wikiLinkClassName || "internal";
|
const wikiLinkClassName = opts.wikiLinkClassName || 'internal';
|
||||||
const defaultHrefTemplate = (permalink: string) => permalink;
|
const defaultHrefTemplate = (permalink: string) => permalink;
|
||||||
const hrefTemplate = opts.hrefTemplate || defaultHrefTemplate;
|
const hrefTemplate = opts.hrefTemplate || defaultHrefTemplate;
|
||||||
|
|
||||||
@@ -41,21 +42,21 @@ function html(opts: HtmlOptions = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function enterWikiLink() {
|
function enterWikiLink() {
|
||||||
let stack = this.getData("wikiLinkStack");
|
let stack = this.getData('wikiLinkStack');
|
||||||
if (!stack) this.setData("wikiLinkStack", (stack = []));
|
if (!stack) this.setData('wikiLinkStack', (stack = []));
|
||||||
|
|
||||||
stack.push({});
|
stack.push({});
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitWikiLinkTarget(token) {
|
function exitWikiLinkTarget(token) {
|
||||||
const target = this.sliceSerialize(token);
|
const target = this.sliceSerialize(token);
|
||||||
const current = top(this.getData("wikiLinkStack"));
|
const current = top(this.getData('wikiLinkStack'));
|
||||||
current.target = target;
|
current.target = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitWikiLinkAlias(token) {
|
function exitWikiLinkAlias(token) {
|
||||||
const alias = this.sliceSerialize(token);
|
const alias = this.sliceSerialize(token);
|
||||||
const current = top(this.getData("wikiLinkStack"));
|
const current = top(this.getData('wikiLinkStack'));
|
||||||
current.alias = alias;
|
current.alias = alias;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +112,9 @@ function html(opts: HtmlOptions = {}) {
|
|||||||
// Temporarily render note transclusion as a regular wiki link
|
// Temporarily render note transclusion as a regular wiki link
|
||||||
if (!format) {
|
if (!format) {
|
||||||
this.tag(
|
this.tag(
|
||||||
`<a href="${hrefTemplate(link + headingId)}" class="${classNames} transclusion">`
|
`<a href="${hrefTemplate(
|
||||||
|
link + headingId
|
||||||
|
)}" class="${classNames} transclusion">`
|
||||||
);
|
);
|
||||||
this.raw(displayName);
|
this.raw(displayName);
|
||||||
this.tag("</a>");
|
this.tag("</a>");
|
||||||
@@ -125,11 +128,18 @@ function html(opts: HtmlOptions = {}) {
|
|||||||
)}#toolbar=0" class="${classNames}" />`
|
)}#toolbar=0" class="${classNames}" />`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.tag(
|
const hasDimensions = alias && /^\d+(x\d+)?$/.test(alias);
|
||||||
`<img src="${hrefTemplate(
|
// Take the target as alt text except if alt name was provided [[target|alt text]]
|
||||||
link
|
const altText = hasDimensions || !alias ? target : alias;
|
||||||
)}" alt="${displayName}" class="${classNames}" />`
|
let imgAttributes = `src="${hrefTemplate(
|
||||||
);
|
link
|
||||||
|
)}" alt="${altText}" class="${classNames}"`;
|
||||||
|
|
||||||
|
if (hasDimensions) {
|
||||||
|
const { width, height } = getImageSize(alias as string);
|
||||||
|
imgAttributes += ` width="${width}" height="${height}"`;
|
||||||
|
}
|
||||||
|
this.tag(`<img ${imgAttributes} />`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.tag(
|
this.tag(
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ describe("micromark-extension-wiki-link", () => {
|
|||||||
html({
|
html({
|
||||||
permalinks: ["/some/folder/Wiki Link"],
|
permalinks: ["/some/folder/Wiki Link"],
|
||||||
pathFormat: "obsidian-short",
|
pathFormat: "obsidian-short",
|
||||||
}) as any // TODO type fix
|
}) as any, // TODO type fix
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
expect(serialized).toBe(
|
expect(serialized).toBe(
|
||||||
@@ -75,7 +75,7 @@ describe("micromark-extension-wiki-link", () => {
|
|||||||
html({
|
html({
|
||||||
permalinks: ["/some/folder/Wiki Link"],
|
permalinks: ["/some/folder/Wiki Link"],
|
||||||
pathFormat: "obsidian-absolute",
|
pathFormat: "obsidian-absolute",
|
||||||
}) as any // TODO type fix
|
}) as any, // TODO type fix
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
expect(serialized).toBe(
|
expect(serialized).toBe(
|
||||||
@@ -97,10 +97,14 @@ describe("micromark-extension-wiki-link", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("parses a wiki link with heading and alias", () => {
|
test("parses a wiki link with heading and alias", () => {
|
||||||
const serialized = micromark("[[Wiki Link#Some Heading|Alias]]", "ascii", {
|
const serialized = micromark(
|
||||||
extensions: [syntax()],
|
"[[Wiki Link#Some Heading|Alias]]",
|
||||||
htmlExtensions: [html() as any], // TODO type fix
|
"ascii",
|
||||||
});
|
{
|
||||||
|
extensions: [syntax()],
|
||||||
|
htmlExtensions: [html() as any], // TODO type fix
|
||||||
|
}
|
||||||
|
);
|
||||||
// note: lowercased and hyphenated heading
|
// note: lowercased and hyphenated heading
|
||||||
expect(serialized).toBe(
|
expect(serialized).toBe(
|
||||||
'<p><a href="Wiki Link#some-heading" class="internal new">Alias</a></p>'
|
'<p><a href="Wiki Link#some-heading" class="internal new">Alias</a></p>'
|
||||||
@@ -134,7 +138,7 @@ describe("micromark-extension-wiki-link", () => {
|
|||||||
extensions: [syntax()],
|
extensions: [syntax()],
|
||||||
htmlExtensions: [html() as any], // TODO type fix
|
htmlExtensions: [html() as any], // TODO type fix
|
||||||
});
|
});
|
||||||
expect(serialized).toBe("<p>![[My Image.xyz]]</p>");
|
expect(serialized).toBe('<p>![[My Image.xyz]]</p>');
|
||||||
});
|
});
|
||||||
|
|
||||||
test("parses and image ambed with a matching permalink", () => {
|
test("parses and image ambed with a matching permalink", () => {
|
||||||
@@ -147,6 +151,28 @@ describe("micromark-extension-wiki-link", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Fix alt attribute
|
||||||
|
test("Can identify the dimensions of the image if exists", () => {
|
||||||
|
const serialized = micromark("![[My Image.jpg|200]]", "ascii", {
|
||||||
|
extensions: [syntax()],
|
||||||
|
htmlExtensions: [html({ permalinks: ["My Image.jpg"] }) as any], // TODO type fix
|
||||||
|
});
|
||||||
|
expect(serialized).toBe(
|
||||||
|
'<p><img src="My Image.jpg" alt="My Image.jpg" class="internal" width="200" height="200" /></p>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Fix alt attribute
|
||||||
|
test("Can identify the dimensions of the image if exists", () => {
|
||||||
|
const serialized = micromark("![[My Image.jpg|200x200]]", "ascii", {
|
||||||
|
extensions: [syntax()],
|
||||||
|
htmlExtensions: [html({ permalinks: ["My Image.jpg"] }) as any], // TODO type fix
|
||||||
|
});
|
||||||
|
expect(serialized).toBe(
|
||||||
|
'<p><img src="My Image.jpg" alt="My Image.jpg" class="internal" width="200" height="200" /></p>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("parses an image embed with a matching permalink and Obsidian-style shortedned path", () => {
|
test("parses an image embed with a matching permalink and Obsidian-style shortedned path", () => {
|
||||||
const serialized = micromark("![[My Image.jpg]]", {
|
const serialized = micromark("![[My Image.jpg]]", {
|
||||||
extensions: [syntax()],
|
extensions: [syntax()],
|
||||||
@@ -154,7 +180,7 @@ describe("micromark-extension-wiki-link", () => {
|
|||||||
html({
|
html({
|
||||||
permalinks: ["/assets/My Image.jpg"],
|
permalinks: ["/assets/My Image.jpg"],
|
||||||
pathFormat: "obsidian-short",
|
pathFormat: "obsidian-short",
|
||||||
}) as any // TODO type fix
|
}) as any, // TODO type fix
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
expect(serialized).toBe(
|
expect(serialized).toBe(
|
||||||
@@ -189,7 +215,7 @@ describe("micromark-extension-wiki-link", () => {
|
|||||||
extensions: [syntax()],
|
extensions: [syntax()],
|
||||||
htmlExtensions: [html() as any], // TODO type fix
|
htmlExtensions: [html() as any], // TODO type fix
|
||||||
});
|
});
|
||||||
expect(serialized).toBe("<p>[[Wiki Link</p>");
|
expect(serialized).toBe('<p>[[Wiki Link</p>');
|
||||||
});
|
});
|
||||||
|
|
||||||
test("doesn't parse a wiki link with one missing closing bracket", () => {
|
test("doesn't parse a wiki link with one missing closing bracket", () => {
|
||||||
@@ -197,7 +223,7 @@ describe("micromark-extension-wiki-link", () => {
|
|||||||
extensions: [syntax()],
|
extensions: [syntax()],
|
||||||
htmlExtensions: [html() as any], // TODO type fix
|
htmlExtensions: [html() as any], // TODO type fix
|
||||||
});
|
});
|
||||||
expect(serialized).toBe("<p>[[Wiki Link]</p>");
|
expect(serialized).toBe('<p>[[Wiki Link]</p>');
|
||||||
});
|
});
|
||||||
|
|
||||||
test("doesn't parse a wiki link with a missing opening bracket", () => {
|
test("doesn't parse a wiki link with a missing opening bracket", () => {
|
||||||
@@ -205,7 +231,7 @@ describe("micromark-extension-wiki-link", () => {
|
|||||||
extensions: [syntax()],
|
extensions: [syntax()],
|
||||||
htmlExtensions: [html() as any], // TODO type fix
|
htmlExtensions: [html() as any], // TODO type fix
|
||||||
});
|
});
|
||||||
expect(serialized).toBe("<p>[Wiki Link]]</p>");
|
expect(serialized).toBe('<p>[Wiki Link]]</p>');
|
||||||
});
|
});
|
||||||
|
|
||||||
test("doesn't parse a wiki link in single brackets", () => {
|
test("doesn't parse a wiki link in single brackets", () => {
|
||||||
@@ -213,7 +239,7 @@ describe("micromark-extension-wiki-link", () => {
|
|||||||
extensions: [syntax()],
|
extensions: [syntax()],
|
||||||
htmlExtensions: [html() as any], // TODO type fix
|
htmlExtensions: [html() as any], // TODO type fix
|
||||||
});
|
});
|
||||||
expect(serialized).toBe("<p>[Wiki Link]</p>");
|
expect(serialized).toBe('<p>[Wiki Link]</p>');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -225,7 +251,7 @@ describe("micromark-extension-wiki-link", () => {
|
|||||||
html({
|
html({
|
||||||
newClassName: "test-new",
|
newClassName: "test-new",
|
||||||
wikiLinkClassName: "test-wiki-link",
|
wikiLinkClassName: "test-wiki-link",
|
||||||
}) as any // TODO type fix
|
}) as any, // TODO type fix
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
expect(serialized).toBe(
|
expect(serialized).toBe(
|
||||||
@@ -251,7 +277,7 @@ describe("micromark-extension-wiki-link", () => {
|
|||||||
wikiLinkResolver: (page) => [
|
wikiLinkResolver: (page) => [
|
||||||
page.replace(/\s+/, "-").toLowerCase(),
|
page.replace(/\s+/, "-").toLowerCase(),
|
||||||
],
|
],
|
||||||
}) as any // TODO type fix
|
}) as any, // TODO type fix
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
expect(serialized).toBe(
|
expect(serialized).toBe(
|
||||||
@@ -330,5 +356,5 @@ describe("micromark-extension-wiki-link", () => {
|
|||||||
});
|
});
|
||||||
expect(serialized).toBe(`<p><a href="li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#li-nk-w(i)th-àcèô-íã_a(n)d_underline!:ª%@'*º$-°~./\\" class="internal new">li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#LI NK-W(i)th-àcèô íã_a(n)d_uNdErlinE!:ª%@'*º$ °~./\\</a></p>`);
|
expect(serialized).toBe(`<p><a href="li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#li-nk-w(i)th-àcèô-íã_a(n)d_underline!:ª%@'*º$-°~./\\" class="internal new">li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#LI NK-W(i)th-àcèô íã_a(n)d_uNdErlinE!:ª%@'*º$ °~./\\</a></p>`);
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -246,6 +246,28 @@ describe("remark-wiki-link", () => {
|
|||||||
expect(node.data?.hName).toEqual("img");
|
expect(node.data?.hName).toEqual("img");
|
||||||
expect((node.data?.hProperties as any).src).toEqual("My Image.png");
|
expect((node.data?.hProperties as any).src).toEqual("My Image.png");
|
||||||
expect((node.data?.hProperties as any).alt).toEqual("My Image.png");
|
expect((node.data?.hProperties as any).alt).toEqual("My Image.png");
|
||||||
|
expect((node.data?.hProperties as any).width).toBeUndefined();
|
||||||
|
expect((node.data?.hProperties as any).height).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Can identify the dimensions of the image if exists", () => {
|
||||||
|
const processor = unified().use(markdown).use(wikiLinkPlugin);
|
||||||
|
|
||||||
|
let ast = processor.parse("![[My Image.png|132x612]]");
|
||||||
|
ast = processor.runSync(ast);
|
||||||
|
|
||||||
|
expect(select("wikiLink", ast)).not.toEqual(null);
|
||||||
|
|
||||||
|
visit(ast, "wikiLink", (node: Node) => {
|
||||||
|
expect(node.data?.isEmbed).toEqual(true);
|
||||||
|
expect(node.data?.target).toEqual("My Image.png");
|
||||||
|
expect(node.data?.permalink).toEqual("My Image.png");
|
||||||
|
expect(node.data?.hName).toEqual("img");
|
||||||
|
expect((node.data?.hProperties as any).src).toEqual("My Image.png");
|
||||||
|
expect((node.data?.hProperties as any).alt).toEqual("My Image.png");
|
||||||
|
expect((node.data?.hProperties as any).width).toBe("132");
|
||||||
|
expect((node.data?.hProperties as any).height).toBe("612");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -365,13 +387,17 @@ describe("remark-wiki-link", () => {
|
|||||||
test("parses a link with special characters and symbols", () => {
|
test("parses a link with special characters and symbols", () => {
|
||||||
const processor = unified().use(markdown).use(wikiLinkPlugin);
|
const processor = unified().use(markdown).use(wikiLinkPlugin);
|
||||||
|
|
||||||
let ast = processor.parse("[[li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#li-nk-w(i)th-àcèô íã_a(n)D_UNDERLINE!:ª%@'*º$ °~./\\]]");
|
let ast = processor.parse(
|
||||||
|
"[[li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#li-nk-w(i)th-àcèô íã_a(n)D_UNDERLINE!:ª%@'*º$ °~./\\]]"
|
||||||
|
);
|
||||||
ast = processor.runSync(ast);
|
ast = processor.runSync(ast);
|
||||||
expect(select("wikiLink", ast)).not.toEqual(null);
|
expect(select("wikiLink", ast)).not.toEqual(null);
|
||||||
|
|
||||||
visit(ast, "wikiLink", (node: Node) => {
|
visit(ast, "wikiLink", (node: Node) => {
|
||||||
expect(node.data?.exists).toEqual(false);
|
expect(node.data?.exists).toEqual(false);
|
||||||
expect(node.data?.permalink).toEqual("li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\");
|
expect(node.data?.permalink).toEqual(
|
||||||
|
"li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\"
|
||||||
|
);
|
||||||
expect(node.data?.alias).toEqual(null);
|
expect(node.data?.alias).toEqual(null);
|
||||||
expect(node.data?.hName).toEqual("a");
|
expect(node.data?.hName).toEqual("a");
|
||||||
expect((node.data?.hProperties as any).className).toEqual(
|
expect((node.data?.hProperties as any).className).toEqual(
|
||||||
@@ -383,9 +409,9 @@ describe("remark-wiki-link", () => {
|
|||||||
expect((node.data?.hChildren as any)[0].value).toEqual(
|
expect((node.data?.hChildren as any)[0].value).toEqual(
|
||||||
"li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#li-nk-w(i)th-àcèô íã_a(n)D_UNDERLINE!:ª%@'*º$ °~./\\"
|
"li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#li-nk-w(i)th-àcèô íã_a(n)D_UNDERLINE!:ª%@'*º$ °~./\\"
|
||||||
);
|
);
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
describe("invalid wiki links", () => {
|
describe("invalid wiki links", () => {
|
||||||
test("doesn't parse a wiki link with two missing closing brackets", () => {
|
test("doesn't parse a wiki link with two missing closing brackets", () => {
|
||||||
@@ -586,4 +612,3 @@ describe("remark-wiki-link", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
github: 'https://github.com/datopian/portaljs',
|
github: 'https://github.com/datopian/portaljs',
|
||||||
discord: 'https://discord.gg/EeyfGrGu4U',
|
discord: 'https://discord.gg/xfFDMPU9dC',
|
||||||
tableOfContents: true,
|
tableOfContents: true,
|
||||||
analytics: 'G-96GWZHMH57',
|
analytics: 'G-96GWZHMH57',
|
||||||
// editLinkShow: true,
|
// editLinkShow: true,
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../tsconfig.base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "../dist/out-tsc/tools",
|
|
||||||
"rootDir": ".",
|
|
||||||
"module": "commonjs",
|
|
||||||
"target": "es5",
|
|
||||||
"types": ["node"],
|
|
||||||
"importHelpers": false
|
|
||||||
},
|
|
||||||
"include": ["**/*.ts"]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user