diff --git a/static-site/src/components/ListResources/IndividualResource.tsx b/static-site/src/components/ListResources/IndividualResource.tsx
index 3e7d276ae..91db2798a 100644
--- a/static-site/src/components/ListResources/IndividualResource.tsx
+++ b/static-site/src/components/ListResources/IndividualResource.tsx
@@ -86,7 +86,7 @@ export function TooltipWrapper({description, children}) {
{children}
)
-}
+}
interface IconContainerProps {
Icon: IconType
@@ -144,7 +144,7 @@ export const IndividualResource = ({resource, isMobile}: IndividualResourceProps
// add history if mobile and resource has version info
let history: React.JSX.Element | null = null
- if (!isMobile && resource.updateCadence && resource.nVersions) {
+ if (!isMobile && resource.updateCadence && resource.nVersions && resource.lastUpdated) {
history = (
Last known update on ${resource.lastUpdated}` +
@@ -158,12 +158,14 @@ export const IndividualResource = ({resource, isMobile}: IndividualResourceProps
)
}
+ const description = resource.lastUpdated ? `Last known update on ${resource.lastUpdated}` : "";
+
return (
-
+ setModalResource(resource)}>
@@ -192,5 +194,5 @@ export const ResourceLinkWrapper = ({children, onShiftClick}) => {
{children}
- )
+ )
}
diff --git a/static-site/src/components/ListResources/ResourceGroup.tsx b/static-site/src/components/ListResources/ResourceGroup.tsx
index 82a244ae6..b0a801af4 100644
--- a/static-site/src/components/ListResources/ResourceGroup.tsx
+++ b/static-site/src/components/ListResources/ResourceGroup.tsx
@@ -31,7 +31,7 @@ const ResourceGroupHeader = ({group, isMobile, setCollapsed, collapsible, isColl
-
+
{group.groupUrl ? (
@@ -49,7 +49,7 @@ const ResourceGroupHeader = ({group, isMobile, setCollapsed, collapsible, isColl
(however there may have been a more recent update since we indexed the data)'}>
- {`Most recent snapshot: ${group.lastUpdated}`}
+ {group.lastUpdated && `Most recent snapshot: ${group.lastUpdated}`}
)}
@@ -164,7 +164,7 @@ const ResourceGroupContainer = styled.div`
`;
const IndividualResourceContainer = styled.div<{$maxResourceWidth: number}>`
- /* Columns are a simple CSS solution which works really well _if_ we can calculate the expected maximum
+ /* Columns are a simple CSS solution which works really well _if_ we can calculate the expected maximum
resource width */
column-width: ${(props) => props.$maxResourceWidth}px;
column-gap: 20px;
@@ -239,7 +239,7 @@ function NextstrainLogo() {
/**
* Adds the `displayName` property to each resource.
* Given successive nameParts:
- * [ seasonal-flu, h1n1pdm]
+ * [ seasonal-flu, h1n1pdm]
* [ seasonal-flu, h3n2]
* We want to produce two names: a default name, which contains all parts,
* and a displayName which hides the fields that match the preceding name.
@@ -255,7 +255,7 @@ function _setDisplayName(resources: Resource[]) {
name = r.nameParts.join(sep);
} else {
let matchIdx = r.nameParts.map((word, j) => word === resources[i-1]?.nameParts[j]).findIndex((v) => !v);
- if (matchIdx===-1) { // -1 means every word is in the preceding name, but we should display the last word anyway
+ if (matchIdx===-1) { // -1 means every word is in the preceding name, but we should display the last word anyway
matchIdx = r.nameParts.length-2;
}
name = r.nameParts.map((word, j) => j < matchIdx ? ' '.repeat(word.length) : word).join(sep);
@@ -277,4 +277,4 @@ function collapseThresolds(numGroups: number) {
resourcesToShowWhenCollapsed = 40;
}
return {collapseThreshold, resourcesToShowWhenCollapsed}
-}
\ No newline at end of file
+}
diff --git a/static-site/src/components/ListResources/index.tsx b/static-site/src/components/ListResources/index.tsx
index 2db23f7da..5632abdca 100644
--- a/static-site/src/components/ListResources/index.tsx
+++ b/static-site/src/components/ListResources/index.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useRef, useEffect, useCallback, createContext, useContext } from 'react';
+import React, { FormEvent, useState, useRef, useEffect, useCallback, createContext, useContext } from 'react';
import styled from 'styled-components';
import Select, { MultiValue } from 'react-select';
import ScrollableAnchor, { goToAnchor } from '../../../vendored/react-scrollable-anchor/index';
@@ -12,7 +12,8 @@ import { ErrorContainer } from "../../pages/404";
import { TooltipWrapper } from "./IndividualResource";
import {ResourceModal, SetModalResourceContext} from "./Modal";
import { CardImgWrapper, CardInner, CardOuter, CardTitle, Showcase } from "../Showcase";
-import { FilterCard, FilterOption, Group, GroupDisplayNames, QuickLink, Resource } from './types';
+import { FilterCard, FilterOption, Group, GroupDisplayNames, QuickLink, Resource, ResourceListingInfo } from './types';
+import { HugeSpacer } from "../../layouts/generalComponents";
interface ListResourcesProps extends ListResourcesResponsiveProps {
elWidth: number
@@ -23,23 +24,29 @@ const LIST_ANCHOR = "list";
const SetSelectedFilterOptions = createContext> | null>(null);
/**
- * A React component to fetch data and display the available resources,
- * including past versions ("snapshots").
+ * A React component that uses a callback to fetch data about
+ * available resources, and then display them, including past versions
+ * ("snapshots"), if those exist.
*
* Note that currently this only uses 'dataset' resources. In the future this
* will be expanded. Similarly, we define versioned: boolean here in the UI whereas
* this may be better expressed as a property of the API response.
*/
function ListResources({
- sourceId,
versioned=true,
elWidth,
quickLinks,
defaultGroupLinks=false,
groupDisplayNames,
showcase,
+ resourceListingCallback: resourceListingCallback,
}: ListResourcesProps) {
- const {groups, dataFetchError} = useDataFetch(sourceId, versioned, defaultGroupLinks, groupDisplayNames);
+ const {groups, dataFetchError} = useDataFetch(
+ versioned,
+ defaultGroupLinks,
+ groupDisplayNames,
+ resourceListingCallback,
+ );
const showcaseCards = useShowcaseCards(showcase, groups);
const [selectedFilterOptions, setSelectedFilterOptions] = useState([]);
const [sortMethod, changeSortMethod] = useState("alphabetical");
@@ -50,7 +57,7 @@ function ListResources({
if (dataFetchError) {
return (
-
+
{"Whoops - listing resources isn't working!"}
{'Please '}get in touch{" if this keeps happening"}
@@ -69,17 +76,25 @@ function ListResources({
return (
-
- Showcase resources: click to filter the resources to a pathogen
-
+ { showcaseCards.length > 0 && (
+ <>
+
+ Showcase resources: click to filter the resources to a pathogen
+
-
-
-
+
+
+
+ >
+ )}
-
+ { groups?.[0]?.lastUpdated && (
+
+ ) || (
+
+ )}
@@ -108,7 +123,6 @@ function ListResources({
interface ListResourcesResponsiveProps {
- sourceId: string
versioned: boolean
quickLinks: QuickLink[]
@@ -116,6 +130,7 @@ interface ListResourcesResponsiveProps {
defaultGroupLinks: boolean
groupDisplayNames: GroupDisplayNames
showcase: FilterCard[]
+ resourceListingCallback: () => Promise;
}
/**
@@ -155,12 +170,12 @@ export default ListResourcesResponsive
function SortOptions({sortMethod, changeSortMethod}) {
- function onChangeValue(event) {
- changeSortMethod(event.target.value);
+ function onChangeValue(event:FormEvent): void {
+ changeSortMethod(event.currentTarget.value);
}
return (
- Sort pathogens by:
+ Sort pathogens by:
display name
*/
diff --git a/static-site/src/components/ListResources/useDataFetch.ts b/static-site/src/components/ListResources/useDataFetch.ts
index ef68e585e..b5eccdfb3 100644
--- a/static-site/src/components/ListResources/useDataFetch.ts
+++ b/static-site/src/components/ListResources/useDataFetch.ts
@@ -1,53 +1,52 @@
import { useState, useEffect } from 'react';
-import { Group, GroupDisplayNames, PathVersions, Resource } from './types';
+import { Group, GroupDisplayNames, PathVersions, Resource, ResourceListingInfo } from './types';
/**
- * Fetches the datasets for the provided `sourceId` and parses the data into an
- * array of groups, each representing a "pathogen" and detailing the available
- * resources for each, and the available versions (snapshots) for each of those
- * resources.
+ * Uses the provided callback to fetch resources and parse those
+ * resources into the `pathVersions` and `pathPrefix` values. The
+ * callback is expected (and encouraged!) to throw() on any errors.
*
- * The current implementation defines `versioned: boolean` for the entire API
- * response, however in the future we may shift this to the API response and it
- * may vary across the resources returned.
+ * Continues on to parse the `pathVersions`/`pathPrefix` data
+ * structures into an array of groups, each representing a "pathogen"
+ * and detailing the available resources for each, and the available
+ * versions (snapshots) for each of those resources. In the case of
+ * un-versioned resources, versions will be a zero-length array (i.e.,
+ * `[]`)
+ *
+ * The current implementation defines `versioned: boolean` for the
+ * entire API response, however in the future we may shift this to the
+ * API response and it may vary across the resources returned.
*/
-export function useDataFetch(sourceId: string, versioned: boolean, defaultGroupLinks: boolean, groupDisplayNames: GroupDisplayNames) {
+export function useDataFetch(
+ versioned: boolean,
+ defaultGroupLinks: boolean,
+ groupDisplayNames: GroupDisplayNames,
+ resourceListingCallback: () => Promise,
+) : {groups: Group[] | undefined, dataFetchError: boolean} {
const [groups, setGroups] = useState();
- const [dataFetchError, setDataFetchError] = useState();
- useEffect(() => {
- const url = `/list-resources/${sourceId}`;
+ const [dataFetchError, setDataFetchError] = useState(false);
- async function fetchAndParse() {
- let pathVersions: PathVersions, pathPrefix: string;
+ useEffect((): void => {
+ async function fetchAndParse(): Promise {
try {
- const response = await fetch(url, {headers: {accept: "application/json"}});
- if (response.status !== 200) {
- console.error(`ERROR: fetching data from "${url}" returned status code ${response.status}`);
- return setDataFetchError(true);
- }
- ({ pathVersions, pathPrefix } = (await response.json()).dataset[sourceId]);
- } catch (err) {
- console.error(`Error while fetching data from "${url}"`);
- console.error(err);
- return setDataFetchError(true);
- }
+ const { pathPrefix, pathVersions } = await resourceListingCallback();
- /* group/partition the resources by pathogen (the first word of the
- resource path). This grouping is constant for all UI options so we do it a
- single time following the data fetch */
- try {
+ /* group/partition the resources by pathogen (the first word
+ of the resource path). This grouping is constant for all UI
+ options so we do it a single time following the data fetch */
const partitions = partitionByPathogen(pathVersions, pathPrefix, versioned);
setGroups(groupsFrom(partitions, pathPrefix, defaultGroupLinks, groupDisplayNames));
} catch (err) {
- console.error(`Error while parsing fetched data`);
+ console.error(`Error while fetching and/or parsing data`);
console.error(err);
return setDataFetchError(true);
}
}
fetchAndParse();
- }, [sourceId, versioned, defaultGroupLinks, groupDisplayNames]);
+ }, [versioned, defaultGroupLinks, groupDisplayNames, resourceListingCallback]);
+
return {groups, dataFetchError}
}
@@ -60,15 +59,12 @@ interface Partitions {
/**
* Groups the provided array of pathVersions into an object with keys
* representing group names (pathogen names) and values which are arrays of
- * resource objects.
+ * resource objects.
*/
function partitionByPathogen(pathVersions: PathVersions, pathPrefix: string, versioned: boolean) {
return Object.entries(pathVersions).reduce((store: Partitions, [name, dates]) => {
const sortedDates = [...dates].sort();
- // do nothing if resource has no dates
- if (sortedDates.length < 1) return store
-
const nameParts = name.split('/');
// split() will always return at least 1 string
const groupName = nameParts[0]!;
@@ -79,11 +75,10 @@ function partitionByPathogen(pathVersions: PathVersions, pathPrefix: string, ver
nameParts,
sortingName: _sortableName(nameParts),
url: `/${pathPrefix}${name}`,
- lastUpdated: sortedDates.at(-1)!,
+ lastUpdated: sortedDates.at(-1),
};
if (versioned) {
resourceDetails.firstUpdated = sortedDates[0]!;
- resourceDetails.lastUpdated = sortedDates.at(-1)!;
resourceDetails.dates = sortedDates;
resourceDetails.nVersions = sortedDates.length;
resourceDetails.updateCadence = updateCadence(sortedDates.map((date)=> new Date(date)));
@@ -187,4 +182,4 @@ function updateCadence(dateObjects) {
return {summary: "monthly", description};
}
return {summary: "rarely", description};
-}
\ No newline at end of file
+}
diff --git a/static-site/src/pages/groups.jsx b/static-site/src/pages/groups.jsx
index 1db066d54..7b3000a33 100644
--- a/static-site/src/pages/groups.jsx
+++ b/static-site/src/pages/groups.jsx
@@ -4,23 +4,44 @@ import { SmallSpacer, HugeSpacer, FlexCenter } from "../layouts/generalComponent
import * as splashStyles from "../components/splash/styles";
import { fetchAndParseJSON } from "../util/datasetsHelpers";
import DatasetSelect from "../components/Datasets/dataset-select";
+import ListResources from "../components/ListResources/index";
import { GroupCards } from "../components/splash/groupCards";
import GenericPage from "../layouts/generic-page";
import { UserContext } from "../layouts/userDataWrapper";
import { DataFetchErrorParagraph } from "../components/splash/errorMessages";
import { groupsTitle, GroupsAbstract } from "../../data/SiteConfig";
-const datasetColumns = ({isNarrative}) => [
+const resourceListingCallback = async () => {
+ const sourceUrl = "/charon/getAvailable?prefix=/groups/";
+
+ const response = await fetch(sourceUrl);
+ if (response.status !== 200) {
+ throw new Error(`fetching data from "${sourceUrl}" returned status code ${response.status}`);
+ }
+
+ const datasets = (await response.json()).datasets;
+ // Use an empty array as the value side, to indicate that there are
+ // no dated versions associated with this data
+ const pathVersions = Object.assign(
+ ...datasets.map((ds) => ({
+ [ds.request.replace(/^\/groups\//, "")]: []
+ })),
+ );
+
+ return { pathPrefix: "groups/", pathVersions };
+};
+
+const datasetColumns = [
{
- name: isNarrative ? "Narrative" : "Dataset",
+ name: "Narrative",
value: (d) => d.request.replace("/groups/", "").replace(/\//g, " / "),
- url: (d) => d.url
+ url: (d) => d.url,
},
{
name: "Group Name",
value: (d) => d.request.split("/")[2],
- url: (d) => `/groups/${d.request.split("/")[2]}`
- }
+ url: (d) => `/groups/${d.request.split("/")[2]}`,
+ },
];
const GroupListingInfo = () => {
@@ -92,31 +113,21 @@ class GroupsPage extends React.Component {
-
- Available Datasets
-
-
-
- {`Note that this listing is refreshed every ~6 hours.
- To see a current listing, visit the page for that group by clicking on that group's tile, above.`}
-
-
-
- {this.state.dataLoaded && (
-
- )}
+
+
+
Available Narratives
+
{this.state.dataLoaded && (
+ columns={datasetColumns}/>
)}
{ this.state.errorFetchingData && }
diff --git a/static-site/src/sections/pathogens.jsx b/static-site/src/sections/pathogens.jsx
index 90feae2b6..e610077a2 100644
--- a/static-site/src/sections/pathogens.jsx
+++ b/static-site/src/sections/pathogens.jsx
@@ -21,6 +21,18 @@ const abstract = (
>
);
+const resourceListingCallback = async () => {
+ const sourceId = "core"
+ const sourceUrl = `list-resources/${sourceId}`;
+
+ const response = await fetch(sourceUrl, {headers: {accept: "application/json"}});
+ if (response.status !== 200) {
+ throw new Error(`fetching data from "${sourceUrl}" returned status code ${response.status}`);
+ }
+
+ return (await response.json()).dataset[sourceId];
+};
+
class Index extends React.Component {
render() {
return (
@@ -35,9 +47,13 @@ class Index extends React.Component {
-
+ quickLinks={coreQuickLinks} defaultGroupLinks
+ groupDisplayNames={coreGroupDisplayNames}
+ resourceListingCallback={resourceListingCallback}/>
+
);
diff --git a/static-site/src/sections/staging-page.jsx b/static-site/src/sections/staging-page.jsx
index 1b87b68f4..c35915003 100644
--- a/static-site/src/sections/staging-page.jsx
+++ b/static-site/src/sections/staging-page.jsx
@@ -18,6 +18,18 @@ const abstract = (
>
);
+const resourceListingCallback = async () => {
+ const sourceId = "staging"
+ const sourceUrl = `list-resources/${sourceId}`;
+
+ const response = await fetch(sourceUrl, {headers: {accept: "application/json"}});
+ if (response.status !== 200) {
+ throw new Error(`fetching data from "${sourceUrl}" returned status code ${response.status}`);
+ }
+
+ return (await response.json()).dataset[sourceId];
+};
+
class Index extends React.Component {
constructor(props) {
super(props);
@@ -30,7 +42,7 @@ class Index extends React.Component {
We update state which results in an error banner being shown. */
if (!this.state.resourcePath && this.props.router.query?.staging) {
this.setState({resourcePath: "staging/"+this.props.router.query.staging.join("/")})
- }
+ }
}
componentDidMount() {this.checkRouterParams()}
@@ -62,7 +74,8 @@ class Index extends React.Component {
-
+