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