diff --git a/frontend/packages/data-portal/app/components/BrowseData/DatasetTable.tsx b/frontend/packages/data-portal/app/components/BrowseData/DatasetTable.tsx index 214a3afb4..832ae24cd 100644 --- a/frontend/packages/data-portal/app/components/BrowseData/DatasetTable.tsx +++ b/frontend/packages/data-portal/app/components/BrowseData/DatasetTable.tsx @@ -12,7 +12,7 @@ import { import Paper from '@mui/material/Paper' import Skeleton from '@mui/material/Skeleton' import TableContainer from '@mui/material/TableContainer' -import { useLoaderData, useSearchParams } from '@remix-run/react' +import { useLoaderData, useLocation, useSearchParams } from '@remix-run/react' import { range } from 'lodash-es' import { ComponentProps, ReactNode } from 'react' @@ -110,6 +110,8 @@ export function DatasetTable() { ) : data.datasets + const location = useLocation() + return ( // Need to subtract 244px from 100vw to account for the sidebar and padding: // sidebar width = 200px, padding = 22px * 2 = 44px @@ -162,6 +164,11 @@ export function DatasetTable() { .map((val) => `Object ${val}`) .map((obj) =>
  • {obj}
  • ) + const previousUrl = `${location.pathname}${location.search}` + const datasetUrl = `/datasets/${ + dataset.id + }?prev=${encodeURIComponent(previousUrl)}` + return ( {/* Dataset information cell */} @@ -183,9 +190,7 @@ export function DatasetTable() { {isLoadingDebounced ? ( ) : ( - - {dataset.title} - + {dataset.title} )}

    @@ -229,7 +234,7 @@ export function DatasetTable() { {dataset.authors.length > AUTHOR_MAX && ( + {dataset.authors.length + 1 - AUTHOR_MAX} more diff --git a/frontend/packages/data-portal/app/components/Layout/Layout.tsx b/frontend/packages/data-portal/app/components/Layout/Layout.tsx index fb5187254..ca3978747 100644 --- a/frontend/packages/data-portal/app/components/Layout/Layout.tsx +++ b/frontend/packages/data-portal/app/components/Layout/Layout.tsx @@ -6,11 +6,11 @@ import { TopNavigation } from './TopNavigation' export function Layout({ children }: { children: ReactNode }) { return ( -
    + <> -
    {children}
    +
    {children}
    -
    + ) } diff --git a/frontend/packages/data-portal/app/i18n.ts b/frontend/packages/data-portal/app/i18n.ts index ab8df71d3..b21af7f7f 100644 --- a/frontend/packages/data-portal/app/i18n.ts +++ b/frontend/packages/data-portal/app/i18n.ts @@ -18,11 +18,13 @@ export const i18n = { goToDocs: 'Go to Documentation', helpAndReport: 'Help & Report', keyPhoto: 'key photo', + lastModified: (date: string) => `Last Modified: ${date}`, license: 'License', napariPlugin: 'napari Plugin', portalId: (id: number | string) => `Portal ID: ${id}`, privacy: 'Privacy', privacyPolicy: 'Privacy Policy', + releaseDate: (date: string) => `Release Date: ${date}`, reportIssueOnGithub: 'Report Issue on GitHub', runs: 'Runs', runsTab: (count: number) => `Runs ${count}`, diff --git a/frontend/packages/data-portal/app/routes/datasets.$id.tsx b/frontend/packages/data-portal/app/routes/datasets.$id.tsx new file mode 100644 index 000000000..0e9308b7d --- /dev/null +++ b/frontend/packages/data-portal/app/routes/datasets.$id.tsx @@ -0,0 +1,101 @@ +/* eslint-disable @typescript-eslint/no-throw-literal */ + +import { Icon } from '@czi-sds/components' +import { useLoaderData, useSearchParams } from '@remix-run/react' +import { json, LoaderFunctionArgs } from '@remix-run/server-runtime' + +import { gql } from 'app/__generated__' +import { GetDatasetByIdQuery } from 'app/__generated__/graphql' +import { apolloClient } from 'app/apollo.server' +import { Demo } from 'app/components/Demo' +import { Link } from 'app/components/Link' +import { i18n } from 'app/i18n' +import { cns } from 'app/utils/cns' + +const GET_DATASET_BY_ID = gql(` + query GetDatasetById($id: Int) { + datasets(where: { id: { _eq: $id } }) { + id + deposition_date + last_modified_date + release_date + } + } +`) + +export async function loader({ params }: LoaderFunctionArgs) { + const id = params.id ? +params.id : NaN + + if (Number.isNaN(+id)) { + throw new Response(null, { + status: 400, + statusText: 'ID is not defined', + }) + } + + const { data } = await apolloClient.query({ + query: GET_DATASET_BY_ID, + variables: { + id: +id, + }, + }) + + if (data.datasets.length === 0) { + throw new Response(null, { + status: 404, + statusText: `Dataset with ID ${id} not found`, + }) + } + + return json(data) +} + +export default function DatasetByIdPage() { + const [params] = useSearchParams() + const previousUrl = params.get('prev') + + const { + datasets: [dataset], + } = useLoaderData() + + return ( + <> +
    +
    + {previousUrl && ( + + + + Back to results + + + )} + +
    +

    {i18n.releaseDate(dataset.release_date)}

    +
    +

    + {i18n.lastModified( + dataset.last_modified_date ?? dataset.deposition_date, + )} +

    +
    +
    +
    + + Dataset {dataset.id} + + ) +} diff --git a/frontend/packages/data-portal/app/utils/url.test.ts b/frontend/packages/data-portal/app/utils/url.test.ts index e481ca230..ee7b7c4d4 100644 --- a/frontend/packages/data-portal/app/utils/url.test.ts +++ b/frontend/packages/data-portal/app/utils/url.test.ts @@ -1,4 +1,4 @@ -import { isExternalUrl } from './url' +import { createUrl, isExternalUrl } from './url' describe('utils/url', () => { describe('isExternalUrl()', () => { @@ -10,4 +10,26 @@ describe('utils/url', () => { expect(isExternalUrl('/faq')).toBe(false) }) }) + + describe('createUrl()', () => { + it('should create url object without host', () => { + const url = createUrl('/path') + expect(url.pathname).toBe('/path') + expect(url.host).toBe('tmp.com') + }) + + it('should create url object with host', () => { + const testCases: [string, string?][] = [ + ['https://example.com/path'], + ['/path', 'http://example.com'], + ] + + for (const testCase of testCases) { + const url = createUrl(...testCase) + + expect(url.pathname).toBe('/path') + expect(url.host).toBe('example.com') + } + }) + }) }) diff --git a/frontend/packages/data-portal/app/utils/url.ts b/frontend/packages/data-portal/app/utils/url.ts index 6f97d7e4b..f63fbb66c 100644 --- a/frontend/packages/data-portal/app/utils/url.ts +++ b/frontend/packages/data-portal/app/utils/url.ts @@ -13,3 +13,23 @@ export function isExternalUrl(url: string): boolean { return false } } +/** + * Wrapper over the URL constructor with additional functionality. URLs that + * cannot be constructor without a base will automatically have the base + * `http://tmp.com` added to the URL. This is to ensure URLs can be created from + * paths and full URLs, and for use cases where operating on the URL pathname + * matter more than the actual host. + * + * @param urlOrPath URL or path string. + * @param baseUrl URL to use for final URL. + * @returns The combined URL object. + */ +export function createUrl(urlOrPath: string, baseUrl?: string): URL { + let base = baseUrl + + if (!base && !isExternalUrl(urlOrPath)) { + base = 'http://tmp.com' + } + + return new URL(urlOrPath, base) +} diff --git a/frontend/packages/data-portal/codegen.ts b/frontend/packages/data-portal/codegen.ts index e1365fbf6..e9cd0542b 100644 --- a/frontend/packages/data-portal/codegen.ts +++ b/frontend/packages/data-portal/codegen.ts @@ -11,9 +11,16 @@ const config: CodegenConfig = { './app/__generated__/': { preset: 'client', plugins: [], + presetConfig: { gqlTagName: 'gql', }, + + config: { + scalars: { + date: 'string', + }, + }, }, }, ignoreNoDocuments: true, diff --git a/frontend/packages/data-portal/tailwind.config.ts b/frontend/packages/data-portal/tailwind.config.ts index b8467c50e..e159c8afd 100644 --- a/frontend/packages/data-portal/tailwind.config.ts +++ b/frontend/packages/data-portal/tailwind.config.ts @@ -5,7 +5,13 @@ import type { Config } from 'tailwindcss' export default { content: ['./app/**/*.{ts,tsx,scss}'], theme: { - extend: sds, + extend: { + ...sds, + + maxWidth: { + content: '1600px', + }, + }, }, plugins: [], } satisfies Config