diff --git a/static-site/components/list-resources/individual-resource.tsx b/static-site/components/list-resources/individual-resource.tsx index 3894cf7f1..11d65f711 100644 --- a/static-site/components/list-resources/individual-resource.tsx +++ b/static-site/components/list-resources/individual-resource.tsx @@ -10,6 +10,7 @@ import { SetModalDataContext } from "./modal"; import TooltipWrapper from "./tooltip-wrapper"; import { Resource } from "./types"; +import { displayResourceType } from "."; import styles from "./individual-resource.module.css"; @@ -70,17 +71,17 @@ export function IndividualResource({ return null; } - const restricted = resource.restrictedDataWarning ? ( - + const restricted = resource.maybeRestricted ? ( + ) : null; // If an out of date warning exists then show it. Otherwise show cadence information if it's available let history: React.JSX.Element | null = null; - if (resource.outOfDateWarning) { + if (resource.maybeOutOfDate) { history = ( - + ); diff --git a/static-site/components/list-resources/listResourcesApi.tsx b/static-site/components/list-resources/listResourcesApi.tsx index 3e3ce7824..010a7f661 100644 --- a/static-site/components/list-resources/listResourcesApi.tsx +++ b/static-site/components/list-resources/listResourcesApi.tsx @@ -1,4 +1,4 @@ -import { ResourceType, Resource, Group, PathVersionsForGroup, FetchGroupHistory } from "./types"; +import { ResourceType, Resource, Group, PathVersionsForGroup, FetchGroupHistory, maybeRestrictedData } from "./types"; import { InternalError } from "../error-boundary"; import fetchAndParseJSON from "../../util/fetch-and-parse-json"; @@ -62,7 +62,7 @@ export async function listResourcesAPI( } const groups = await Promise.all(Object.entries( areDatasets(data) ? - groupDatasetsByPathogen(data.pathVersions, urlBuilder, versioned, groupNameBuilder) : + groupDatasetsByPathogen(data.pathVersions, urlBuilder, versioned, groupNameBuilder, resourceType) : groupIntermediatesByPathogen(data.latestVersions, groupNameBuilder) ).map(async ([groupName, resources]) => { const group = resourceGroup(groupName, resources); @@ -135,6 +135,9 @@ function groupDatasetsByPathogen( /** constructs the name (e.g. pathogen) under which to group a dataset */ groupNameBuilder: (name: string) => string, + + /** the type of resource */ + resourceType: ResourceType, ): Record { return Object.entries(pathVersions).reduce( (store: Record, [name, dates]) => { @@ -152,6 +155,7 @@ function groupDatasetsByPathogen( nameParts, sortingName: _sortableName(nameParts), url: urlBuilder(name), + resourceType, }; if (versioned) { @@ -160,6 +164,7 @@ function groupDatasetsByPathogen( throw new InternalError("Resource does not have any dates."); } const lastUpdated = sortedDates.at(-1) as string; // eslint-disable-line @typescript-eslint/consistent-type-assertions + const nDaysOld = _timeDelta(lastUpdated); resourceDetails.lastUpdated = lastUpdated; resourceDetails.firstUpdated = sortedDates[0]; resourceDetails.dates = sortedDates; @@ -167,10 +172,7 @@ function groupDatasetsByPathogen( resourceDetails.updateCadence = _updateCadence( sortedDates.map((date) => new Date(date)), ); - const nDaysOld = _timeDelta(lastUpdated); - if (nDaysOld && nDaysOld>365) { - resourceDetails.outOfDateWarning = `Warning! This dataset may be over a year old. Last known update on ${lastUpdated}`; - } + resourceDetails.maybeOutOfDate = !!nDaysOld && nDaysOld>365; } (store[groupName] ??= []).push(resourceDetails); @@ -198,22 +200,18 @@ function groupIntermediatesByPathogen( if (filename==='mostRecentlyIndexed') continue; const nameParts = [...baseParts, filename] const [url, lastUpdated] = urlDatePair; + const nDaysOld = _timeDelta(lastUpdated); const resourceDetails: Resource = { name: nameParts.join('/'), // includes filename groupName, // decoupled from nameParts nameParts, sortingName: _sortableName(nameParts), url, + resourceType: 'intermediate', lastUpdated, + maybeRestricted: maybeRestrictedData(filename), + maybeOutOfDate: !!nDaysOld && nDaysOld>365, }; - if (nameParts.at(-1)?.includes("restricted")) { - // "restricted" in filename - resourceDetails.restrictedDataWarning = "Warning! This file may contain restricted data. Please refer to Restricted Data Terms of Use linked above."; - } - const nDaysOld = _timeDelta(lastUpdated); - if (nDaysOld && nDaysOld>365) { - resourceDetails.outOfDateWarning = `Warning! This file may be over a year old. Last known update on ${lastUpdated}`; - } (store[groupName] ??= []).push(resourceDetails); } diff --git a/static-site/components/list-resources/modal-contents-group-history.tsx b/static-site/components/list-resources/modal-contents-group-history.tsx index 3909bbe96..fa1b5dc9c 100644 --- a/static-site/components/list-resources/modal-contents-group-history.tsx +++ b/static-site/components/list-resources/modal-contents-group-history.tsx @@ -1,7 +1,9 @@ "use client"; import React, { useEffect, useState} from "react"; -import { FilterOption, Group, PathVersionsForGroup, GroupFilesChangelog } from "./types"; +import { FilterOption, Group, PathVersionsForGroup, GroupFilesChangelog, maybeRestrictedData } from "./types"; +import IconContainer from "./icon-container"; +import TooltipWrapper from "./tooltip-wrapper"; import styles from "./modal.module.css"; import Spinner from "../spinner"; @@ -61,8 +63,9 @@ export function GroupHistory({ const filteredHistory = _filterHistory(history, filterWords); const nDays = filteredHistory.length; - const allFiles = filteredHistory.flatMap((h) => Object.keys(h[1])); - const nFilesUnique = (new Set(allFiles)).size; + const allFiles = filteredHistory.flatMap((h) => h[1]); + const nFilesUnique = (new Set(allFiles.map((file) => file.name))).size; + const hasRestricted = allFiles.some((file) => file.maybeRestricted); return ( <> @@ -76,6 +79,17 @@ export function GroupHistory({ {` and thus there could be more recent versions of a particular file uploaded since ${filteredHistory.at(0)?.[0]};`} {` please use the link on the background page to guarantee you're getting the latest version of a particular file.`} {` Finally there may have been files or versions beyond those shown here which are no longer available.`} + {hasRestricted && (<> +
+
+ {`Some of these files contain Restricted Data from Pathoplexus.`} +
+ {`To use these in your own analysis, please read `} + + Pathoplexus Restricted Data Terms of Use + + {`.`} + )} {filterWords.length!==0 && (
@@ -90,11 +104,16 @@ export function GroupHistory({ {date}
    - {Object.entries(info).map(([filename, url]) => { - const displayFilename = filename.split('/').join(' / '); + {info.map((file) => { + const displayFilename = file.name.split('/').join(' / '); return ( -
  • - {displayFilename} +
  • + {displayFilename} + {file.maybeRestricted && ( + + + + )}
  • ) })} @@ -112,24 +131,24 @@ function _filterHistory(history: GroupFilesChangelog, filters: string[]): GroupF // Use a cache to speed things up as the same filenames are repeated many times const cache: Map = new Map(); // true: passes filter, false: exclude return history.flatMap(([date, files]) => { // flatMap allows empty array returns to disappear - const filePairs = Object.entries(files) - .filter(([filename, ]) => { - if (!cache.has(filename)) { - const filewords = filename.split('/'); + const filtered = files + .filter((file) => { + if (!cache.has(file.name)) { + const filewords = file.name.split('/'); const passes = filters.map((w) => filewords.includes(w)).every((el) => el===true); - cache.set(filename, passes); + cache.set(file.name, passes); } - return cache.get(filename); + return cache.get(file.name); }) - if (filePairs.length===0) return []; // no matches from this day - return [[date, Object.fromEntries(filePairs)]] + if (filtered.length===0) return []; // no matches from this day + return [[date, filtered]] }) } function _changelog(pathVersions: PathVersionsForGroup): GroupFilesChangelog { const dates = _orderedDates(pathVersions); const indexes = Object.fromEntries(dates.map((d, i) => [d, i])); - const history: GroupFilesChangelog = dates.map((d) => [d, {}]); + const history: GroupFilesChangelog = dates.map((d) => [d, []]); for (const [id, infoByDay] of Object.entries(pathVersions)) { for (const info of infoByDay) { const date = info.date; @@ -139,7 +158,11 @@ function _changelog(pathVersions: PathVersionsForGroup): GroupFilesChangelog { if (dateIdx===undefined) continue; const historyEl = history[dateIdx]; if (historyEl===undefined) continue; - historyEl[1][`${id}/${filename}`] = url; + historyEl[1].push({ + name: `${id}/${filename}`, + url, + maybeRestricted: maybeRestrictedData(filename), + }); } } } diff --git a/static-site/components/list-resources/modal.module.css b/static-site/components/list-resources/modal.module.css index 9e142a742..db7ff43a7 100644 --- a/static-site/components/list-resources/modal.module.css +++ b/static-site/components/list-resources/modal.module.css @@ -64,6 +64,9 @@ } .list { + display: flex; + align-items: center; + gap: 5px; margin-left: 20px; margin-bottom: 3px; } diff --git a/static-site/components/list-resources/resource-group.tsx b/static-site/components/list-resources/resource-group.tsx index 8102d91cb..3e3f37d28 100644 --- a/static-site/components/list-resources/resource-group.tsx +++ b/static-site/components/list-resources/resource-group.tsx @@ -377,10 +377,10 @@ function _getMaxResourceWidth( (w: number, r: DisplayNamedResource): number => { /* add the pixels for the display name */ let _w = r.displayName.default.length * namePxPerChar; - if (r.restrictedDataWarning) { + if (r.maybeRestricted) { _w += 40; // icon + padding } - if (r.outOfDateWarning) { + if (r.maybeOutOfDate) { _w += 40; // icon + padding } else if (r.nVersions && r.updateCadence) { _w += gapWidth + iconWidth; diff --git a/static-site/components/list-resources/styles.module.css b/static-site/components/list-resources/styles.module.css index 228a07feb..41675dbec 100644 --- a/static-site/components/list-resources/styles.module.css +++ b/static-site/components/list-resources/styles.module.css @@ -14,6 +14,7 @@ .resourceTooltip { font-size: 1.6rem; + z-index: 200; /* higher than .modalContainer's z-index (100) */ } .sortContainer { diff --git a/static-site/components/list-resources/types.ts b/static-site/components/list-resources/types.ts index fbfc8761f..54615f7fe 100644 --- a/static-site/components/list-resources/types.ts +++ b/static-site/components/list-resources/types.ts @@ -41,9 +41,15 @@ export type PathVersionsForGroup = { }[] } +export interface ChangelogFile { + name: string; + url: string; + maybeRestricted: boolean; +} + export type GroupFilesChangelog = [ date: string, - {[nameInclFilename: string]: string} + ChangelogFile[] ][] @@ -70,19 +76,25 @@ export interface Resource { sortingName: string; url: string; - resourceType?: ResourceType; + resourceType: ResourceType; lastUpdated?: string; // date firstUpdated?: string; // date dates?: string[]; nVersions?: number; updateCadence?: UpdateCadence; - /** Warning is set if the resource potentially uses restricted data. */ - restrictedDataWarning?: string; + /** True if the resource potentially uses restricted data. */ + maybeRestricted?: boolean; - /** If the resource is (potentially) out of date (according to the `lastUpdated` property) - * the `outOfDateWarning` may be set */ - outOfDateWarning?: string; + /** True if the resource is potentially out of date (according to the `lastUpdated` property). */ + maybeOutOfDate?: boolean; +} + +/** + * Check if the filename indicates restricted data. + */ +export function maybeRestrictedData(filename: string): boolean { + return filename.includes("restricted"); } export interface DisplayNamedResource extends Resource {